Self written tape stuff
This commit is contained in:
@@ -136,6 +136,7 @@ Assembly applications include `include/bios.inc` and call these fixed entries:
|
||||
| `C065` | `BIOS_BANK` | `A=0`: split lower 48K/upper 16K; otherwise select upper 64K |
|
||||
| `C068` | `BIOS_ROM_DISABLE` | Disable ROM while retaining the selected RAM mapping |
|
||||
| `C06B` | `BIOS_ROM_ENABLE` | Enable ROM while retaining the selected RAM mapping |
|
||||
| `C06E` | `BIOS_FLUSH` | Wait until the UART output queue is empty |
|
||||
|
||||
The service wrappers preserve working registers except for documented return
|
||||
values. The monitor uses the corresponding C API declared in `bios.h`.
|
||||
|
||||
+45
-131
@@ -1,22 +1,4 @@
|
||||
#!/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
|
||||
@@ -25,29 +7,16 @@ 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")
|
||||
def __init__(self, sample_rate: int, c_low: int, c_high: int, f_low: float, f_high: float, amplitude: float):
|
||||
self.sample_rate = sample_rate
|
||||
self.baud = baud
|
||||
self.c_low = c_low
|
||||
self.c_high = c_high
|
||||
self.f_low = f_low
|
||||
self.f_high = f_high
|
||||
self.amplitude = amplitude
|
||||
self.samples_per_half_bit = sample_rate // (2 * baud)
|
||||
self.samples_per_low = (sample_rate * c_low) // f_low
|
||||
self.samples_per_high = (sample_rate * c_high) // f_high
|
||||
self.phase = 0.0
|
||||
self.samples = bytearray()
|
||||
|
||||
@@ -57,7 +26,7 @@ class FSKWriter:
|
||||
phase = self.phase
|
||||
out = self.samples
|
||||
|
||||
for _ in range(sample_count):
|
||||
for _ in range(int(sample_count)):
|
||||
sample = int(round(scale * math.sin(phase)))
|
||||
out.extend(struct.pack("<h", sample))
|
||||
phase += phase_step
|
||||
@@ -66,20 +35,19 @@ class FSKWriter:
|
||||
|
||||
self.phase = phase
|
||||
|
||||
def leader(self, seconds: float) -> None:
|
||||
self._tone(self.f_high, round(seconds * self.sample_rate))
|
||||
def leader(self) -> None:
|
||||
self._tone(self.f_high, (200*self.sample_rate)//self.f_high)
|
||||
self._tone(self.f_low, (200*self.sample_rate)//self.f_low)
|
||||
for _ in range(8):
|
||||
self.bit(1)
|
||||
self.bit(0)
|
||||
self.byte(0x0f)
|
||||
|
||||
def bit(self, value: int) -> None:
|
||||
# Manchester-FSK:
|
||||
# 0 = low -> high
|
||||
# 1 = high -> low
|
||||
if value:
|
||||
first, second = self.f_high, self.f_low
|
||||
self._tone(self.f_high, self.samples_per_high)
|
||||
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)
|
||||
self._tone(self.f_low, self.samples_per_low)
|
||||
|
||||
def byte(self, value: int) -> None:
|
||||
# MSB first
|
||||
@@ -90,100 +58,46 @@ class FSKWriter:
|
||||
for value in data:
|
||||
self.byte(value)
|
||||
|
||||
def block(self, nr: int, data: bytes) -> None:
|
||||
self.leader()
|
||||
self.byte(nr)
|
||||
l = len(data)
|
||||
self.byte(l>>8)
|
||||
self.byte(l&0xff)
|
||||
self.bytes(data)
|
||||
self.byte(self.chsum(data))
|
||||
|
||||
def make_block(sequence: int, payload: bytes) -> bytes:
|
||||
header = struct.pack("<HH", sequence & 0xFFFF, len(payload))
|
||||
crc = crc16_ccitt_false(header + payload)
|
||||
return SYNC + header + payload + struct.pack("<H", crc)
|
||||
def chsum(self, data: bytes) -> int:
|
||||
cs = 0
|
||||
for d in data:
|
||||
cs += int(d)
|
||||
return cs
|
||||
|
||||
|
||||
def make_eof_block(sequence: int) -> bytes:
|
||||
header = struct.pack("<HH", sequence & 0xFFFF, 0)
|
||||
crc = crc16_ccitt_false(header)
|
||||
return SYNC + header + struct.pack("<H", crc)
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Encode a binary file as two tone FSK WAV")
|
||||
parser.add_argument("input", type=Path, help="Binary input file")
|
||||
parser.add_argument("output", type=Path, help="output WAVl default <input>.wav")
|
||||
parser.add_argument("--sample-rate", type=int, default=48000)
|
||||
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("--low-cycles", type=int, default=2, help="low tone cycle count for a 0")
|
||||
parser.add_argument("--high-cycles", type=int, default=4, help="high tone cycle count for a 1")
|
||||
parser.add_argument("--amplitude", type=float, default=0.75)
|
||||
parser.add_argument("--block-size", type=int, default=64)
|
||||
args = parser.parse_args()
|
||||
|
||||
output_path = args.output or args.input.with_name(args.input.stem + ".wav")
|
||||
|
||||
def encode(
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
*,
|
||||
sample_rate: int = 48_000,
|
||||
baud: int = 600,
|
||||
f_low: float = 2_000.0,
|
||||
f_high: float = 4_000.0,
|
||||
leader_seconds: float = 1.0,
|
||||
preamble_bytes: int = 64,
|
||||
block_size: int = 256,
|
||||
amplitude: float = 0.75,
|
||||
) -> None:
|
||||
data = input_path.read_bytes()
|
||||
tones = FSKWriter(args.sample_rate, args.low_cycles, args.high_cycles, args.low, args.high, args.amplitude)
|
||||
tones.block(0, args.input.name.encode())
|
||||
|
||||
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=150)
|
||||
parser.add_argument("--low", type=float, default=1000.0, help="low tone in Hz")
|
||||
parser.add_argument("--high", type=float, default=2000.0, help="high tone in Hz")
|
||||
parser.add_argument("--sample-rate", type=int, default=48000)
|
||||
parser.add_argument("--leader", type=float, default=4.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,
|
||||
)
|
||||
|
||||
wav.setframerate(args.sample_rate)
|
||||
wav.writeframes(tones.samples)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+211
-1521
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,6 +19,7 @@ BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs
|
||||
.globl _bios_putc
|
||||
.globl _bios_try_getc
|
||||
.globl _bios_getc
|
||||
.globl _bios_flush
|
||||
.globl _bios_init
|
||||
.globl _bios_bank
|
||||
.globl _bios_rom_disable
|
||||
@@ -79,7 +80,7 @@ boot_copy:
|
||||
; Stable BIOS jump table for ROM-resident applications.
|
||||
; C050 putc(A), C053 try_getc(), C056 getc(), C059 io_out(A,L),
|
||||
; C05C io_in(A), C05F delay_ms(HL), C062 monitor warm start,
|
||||
; C065 bank(A), C068 disable ROM, C06B enable ROM.
|
||||
; C065 bank(A), C068 disable ROM, C06B enable ROM, C06E flush output.
|
||||
.org BIOS_PUTC
|
||||
jp bios_service_putc
|
||||
.org BIOS_TRY_GETC
|
||||
@@ -100,11 +101,13 @@ boot_copy:
|
||||
jp _bios_rom_disable
|
||||
.org BIOS_ROM_ENABLE
|
||||
jp _bios_rom_enable
|
||||
.org BIOS_FLUSH
|
||||
jp bios_service_flush
|
||||
|
||||
; Port 00 is write-only. Keep its last BIOS-managed value outside the C data
|
||||
; areas so a monitor warm start does not reset it. The bootstrap establishes
|
||||
; this same value in the hardware before the resident image is copied.
|
||||
.org RESIDENT_RAM_BASE + 0x70
|
||||
.org RESIDENT_RAM_BASE + 0x73
|
||||
_bank_control_shadow::
|
||||
.db BOOT_BANK_MAPPING
|
||||
|
||||
@@ -258,6 +261,22 @@ bios_service_getc:
|
||||
pop bc
|
||||
ret
|
||||
|
||||
bios_service_flush:
|
||||
push af
|
||||
push bc
|
||||
push de
|
||||
push hl
|
||||
push ix
|
||||
push iy
|
||||
call _bios_flush
|
||||
pop iy
|
||||
pop ix
|
||||
pop hl
|
||||
pop de
|
||||
pop bc
|
||||
pop af
|
||||
ret
|
||||
|
||||
bios_service_io_out:
|
||||
push af
|
||||
push bc
|
||||
|
||||
@@ -11,3 +11,4 @@ BIOS_MONITOR = RESIDENT_RAM_BASE + 0x62 ; warm-start the monitor; does not r
|
||||
BIOS_BANK = RESIDENT_RAM_BASE + 0x65 ; A=0 split 48K/16K, otherwise upper 64K
|
||||
BIOS_ROM_DISABLE = RESIDENT_RAM_BASE + 0x68 ; disable ROM while preserving the bank selection
|
||||
BIOS_ROM_ENABLE = RESIDENT_RAM_BASE + 0x6B ; enable ROM while preserving the bank selection
|
||||
BIOS_FLUSH = RESIDENT_RAM_BASE + 0x6E ; wait until all queued UART output is sent
|
||||
|
||||
Reference in New Issue
Block a user