Created
November 6, 2021 09:16
-
-
Save alexanderk23/fd11f727d0ad90499d67577cc76b79fd to your computer and use it in GitHub Desktop.
Convert TAP to WAV
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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