joeylib2/examples/spacetaxi/assets/genSpeechPcm.py

106 lines
4.2 KiB
Python
Executable file

#!/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("<BBBBBBBBI", 2, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, 0, 0, 0, len(blob)))
for code, parity, n, off in records:
f.write(struct.pack("<BBHI", code, parity, n, off))
f.write(blob)
total = sum(n for _, _, n, _ in records)
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x 2 polarities, {total} samples, {len(blob)} data bytes, {'biased' if biased else 'signed'}")
if __name__ == "__main__":
main()