#!/usr/bin/env python3 """genSpeechPcm.py - bake Space Taxi's speech to 8-bit PCM. Reads the C64 run-length speech streams from stSpeechData.c (the output of extractSpeech.py) and writes speech.bin: every utterance decoded to 5 kHz sample bytes for each passenger pitch the game can select, in BOTH polarities. The decode is the exact algorithm stAudio.c used to run per sample at play time; on the 65816 that took 2.4 s of CPU per second of speech (three run bytes per sample, each a multiply) and stalled the ride for half a second on every "HEY TAXI". A 1-bit intermediate format came next, but expanding bits still needed a per-byte loop (and an asm copy of it on the IIgs); with sample bytes the player is a block copy. Polarity: an utterance is decoded from a +100 start. The C64 keeps the output level running from one utterance into the next, so a phrase whose first word ends on the low level plays its second word inverted. Both versions are stored and the player picks by the running parity. speech.bin (little-endian): "STSP" u8 version(2) u8 pitchBase u8 pitchCount u8 count u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream) u8 pad[3] u32 dataBytes entries[pitchCount * count * 2]: u8 code, u8 parity, u16 samples, u32 offset -- polarity is the innermost index (0 = as decoded, 1 = inverted). parity = 1 when the utterance ends on the opposite level from the one it started on. data: sample bytes usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased] """ import re import struct import sys SID_HZ = 1022727 # ST_SID_HZ_NUM RATE = 5000 # ST_SPEECH_RATE CYC_PER_SAMPLE = SID_HZ // RATE SYNC_CYCLES = SID_HZ // 60 // 8 CYCLE_TAIL = 33 SYNC = 0xFE END = 0xFF PITCH_BASE = 2 # stFare.c: stAudioSpeech(rng(3) + 2) PITCH_COUNT = 3 def parseSource(path): text = open(path).read() idx = re.search(r"kStSpeechIndex\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1) entries = [(int(c, 16), int(o), int(n)) for c, o, n in re.findall(r"\{\s*0x([0-9A-Fa-f]+)u,\s*(\d+)u,\s*(\d+)u\s*\}", idx)] rle = re.search(r"kStSpeechRle\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1) data = bytes(int(x, 16) for x in re.findall(r"0x([0-9A-Fa-f]{2})", rle)) return entries, data def decode(rle, perOn): """speechDecode() for one utterance from a +100 start, remain 0.""" remain = 0 level = 100 out = [] i = 0 n = len(rle) while True: while remain <= 0: if i >= n: return out, level b = rle[i] i += 1 if b == END: i = n continue if b == SYNC: remain += SYNC_CYCLES continue remain += perOn * b + CYCLE_TAIL level = -100 if level > 0 else 100 out.append(level) remain -= CYC_PER_SAMPLE def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] biased = "--biased" in sys.argv[1:] src, dst = args[0], args[1] entries, data = parseSource(src) records = [] blob = bytearray() for p in range(PITCH_COUNT): perOn = 5 * (PITCH_BASE + p) + 12 for code, off, length in entries: samples, level = decode(data[off:off + length], perOn) parity = 1 if level < 0 else 0 for pol in range(2): pcm = bytearray() for s in samples: v = -s if pol else s pcm.append((v + 0x80) & 0xFF if biased else v & 0xFF) records.append((code, parity, len(samples), len(blob))) blob += pcm with open(dst, "wb") as f: f.write(b"STSP" + struct.pack("