// 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. #include "spacetaxi.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 chirp: one short tone per character, pitched by the character. #define ST_SPEECH_TICKS 3u #define ST_SPEECH_BASE_HZ 300u #define ST_SPEECH_STEP_HZ 9u // 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; static uint16_t gSpeechTicks; static uint16_t sidHz(uint16_t word); 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; } 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 (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); } } if (gSpeechTicks != 0u) { gSpeechTicks--; if (gSpeechTicks == 0u) { jlAudioVoice(ST_VOICE_TONE, 0u, ST_ATTEN_OFF); } } } void stAudioInit(void) { (void)jlAudioInit(); gSfx.voice = ST_VOICE_SFX; gSfx.ticksLeft = 0u; gTone.voice = ST_VOICE_TONE; gTone.ticksLeft = 0u; gNoiseOn = false; gSpeechTicks = 0u; } void stAudioShutdown(void) { voiceOff(&gSfx); voiceOff(&gTone); jlAudioNoise(0u, ST_ATTEN_OFF); jlAudioShutdown(); } // $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); } // $9802 -- one spoken character (a chirp here). void stAudioSpeech(uint8_t ch) { jlAudioVoice(ST_VOICE_TONE, (uint16_t)(ST_SPEECH_BASE_HZ + (uint16_t)(ch & 0x1Fu) * ST_SPEECH_STEP_HZ), ST_ATTEN_LOUD); gSpeechTicks = ST_SPEECH_TICKS; } // $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; }