577 lines
20 KiB
C
577 lines
20 KiB
C
// Space Taxi -- audio sink for the simulation's SID events.
|
|
//
|
|
// The C64 drives three SID voices: voice 1 carries the SFX programs
|
|
// (landing, gear, crash, fuel) and the thrust/crash frequency sweep,
|
|
// voice 2 the level gimmick tones (laser hum, puzzle chime), voice 3
|
|
// the jet noise. JoeyLib's audio HAL is a PSG-style tone/noise
|
|
// interface, so each SID program is mapped to its pitch and release
|
|
// time; envelopes and waveforms are approximated with an attenuation
|
|
// ramp. The speech samples of the original are not extracted; each
|
|
// spoken character becomes a short chirp so the cues still land.
|
|
//
|
|
// UPDATE: the speech IS extracted now (assets/extractSpeech.py), so the
|
|
// chirp is gone. stSpeechData.c carries the C64's own 1-bit run-length
|
|
// streams; assets/genSpeechPcm.py bakes them to sample bytes
|
|
// (DATA/speech.bin) and speechFill below streams those through
|
|
// jlAudioPlaySfxStream.
|
|
|
|
#include <string.h>
|
|
|
|
#include "spacetaxi.h"
|
|
#include <joey/file.h>
|
|
#include "stSongData.h"
|
|
|
|
|
|
// SID voice index in a program's byte 8 -> JoeyLib voice slot.
|
|
#define ST_VOICE_SFX 1u // SID voice 1
|
|
#define ST_VOICE_TONE 2u // SID voice 2
|
|
#define ST_VOICE_NOISE 3u // SID voice 3 (noise)
|
|
|
|
#define ST_ATTEN_LOUD 4u
|
|
#define ST_ATTEN_SOFT 8u
|
|
#define ST_ATTEN_OFF 15u
|
|
|
|
// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096.
|
|
#define ST_SID_HZ_NUM 1022727UL
|
|
#define ST_SID_HZ_DEN 16777216UL
|
|
|
|
// ---- speech ----
|
|
//
|
|
// The C64 player ($9802) busy-waits a delay loop per stream byte and then
|
|
// toggles its 1-bit output, so a byte is one half-cycle. The loop costs
|
|
// (5k + 12) cycles per pass plus a 33-cycle tail, where k is the inner
|
|
// count at $98A2 -- normally 3, giving 27. A code below $09 does not
|
|
// speak: it REWRITES k, which is how the original gives each fare a
|
|
// slightly different voice pitch. stAudioSpeech honours that.
|
|
#define ST_SPEECH_SLOT 0u // slots 1..3 are the SID voices
|
|
#define ST_SPEECH_RATE 5000u // enough for a 1-bit source
|
|
#define ST_SPEECH_PITCH_BASE 3u // $98A2's power-on value
|
|
#define ST_SPEECH_PITCH_MAX 15u // the player's AND #$0F
|
|
// Longest phrase is "PX?" -- pad, digit, please.
|
|
#define ST_SPEECH_QUEUE 4u
|
|
// The speech is baked to sample bytes per pitch, in both polarities, by
|
|
// assets/genSpeechPcm.py (DATA/speech.bin, format in that script), so
|
|
// playing it is a block copy. Decoding the C64's run-length stream at
|
|
// play time cost the 65816 2.4 s of CPU per second of speech and stalled
|
|
// the ride half a second on every "HEY TAXI"; the 1-bit intermediate
|
|
// format that followed still needed a per-byte expansion loop.
|
|
#define ST_SPEECH_FILE "speech.bin"
|
|
#define ST_SPEECH_VERSION 2u
|
|
#define ST_SPEECH_HEADER_BYTES 16u
|
|
#define ST_SPEECH_ENTRY_BYTES 8u
|
|
#define ST_SPEECH_POLARITIES 2u
|
|
#define ST_SPEECH_FORMAT_SIGNED 0u
|
|
#define ST_SPEECH_FORMAT_BIASED 1u
|
|
#define ST_SPEECH_LOAD_CHUNK 16384u
|
|
#define ST_SPEECH_STAGE_MASK (ST_SPEECH_STAGE_BYTES - 1u)
|
|
// Decode-ahead staging: a whole engine chunk (7168 at 5 kHz, 1.4 s) plus
|
|
// slack. The per-tick slice covers playback (5000/s is ~330 a tick)
|
|
// with room to spare and is cheap enough to never need a burst; a
|
|
// starved pull still copies ST_SPEECH_STARVED_SLICE on the spot rather
|
|
// than ending the stream.
|
|
#define ST_SPEECH_STAGE_BYTES 8192u
|
|
#define ST_SPEECH_TICK_SLICE 384u
|
|
#define ST_SPEECH_STARVED_SLICE 1024u
|
|
#define ST_SPEECH_ARM_SLICE 1024u
|
|
|
|
// Thrust sweep register value -> Hz: the C64 writes the same byte to
|
|
// both frequency bytes, so the word is value * 257.
|
|
#define ST_SWEEP_WORD_SCALE 257u
|
|
|
|
|
|
typedef struct {
|
|
uint16_t ticksLeft; // release countdown (game ticks); 0 = idle
|
|
uint8_t voice;
|
|
} StSfxSlotT;
|
|
|
|
|
|
static StSfxSlotT gSfx; // SID voice 1 (programs + sweep)
|
|
static StSfxSlotT gTone; // SID voice 2
|
|
static bool gNoiseOn;
|
|
|
|
// Speech playback state. The fill callback walks a queue of utterances so
|
|
// a phrase like "HT" ("Hey" + "Taxi") runs as one continuous stream
|
|
// instead of each character cutting off the last.
|
|
static uint8_t gSpeechQueue[ST_SPEECH_QUEUE];
|
|
static uint8_t gSpeechHead;
|
|
static uint8_t gSpeechTail;
|
|
static bool gSpeechActive;
|
|
static uint8_t gSpeechPitch = ST_SPEECH_PITCH_BASE;
|
|
// DATA/speech.bin, whole: header, entry table, sample bytes. The bytes
|
|
// are already in the port's raw stream format (jlAudioSfxRawFormat):
|
|
// signed +/-100, or 0x80-biased on the IIgs so a chunk needs no
|
|
// conversion; speechLoad refuses a file baked for the other one.
|
|
static uint8_t *gSpeechBlob;
|
|
static uint8_t gSpeechPitchBase;
|
|
static uint8_t gSpeechPitchCount;
|
|
static uint8_t gSpeechCount; // utterances per pitch
|
|
static const uint8_t *gSpeechPcm; // current utterance's samples
|
|
static uint16_t gSpeechSample; // next sample index in it
|
|
static uint16_t gSpeechSamples; // its length
|
|
static uint8_t gSpeechParity; // it ends on the opposite level
|
|
static uint8_t gSpeechPol; // 0 / 1: which baked polarity the next utterance plays
|
|
// Copied-ahead PCM ring for the stream (see speechFill). Allocated by
|
|
// stAudioInit rather than static: 8 KB of BSS is a fifth of the IIgs
|
|
// entry bank, which text, rodata, BSS and the C heap all share.
|
|
static uint8_t *gSpeechStage;
|
|
static uint16_t gSpeechStageRead; // byte index of the next unread sample
|
|
static uint16_t gSpeechStageWrite; // byte index of the next free byte
|
|
static uint16_t gSpeechStageCount; // bytes staged and unread
|
|
static bool gSpeechDecoding; // the decoder still has stream left
|
|
|
|
|
|
static uint16_t sidHz(uint16_t word);
|
|
static void speechLoad(void);
|
|
static bool speechNext(void);
|
|
static void speechArm(void);
|
|
static uint16_t speechDecode(uint8_t *dst, uint16_t count);
|
|
static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count);
|
|
static void speechPumpSlice(uint16_t samples);
|
|
static bool speechQueue(uint8_t ch);
|
|
static void voiceOff(StSfxSlotT *slot);
|
|
|
|
|
|
static uint16_t sidHz(uint16_t word) {
|
|
uint32_t hz = ((uint32_t)word * ST_SID_HZ_NUM) / ST_SID_HZ_DEN;
|
|
|
|
if (hz > 20000u) {
|
|
hz = 20000u;
|
|
}
|
|
return (uint16_t)hz;
|
|
}
|
|
|
|
|
|
// Point the decoder at the next queued utterance. False when the queue
|
|
// is empty, which ends the stream.
|
|
// DATA/speech.bin into one jlAlloc block; without it the game is mute
|
|
// but otherwise fine.
|
|
static void speechLoad(void) {
|
|
FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb");
|
|
uint8_t head[ST_SPEECH_HEADER_BYTES];
|
|
uint8_t format;
|
|
uint32_t dataBytes;
|
|
uint32_t total;
|
|
uint32_t done;
|
|
|
|
gSpeechBlob = 0;
|
|
if (fp == 0) {
|
|
return;
|
|
}
|
|
format = (jlAudioSfxRawFormat() == JL_SFX_RAW_UNSIGNED8_NOZERO) ? ST_SPEECH_FORMAT_BIASED : ST_SPEECH_FORMAT_SIGNED;
|
|
if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format) {
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
gSpeechPitchBase = head[5];
|
|
gSpeechPitchCount = head[6];
|
|
gSpeechCount = head[7];
|
|
dataBytes = (uint32_t)head[12] | ((uint32_t)head[13] << 8) | ((uint32_t)head[14] << 16) | ((uint32_t)head[15] << 24);
|
|
total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + dataBytes;
|
|
gSpeechBlob = (uint8_t *)jlAlloc(total);
|
|
if (gSpeechBlob == 0) {
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
memcpy(gSpeechBlob, head, sizeof(head));
|
|
// The block spans banks on the IIgs; read it in bank-safe pieces.
|
|
done = ST_SPEECH_HEADER_BYTES;
|
|
while (done < total) {
|
|
uint32_t want = total - done;
|
|
|
|
if (want > ST_SPEECH_LOAD_CHUNK) {
|
|
want = ST_SPEECH_LOAD_CHUNK;
|
|
}
|
|
if (fread(gSpeechBlob + done, 1u, (size_t)want, fp) != (size_t)want) {
|
|
jlFree(gSpeechBlob);
|
|
gSpeechBlob = 0;
|
|
break;
|
|
}
|
|
done += want;
|
|
}
|
|
fclose(fp);
|
|
}
|
|
|
|
|
|
static bool speechNext(void) {
|
|
uint8_t row = 0u;
|
|
|
|
if (gSpeechBlob == 0) {
|
|
return false;
|
|
}
|
|
if (gSpeechPitch > gSpeechPitchBase) {
|
|
row = (uint8_t)(gSpeechPitch - gSpeechPitchBase);
|
|
if (row >= gSpeechPitchCount) {
|
|
row = (uint8_t)(gSpeechPitchCount - 1u);
|
|
}
|
|
}
|
|
while (gSpeechHead != gSpeechTail) {
|
|
uint8_t code = gSpeechQueue[gSpeechHead];
|
|
const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES;
|
|
uint8_t k;
|
|
|
|
gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE);
|
|
for (k = 0u; k < gSpeechCount; k++, e += ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES) {
|
|
if (e[0] == code) {
|
|
const uint8_t *v = e + (uint16_t)gSpeechPol * ST_SPEECH_ENTRY_BYTES;
|
|
uint32_t offset = (uint32_t)v[4] | ((uint32_t)v[5] << 8) | ((uint32_t)v[6] << 16) | ((uint32_t)v[7] << 24);
|
|
|
|
gSpeechParity = v[1];
|
|
gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8));
|
|
gSpeechSample = 0u;
|
|
gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + offset;
|
|
return true;
|
|
}
|
|
}
|
|
// Unknown code: skip it rather than dropping the whole phrase.
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Copy up to `count` samples of the queued utterances into `dst`, fewer
|
|
// when they run out (0 = none left). jlMemCopy is an MVN on the IIgs;
|
|
// the utterance bytes are already in the stream's format, so this is
|
|
// the whole of the play-time speech work.
|
|
static uint16_t speechDecode(uint8_t *dst, uint16_t count) {
|
|
uint16_t n = 0u;
|
|
|
|
while (n < count) {
|
|
uint16_t left = (uint16_t)(gSpeechSamples - gSpeechSample);
|
|
uint16_t take = (uint16_t)(count - n);
|
|
|
|
if (left == 0u) {
|
|
// The next utterance carries on from the level this one left.
|
|
if (gSpeechParity != 0u) {
|
|
gSpeechPol ^= 1u;
|
|
}
|
|
if (!speechNext()) {
|
|
return n;
|
|
}
|
|
continue;
|
|
}
|
|
if (take > left) {
|
|
take = left;
|
|
}
|
|
jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take);
|
|
gSpeechSample = (uint16_t)(gSpeechSample + take);
|
|
n = (uint16_t)(n + take);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
|
|
// The audio engine's chunk pull. It used to decode the whole chunk right
|
|
// here -- 7168 samples, ~1.5 s of speech, in ONE frame: every "HEY
|
|
// TAXI" froze the ride for over a second on the IIgs. Now the chunk is
|
|
// copied out of the staging buffer that speechPumpSlice fills a slice at
|
|
// a time across the ticks before it is due, and only a stream that has
|
|
// outrun the pump decodes here, a bounded slice, so it never starves.
|
|
static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) {
|
|
uint16_t want = (count > ST_SPEECH_STAGE_BYTES) ? (uint16_t)ST_SPEECH_STAGE_BYTES : (uint16_t)count;
|
|
uint16_t n;
|
|
|
|
(void)ctx;
|
|
if (gSpeechStageCount == 0u && gSpeechDecoding) {
|
|
speechPumpSlice(ST_SPEECH_STARVED_SLICE);
|
|
}
|
|
n = (gSpeechStageCount < want) ? gSpeechStageCount : want;
|
|
if (n != 0u) {
|
|
// jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there),
|
|
// in two runs when the unread bytes wrap the ring's end.
|
|
uint8_t *to = (uint8_t *)dst;
|
|
uint16_t left = n;
|
|
|
|
while (left != 0u) {
|
|
uint16_t run = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageRead);
|
|
|
|
if (run > left) {
|
|
run = left;
|
|
}
|
|
jlMemCopy(to, gSpeechStage + gSpeechStageRead, run);
|
|
to += run;
|
|
gSpeechStageRead = (uint16_t)((gSpeechStageRead + run) & ST_SPEECH_STAGE_MASK);
|
|
left = (uint16_t)(left - run);
|
|
}
|
|
gSpeechStageCount = (uint16_t)(gSpeechStageCount - n);
|
|
}
|
|
if (n == 0u) {
|
|
// Out of queued utterances. Only END the stream if this chunk
|
|
// produced nothing at all -- a phrase is fed one character at a
|
|
// time ("HT" is "Hey" then "Taxi"), and the first arm decodes
|
|
// all of 'H' before 'T' has even been queued. Ending here would
|
|
// let the 'T' arm a NEW stream that cut 'H' off, so the player
|
|
// only ever heard the last character. Returning a short chunk
|
|
// keeps the slot armed and the next pull picks up whatever
|
|
// arrived since.
|
|
gSpeechActive = false;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
|
|
// Decode up to `samples` more into the staging buffer. Once the decoder
|
|
// reports nothing left it stays quiet until the next speechArm. Reads
|
|
// and writes stay even so the chunk copy can move words.
|
|
static void speechPumpSlice(uint16_t samples) {
|
|
uint16_t room = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageCount);
|
|
|
|
if (!gSpeechDecoding || room == 0u) {
|
|
return;
|
|
}
|
|
if (samples > room) {
|
|
samples = room;
|
|
}
|
|
// Fill towards the ring's end, then from its start.
|
|
while (samples != 0u) {
|
|
uint16_t run = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageWrite);
|
|
uint16_t got;
|
|
|
|
if (run > samples) {
|
|
run = samples;
|
|
}
|
|
got = speechDecode(gSpeechStage + gSpeechStageWrite, run);
|
|
if (got < run) {
|
|
gSpeechDecoding = false;
|
|
}
|
|
gSpeechStageWrite = (uint16_t)((gSpeechStageWrite + got) & ST_SPEECH_STAGE_MASK);
|
|
gSpeechStageCount = (uint16_t)(gSpeechStageCount + got);
|
|
samples = (uint16_t)(samples - got);
|
|
if (!gSpeechDecoding) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void voiceOff(StSfxSlotT *slot) {
|
|
if (slot->ticksLeft != 0u) {
|
|
jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF);
|
|
slot->ticksLeft = 0u;
|
|
}
|
|
}
|
|
|
|
|
|
// Once per game tick: run the release timers ($4320 sfxEnvelopeTick).
|
|
void stAudioTick(void) {
|
|
if (gSpeechActive) {
|
|
speechPumpSlice(ST_SPEECH_TICK_SLICE);
|
|
}
|
|
if (gSfx.ticksLeft != 0u) {
|
|
gSfx.ticksLeft--;
|
|
if (gSfx.ticksLeft == 0u) {
|
|
jlAudioVoice(gSfx.voice, 0u, ST_ATTEN_OFF);
|
|
}
|
|
}
|
|
if (gTone.ticksLeft != 0u) {
|
|
gTone.ticksLeft--;
|
|
if (gTone.ticksLeft == 0u) {
|
|
jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void stAudioInit(void) {
|
|
(void)jlAudioInit();
|
|
gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES);
|
|
speechLoad();
|
|
gSfx.voice = ST_VOICE_SFX;
|
|
gSfx.ticksLeft = 0u;
|
|
gTone.voice = ST_VOICE_TONE;
|
|
gTone.ticksLeft = 0u;
|
|
gNoiseOn = false;
|
|
gSpeechHead = 0u;
|
|
gSpeechTail = 0u;
|
|
gSpeechActive = false;
|
|
gSpeechPitch = ST_SPEECH_PITCH_BASE;
|
|
gSpeechPol = 0u;
|
|
}
|
|
|
|
|
|
void stAudioShutdown(void) {
|
|
voiceOff(&gSfx);
|
|
voiceOff(&gTone);
|
|
jlAudioNoise(0u, ST_ATTEN_OFF);
|
|
jlAudioShutdown();
|
|
jlFree(gSpeechStage);
|
|
gSpeechStage = 0;
|
|
jlFree(gSpeechBlob);
|
|
gSpeechBlob = 0;
|
|
}
|
|
|
|
|
|
// $6D6A / $6A3B -- voice 3 jet noise gate.
|
|
void stAudioNoise(bool on) {
|
|
if (on == gNoiseOn) {
|
|
return;
|
|
}
|
|
gNoiseOn = on;
|
|
if (on) {
|
|
jlAudioNoise(31u, ST_ATTEN_SOFT);
|
|
} else {
|
|
jlAudioNoise(0u, ST_ATTEN_OFF);
|
|
}
|
|
}
|
|
|
|
|
|
// $42E9 -- a 9-byte SID program: freq lo/hi, pulse lo/hi, control,
|
|
// AD, SR, release ticks, voice.
|
|
void stAudioSfx(const uint8_t *program9) {
|
|
uint16_t word = (uint16_t)(program9[0] | ((uint16_t)program9[1] << 8));
|
|
uint8_t ctrl = program9[4];
|
|
uint8_t ticks = program9[7];
|
|
StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx;
|
|
|
|
if ((ctrl & 0x80u) != 0u) {
|
|
// Noise waveform: a burst on the noise generator.
|
|
jlAudioNoise(31u, ST_ATTEN_LOUD);
|
|
gNoiseOn = true;
|
|
return;
|
|
}
|
|
jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD);
|
|
slot->ticksLeft = (uint16_t)(ticks + 1u);
|
|
}
|
|
|
|
|
|
// $6C7F / $6CAA -- voices 1 and 2 gated off.
|
|
void stAudioSilence(void) {
|
|
voiceOff(&gSfx);
|
|
voiceOff(&gTone);
|
|
}
|
|
|
|
|
|
// $44CF -- play one of the game's tunes and block. The C64 silences the
|
|
// SID first ($5C1A / $6906) and then spins until the song ends; the host
|
|
// does the spinning (ST_STATE_JINGLE), so here it is just the start.
|
|
void stAudioJingle(uint8_t song) {
|
|
stAudioMusic(song, false);
|
|
}
|
|
|
|
|
|
// $CB02 -- start a tune and leave it to the player: the level intro's
|
|
// song and the high-score table's. Speech is cancelled too: it streams
|
|
// on the slot the tracker's voice 0 rides.
|
|
void stAudioMusic(uint8_t song, bool loop) {
|
|
if (song >= ST_SONG_COUNT) {
|
|
return;
|
|
}
|
|
stAudioSilence();
|
|
stAudioNoise(false);
|
|
gSpeechActive = false;
|
|
gSpeechDecoding = false;
|
|
gSpeechStageRead = 0u;
|
|
gSpeechStageWrite = 0u;
|
|
gSpeechStageCount = 0u;
|
|
gSpeechHead = gSpeechTail;
|
|
jlAudioStopSfx(ST_SPEECH_SLOT);
|
|
jlMusicPlay(kStSongs[song].data, kStSongs[song].length, loop);
|
|
}
|
|
|
|
|
|
bool stAudioJingleActive(void) {
|
|
return jlMusicIsPlaying();
|
|
}
|
|
|
|
|
|
// Queue one utterance code. Returns false when the code is not speech (a
|
|
// pitch change) or the queue is full.
|
|
static bool speechQueue(uint8_t ch) {
|
|
uint8_t next;
|
|
|
|
if (ch < 9u) {
|
|
// Below $09 the original does not speak: it rewrites the delay
|
|
// loop's inner count, re-pitching every later utterance. That is
|
|
// where the passenger's voice variation comes from, so keep it.
|
|
gSpeechPitch = (uint8_t)(ch & ST_SPEECH_PITCH_MAX);
|
|
if (gSpeechPitch == 0u) {
|
|
gSpeechPitch = ST_SPEECH_PITCH_BASE;
|
|
}
|
|
return false;
|
|
}
|
|
next = (uint8_t)((gSpeechTail + 1u) % ST_SPEECH_QUEUE);
|
|
if (next == gSpeechHead) {
|
|
return false; // queue full; drop rather than stall
|
|
}
|
|
gSpeechQueue[gSpeechTail] = ch;
|
|
gSpeechTail = next;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Start the stream if it is idle. Call this ONCE, after the whole phrase is
|
|
// queued: arming decodes everything queued so far into a single chunk, and
|
|
// each extra chunk costs a refill deadline of dead air, because the refill
|
|
// only happens when the game next calls jlAudioFrameTick -- once per rendered
|
|
// frame. Arming per character is what broke phrases into words with silence
|
|
// between them.
|
|
static void speechArm(void) {
|
|
if (gSpeechActive || gSpeechStage == 0 || gSpeechBlob == 0) {
|
|
return;
|
|
}
|
|
if (!speechNext()) {
|
|
return;
|
|
}
|
|
gSpeechPol = 0u; // a phrase starts on +100
|
|
gSpeechActive = true;
|
|
gSpeechDecoding = true;
|
|
gSpeechStageRead = 0u;
|
|
gSpeechStageWrite = 0u;
|
|
gSpeechStageCount = 0u;
|
|
// A first slice now, so the opening chunk sounds this tick.
|
|
speechPumpSlice(ST_SPEECH_ARM_SLICE);
|
|
jlAudioPlaySfxStreamRaw(ST_SPEECH_SLOT, speechFill, (void *)0, ST_SPEECH_RATE);
|
|
}
|
|
|
|
|
|
// $9802 -- one spoken character. Speaks immediately; prefer
|
|
// stAudioSpeechPhrase for anything longer than a single code.
|
|
void stAudioSpeech(uint8_t ch) {
|
|
if (speechQueue(ch)) {
|
|
speechArm();
|
|
}
|
|
}
|
|
|
|
|
|
// A whole phrase: queue every code FIRST, then arm once, so the phrase
|
|
// decodes into as few chunks as it fits into.
|
|
void stAudioSpeechPhrase(const uint8_t *speech) {
|
|
bool any = false;
|
|
|
|
while (*speech != 0u) {
|
|
if (speechQueue(*speech)) {
|
|
any = true;
|
|
}
|
|
speech++;
|
|
}
|
|
if (any) {
|
|
speechArm();
|
|
}
|
|
}
|
|
|
|
|
|
// $6A8A -- the crash scream: voice 1 frequency written from the sweep.
|
|
void stAudioThrustSweep(uint8_t value) {
|
|
jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD);
|
|
gSfx.ticksLeft = 3u;
|
|
}
|
|
|
|
|
|
// $6E71 -- the fuel pump's rising note on voice 1.
|
|
void stAudioVoice1Freq(uint8_t value) {
|
|
jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD);
|
|
gSfx.ticksLeft = 2u;
|
|
}
|
|
|
|
|
|
// Level hooks poke voice 2 directly (laser hum, puzzle chime).
|
|
void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) {
|
|
uint16_t word = (uint16_t)(freqLo | ((uint16_t)freqHi << 8));
|
|
|
|
if ((ctrl & 0x80u) != 0u) {
|
|
jlAudioNoise((uint8_t)(freqHi & 31u), ST_ATTEN_SOFT);
|
|
gNoiseOn = true;
|
|
return;
|
|
}
|
|
jlAudioVoice(ST_VOICE_TONE, sidHz(word), ST_ATTEN_SOFT);
|
|
gTone.ticksLeft = 2u;
|
|
}
|