#!/usr/bin/env python3 """ z80_tape_encode.py Encode a binary file as a mono PCM WAV using robust two-tone Manchester FSK. Defaults: - 48 kHz sample rate - 600 bit/s - 2 kHz / 4 kHz tones - Manchester FSK: 0 -> low tone for half a bit, then high tone 1 -> high tone for half a bit, then low tone - 1 second 4 kHz leader - 64 bytes of 0x55 preamble - 256-byte CRC-protected blocks - CRC-16/CCITT-FALSE """ from __future__ import annotations import argparse import math import struct import wave from pathlib import Path SYNC = b"\xD3\x91" PREAMBLE_BYTE = 0x55 def crc16_ccitt_false(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if (crc & 0x8000) else (crc << 1) & 0xFFFF return crc class FSKWriter: def __init__(self, sample_rate: int, baud: int, f_low: float, f_high: float, amplitude: float): if sample_rate % (2 * baud) != 0: raise ValueError("sample_rate must be divisible by 2*baud") self.sample_rate = sample_rate self.baud = baud self.f_low = f_low self.f_high = f_high self.amplitude = amplitude self.samples_per_half_bit = sample_rate // (2 * baud) self.phase = 0.0 self.samples = bytearray() def _tone(self, frequency: float, sample_count: int) -> None: phase_step = 2.0 * math.pi * frequency / self.sample_rate scale = int(32767 * self.amplitude) phase = self.phase out = self.samples for _ in range(sample_count): sample = int(round(scale * math.sin(phase))) out.extend(struct.pack("= 2.0 * math.pi: phase -= 2.0 * math.pi self.phase = phase def leader(self, seconds: float) -> None: self._tone(self.f_high, round(seconds * self.sample_rate)) def bit(self, value: int) -> None: # Manchester-FSK: # 0 = low -> high # 1 = high -> low if value: first, second = self.f_high, self.f_low else: first, second = self.f_low, self.f_high self._tone(first, self.samples_per_half_bit) self._tone(second, self.samples_per_half_bit) def byte(self, value: int) -> None: # MSB first for bit_no in range(7, -1, -1): self.bit((value >> bit_no) & 1) def bytes(self, data: bytes) -> None: for value in data: self.byte(value) def make_block(sequence: int, payload: bytes) -> bytes: header = struct.pack(" bytes: header = struct.pack(" None: data = input_path.read_bytes() if not 0 < amplitude <= 1.0: raise ValueError("amplitude must be > 0 and <= 1") if not 1 <= block_size <= 0xFFFF: raise ValueError("block_size must be in 1..65535") writer = FSKWriter(sample_rate, baud, f_low, f_high, amplitude) writer.leader(leader_seconds) writer.bytes(bytes([PREAMBLE_BYTE]) * preamble_bytes) sequence = 0 for start in range(0, len(data), block_size): payload = data[start:start + block_size] writer.bytes(make_block(sequence, payload)) sequence = (sequence + 1) & 0xFFFF writer.bytes(make_eof_block(sequence)) with wave.open(str(output_path), "wb") as wav: wav.setnchannels(1) wav.setsampwidth(2) wav.setframerate(sample_rate) wav.writeframes(writer.samples) duration = len(writer.samples) / 2 / sample_rate print(f"Input: {input_path}") print(f"Output: {output_path}") print(f"Input bytes: {len(data)}") print(f"Bit rate: {baud} bit/s") print(f"Tones: {f_low:g} Hz / {f_high:g} Hz") print(f"Sample rate: {sample_rate} Hz") print(f"Half-bit: {writer.samples_per_half_bit} samples") print(f"Block size: {block_size} bytes") print(f"WAV duration: {duration:.2f} s") def main() -> None: parser = argparse.ArgumentParser(description="Encode a binary file as Manchester two-tone FSK WAV.") parser.add_argument("input", type=Path, help="binary input file") parser.add_argument("output", type=Path, nargs="?", help="output WAV; default: INPUT.wav") parser.add_argument("--baud", type=int, default=600) parser.add_argument("--low", type=float, default=2000.0, help="low tone in Hz") parser.add_argument("--high", type=float, default=4000.0, help="high tone in Hz") parser.add_argument("--sample-rate", type=int, default=48000) parser.add_argument("--leader", type=float, default=1.0, help="leader seconds") parser.add_argument("--preamble", type=int, default=64, help="number of 0x55 preamble bytes") parser.add_argument("--block-size", type=int, default=64) parser.add_argument("--amplitude", type=float, default=0.75) args = parser.parse_args() output = args.output or args.input.with_name(args.input.name + ".wav") encode( args.input, output, sample_rate=args.sample_rate, baud=args.baud, f_low=args.low, f_high=args.high, leader_seconds=args.leader, preamble_bytes=args.preamble, block_size=args.block_size, amplitude=args.amplitude, ) if __name__ == "__main__": main()