Created
September 24, 2025 07:30
-
-
Save nuxeh/592722b67c8a64ada88b18ddb23a8698 to your computer and use it in GitHub Desktop.
7-bit sysex chunking encoder/decoder for SMC midi device
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
7-bit encoding/decoding utility for packet data. | |
This script can encode 8-bit data into 7-bit format or decode 7-bit data back to 8-bit. | |
Handles framed packets with F0/F7 markers. | |
""" | |
import sys | |
import argparse | |
from typing import List, Union | |
def encode_7bit(data: bytes) -> bytes: | |
""" | |
Encode 8-bit data into 7-bit format. | |
Args: | |
data: Input bytes to encode | |
Returns: | |
Encoded bytes where each byte has MSB = 0 | |
""" | |
if not data: | |
return b'' | |
result = [] | |
bit_accumulator = 0 | |
bits_in_accumulator = 0 | |
for byte in data: | |
# Add 8 bits from current byte | |
bit_accumulator |= byte << bits_in_accumulator | |
bits_in_accumulator += 8 | |
# Output 7-bit chunks while we have enough bits | |
while bits_in_accumulator >= 7: | |
result.append(bit_accumulator & 0x7F) | |
bit_accumulator >>= 7 | |
bits_in_accumulator -= 7 | |
# Output any remaining bits | |
if bits_in_accumulator > 0: | |
result.append(bit_accumulator & 0x7F) | |
return bytes(result) | |
def decode_7bit(data: bytes) -> bytes: | |
""" | |
Decode 7-bit data back to 8-bit format. | |
Args: | |
data: 7-bit encoded bytes to decode | |
Returns: | |
Original 8-bit data | |
""" | |
if not data: | |
return b'' | |
result = [] | |
bit_accumulator = 0 | |
bits_in_accumulator = 0 | |
for byte in data: | |
# Ensure input is 7-bit (MSB should be 0) | |
byte &= 0x7F | |
# Add 7 bits from current byte | |
bit_accumulator |= byte << bits_in_accumulator | |
bits_in_accumulator += 7 | |
# Output 8-bit chunks while we have enough bits | |
while bits_in_accumulator >= 8: | |
result.append(bit_accumulator & 0xFF) | |
bit_accumulator >>= 8 | |
bits_in_accumulator -= 8 | |
return bytes(result) | |
def frame_packet(data: bytes, start_marker: int = 0xF0, end_marker: int = 0xF7) -> bytes: | |
""" | |
Add framing markers around data. | |
Args: | |
data: Data to frame | |
start_marker: Start frame marker (default 0xF0) | |
end_marker: End frame marker (default 0xF7) | |
Returns: | |
Framed data | |
""" | |
return bytes([start_marker]) + data + bytes([end_marker]) | |
def unframe_packet(data: bytes, start_marker: int = 0xF0, end_marker: int = 0xF7) -> bytes: | |
""" | |
Remove framing markers from data. | |
Args: | |
data: Framed data | |
start_marker: Expected start frame marker (default 0xF0) | |
end_marker: Expected end frame marker (default 0xF7) | |
Returns: | |
Unframed data | |
Raises: | |
ValueError: If framing markers are invalid | |
""" | |
if len(data) < 2: | |
raise ValueError("Data too short for framing markers") | |
if data[0] != start_marker: | |
raise ValueError(f"Invalid start marker: expected 0x{start_marker:02X}, got 0x{data[0]:02X}") | |
if data[-1] != end_marker: | |
raise ValueError(f"Invalid end marker: expected 0x{end_marker:02X}, got 0x{data[-1]:02X}") | |
return data[1:-1] | |
def parse_hex_line(line: str) -> bytes: | |
""" | |
Parse a line of space-separated hex bytes. | |
Args: | |
line: String like "F0 00 32 09 F7" | |
Returns: | |
Bytes object | |
""" | |
hex_bytes = line.strip().split() | |
return bytes(int(b, 16) for b in hex_bytes if b) | |
def format_hex_line(data: bytes) -> str: | |
""" | |
Format bytes as space-separated hex string. | |
Args: | |
data: Bytes to format | |
Returns: | |
String like "F0 00 32 09 F7" | |
""" | |
return ' '.join(f'{b:02X}' for b in data) | |
def encode_packet(packet_data: bytes) -> bytes: | |
""" | |
Complete packet encoding: 7-bit encode + frame. | |
Args: | |
packet_data: Raw packet data | |
Returns: | |
Framed, 7-bit encoded packet | |
""" | |
encoded = encode_7bit(packet_data) | |
return frame_packet(encoded) | |
def decode_packet(framed_packet: bytes) -> bytes: | |
""" | |
Complete packet decoding: unframe + 7-bit decode. | |
Args: | |
framed_packet: Framed, 7-bit encoded packet | |
Returns: | |
Original raw packet data | |
""" | |
unframed = unframe_packet(framed_packet) | |
return decode_7bit(unframed) | |
def process_file(input_file, output_file, operation: str): | |
""" | |
Process input file line by line. | |
Args: | |
input_file: Input file handle | |
output_file: Output file handle | |
operation: 'encode' or 'decode' | |
""" | |
line_num = 0 | |
for line in input_file: | |
line_num += 1 | |
line = line.strip() | |
if not line or line.startswith('#'): | |
continue | |
try: | |
input_bytes = parse_hex_line(line) | |
if operation == 'encode': | |
# Encode: raw data -> 7-bit encoded + framed | |
output_bytes = encode_packet(input_bytes) | |
elif operation == 'decode': | |
# Decode: framed + 7-bit encoded -> raw data | |
output_bytes = decode_packet(input_bytes) | |
else: | |
raise ValueError(f"Unknown operation: {operation}") | |
output_line = format_hex_line(output_bytes) | |
output_file.write(output_line + '\n') | |
except Exception as e: | |
print(f"Error processing line {line_num}: {e}", file=sys.stderr) | |
print(f"Line content: {line}", file=sys.stderr) | |
continue | |
def main(): | |
parser = argparse.ArgumentParser( | |
description='7-bit encode/decode utility for packet data', | |
formatter_class=argparse.RawDescriptionHelpFormatter, | |
epilog=""" | |
Examples: | |
# Encode raw packets to 7-bit framed format | |
python sevenbit_codec.py encode < raw_packets.txt > encoded_packets.txt | |
# Decode framed packets back to raw format | |
python sevenbit_codec.py decode < encoded_packets.txt > raw_packets.txt | |
# Process from file | |
python sevenbit_codec.py encode -i input.txt -o output.txt | |
Input format: Space-separated hex bytes, one packet per line | |
00 59 22 0B 00 00 05 45 | |
Output format: Same format, processed according to operation | |
""" | |
) | |
parser.add_argument( | |
'operation', | |
choices=['encode', 'decode'], | |
help='Operation to perform' | |
) | |
parser.add_argument( | |
'-i', '--input', | |
type=argparse.FileType('r'), | |
default=sys.stdin, | |
help='Input file (default: stdin)' | |
) | |
parser.add_argument( | |
'-o', '--output', | |
type=argparse.FileType('w'), | |
default=sys.stdout, | |
help='Output file (default: stdout)' | |
) | |
args = parser.parse_args() | |
try: | |
process_file(args.input, args.output, args.operation) | |
except KeyboardInterrupt: | |
print("\nInterrupted by user", file=sys.stderr) | |
sys.exit(1) | |
except Exception as e: | |
print(f"Error: {e}", file=sys.stderr) | |
sys.exit(1) | |
finally: | |
if args.input != sys.stdin: | |
args.input.close() | |
if args.output != sys.stdout: | |
args.output.close() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment