Skip to content

Instantly share code, notes, and snippets.

@alexanderk23
Created November 6, 2021 09:16
Show Gist options
  • Save alexanderk23/fd11f727d0ad90499d67577cc76b79fd to your computer and use it in GitHub Desktop.
Save alexanderk23/fd11f727d0ad90499d67577cc76b79fd to your computer and use it in GitHub Desktop.
Convert TAP to WAV
#!/usr/bin/env python
import argparse
import struct
import wave
SECONDS_PAUSE = 1.5
SECONDS_PILOT_HEADER = 3
SECONDS_PILOT_DATA = 1.5
class StandardTimings:
TSTATES_SYNC_1 = 667
TSTATES_SYNC_2 = 735
TSTATES_ZERO = 855
TSTATES_ONE = 1710
TSTATES_PILOT = 2168
class WeirdTimings:
TSTATES_SYNC_1 = 667 * 0.85
TSTATES_SYNC_2 = 735 * 0.85
TSTATES_ZERO = 855 * 0.85
TSTATES_ONE = 1710 * 0.85
TSTATES_PILOT = 2168 * 0.85
class TapReader:
def __init__(self, filename):
self.filename = filename
def blocks(self):
with open(self.filename, 'rb') as f:
while True:
bs = f.read(2)
if not bs:
break
block_size = struct.unpack('<H', bs)[0]
block = f.read(block_size)
yield block
class WavWriter:
def __init__(self, filename, timings=StandardTimings, rate=48000):
self.timings = timings
self.level = 0
self.rate = rate
self.wav = wave.open(filename, 'wb')
self.wav.setnchannels(1)
self.wav.setsampwidth(1)
self.wav.setframerate(self.rate)
def close(self):
self.wav.close()
def bits(self, iterable):
for byte in iterable:
yield from ((byte >> (7 - i)) & 1 for i in range(8))
def tstates_to_samples(self, tstates):
return int(round(self.rate * tstates / 3500000.0))
def emit_n_samples(self, count):
self.wav.writeframesraw(bytes([self.level] * count))
self.level ^= 0xFF
def emit_samples(self, tstates):
count = self.tstates_to_samples(tstates)
self.emit_n_samples(count)
def emit_pause(self, seconds=SECONDS_PAUSE):
count = int(self.rate * seconds)
self.emit_n_samples(count)
def emit_pulse(self, tstates):
for _ in range(2):
self.emit_samples(tstates)
def emit_sync(self):
self.emit_samples(self.timings.TSTATES_SYNC_1)
self.emit_samples(self.timings.TSTATES_SYNC_2)
def emit_pilot(self, seconds=SECONDS_PILOT_HEADER):
count = int(self.rate * seconds /
self.tstates_to_samples(self.timings.TSTATES_PILOT) / 2)
for _ in range(count):
self.emit_pulse(self.timings.TSTATES_PILOT)
def emit_bit(self, bit):
self.emit_pulse(
self.timings.TSTATES_ONE if bit else self.timings.TSTATES_ZERO)
def emit_block(self, block):
is_header = block[0] == 0x00
self.emit_pilot(
SECONDS_PILOT_HEADER if is_header else SECONDS_PILOT_DATA)
self.emit_sync()
for bit in self.bits(block):
self.emit_bit(bit)
self.emit_pause()
class TapToWavConverter:
def convert(self, tap, wav, timings=StandardTimings, rate=48000):
tap_reader = TapReader(tap)
wav_writer = WavWriter(wav, timings, rate)
for block in tap_reader.blocks():
wav_writer.emit_block(block)
wav_writer.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Convert TAP to WAV')
parser.add_argument('source', help='Source .tap file name')
parser.add_argument('dest', help='Dest .wav file name',
nargs='?', default=None)
args = parser.parse_args()
dest = args.dest or f'{args.source}.wav'
print(f'{args.source} -> {dest}')
converter = TapToWavConverter()
converter.convert(args.source, dest, StandardTimings)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment