Skip to content

Instantly share code, notes, and snippets.

@nuxeh
Created September 24, 2025 07:30
Show Gist options
  • Save nuxeh/592722b67c8a64ada88b18ddb23a8698 to your computer and use it in GitHub Desktop.
Save nuxeh/592722b67c8a64ada88b18ddb23a8698 to your computer and use it in GitHub Desktop.
7-bit sysex chunking encoder/decoder for SMC midi device
#!/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