136 lines
4.0 KiB
Python
136 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import struct
|
|
import wave
|
|
from pathlib import Path
|
|
|
|
|
|
class PulseWriter:
|
|
def __init__(
|
|
self,
|
|
sample_rate: int,
|
|
t_short_us: float,
|
|
t_long_us: float,
|
|
amplitude: float,
|
|
) -> None:
|
|
self.sample_rate = sample_rate
|
|
self.short_samples = self._duration_samples(t_short_us)
|
|
self.long_samples = self._duration_samples(t_long_us)
|
|
self.high = round(32767 * amplitude)
|
|
self.low = -self.high
|
|
self.samples = bytearray()
|
|
|
|
self.space(0.1)
|
|
|
|
def space(self, time: float) -> None:
|
|
self._level(0, int(time*self.sample_rate))
|
|
|
|
def _duration_samples(self, duration_us: float) -> int:
|
|
sample_count = round(duration_us * self.sample_rate / 1_000_000)
|
|
if sample_count < 1:
|
|
raise ValueError(
|
|
f"pulse duration {duration_us:g} us is shorter than one sample"
|
|
)
|
|
return sample_count
|
|
|
|
def _level(self, value: int, sample_count: int) -> None:
|
|
self.samples.extend(struct.pack("<h", value) * sample_count)
|
|
|
|
def short_pulse(self) -> None:
|
|
self._level(self.high, self.short_samples)
|
|
self._level(self.low, self.long_samples)
|
|
|
|
def long_pulse(self) -> None:
|
|
self._level(self.high, self.long_samples)
|
|
self._level(self.low, self.short_samples)
|
|
|
|
def marker(self) -> None:
|
|
self._level(self.high, 2*self.long_samples)
|
|
self._level(self.low, 2*self.long_samples)
|
|
|
|
def bit(self, value: int) -> None:
|
|
if value:
|
|
self.long_pulse()
|
|
else:
|
|
self.short_pulse()
|
|
|
|
def byte(self, value: int) -> None:
|
|
# Transmit bytes most-significant bit first.
|
|
for bit_number in range(7, -1, -1):
|
|
self.bit((value >> bit_number) & 1)
|
|
|
|
def bytes(self, value: bytes) -> None:
|
|
for byte in value:
|
|
self.byte(byte)
|
|
|
|
def leader(self) -> None:
|
|
self.space(0.2)
|
|
for _ in range(50):
|
|
self._level(self.high, self.short_samples)
|
|
self._level(self.low, self.short_samples)
|
|
for _ in range(50):
|
|
self._level(self.high, self.long_samples)
|
|
self._level(self.low, self.long_samples)
|
|
self.marker()
|
|
for value in (0x55, 0x5A, 0xC3):
|
|
self.byte(value)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate the pulse-width encoded tape leader"
|
|
)
|
|
parser.add_argument("output", type=Path, help="output WAV file")
|
|
parser.add_argument("--sample-rate", type=int, default=48_000)
|
|
parser.add_argument(
|
|
"--t-short",
|
|
type=float,
|
|
default=250.0,
|
|
metavar="MICROSECONDS",
|
|
help="short pulse interval in microseconds (default: 250)",
|
|
)
|
|
parser.add_argument(
|
|
"--t-long",
|
|
type=float,
|
|
default=500.0,
|
|
metavar="MICROSECONDS",
|
|
help="long pulse interval in microseconds (default: 500)",
|
|
)
|
|
parser.add_argument("--amplitude", type=float, default=0.75)
|
|
args = parser.parse_args()
|
|
|
|
if args.sample_rate <= 0:
|
|
parser.error("--sample-rate must be positive")
|
|
if args.t_short <= 0 or args.t_long <= 0:
|
|
parser.error("pulse intervals must be positive")
|
|
if args.t_short >= args.t_long:
|
|
parser.error("--t-short must be less than --t-long")
|
|
if not 0.0 <= args.amplitude <= 1.0:
|
|
parser.error("--amplitude must be between 0 and 1")
|
|
|
|
try:
|
|
pulses = PulseWriter(
|
|
args.sample_rate,
|
|
args.t_short,
|
|
args.t_long,
|
|
args.amplitude,
|
|
)
|
|
except ValueError as error:
|
|
parser.error(str(error))
|
|
|
|
pulses.leader()
|
|
pulses.bytes(b'Hello world!\0')
|
|
|
|
pulses.space(1.0)
|
|
with wave.open(str(args.output), "wb") as wav:
|
|
wav.setnchannels(1)
|
|
wav.setsampwidth(2)
|
|
wav.setframerate(args.sample_rate)
|
|
wav.writeframes(pulses.samples)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|