106 lines
4.2 KiB
Python
Executable file
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()
|