// Space Taxi -- audio dispatch. // // Simple 1-voice SFX engine on top of jlAudioVoice. Each SFX call // programs voice slot 2 (conventionally SFX) with a tone + attenuation // and arms a frame-tick countdown. When the countdown reaches 0 the // voice is silenced. New SFX preempt any in-progress one. // // Continuous thrust SFX is edge-triggered: on -> arm a looping tone, // off -> silence. The thrust voice (slot 1) is independent of the // transient SFX voice so a pickup chirp on top of thrust doesn't // cut the thrust note. // // MUSIC MODEL (important): Space Taxi gameplay has NO background // music. The C64 only loads songs via $CB02 at non-gameplay sites: // - title-screen setup ($459C / $46BC) -> song 8 // - title-init via $4741 ($477C) -> song 25 // - score-screen draw ($4C29) -> song 7 // - score-screen variant ($4EB6) -> song 6 // During gameplay the IRQ music engine ticks ($CC6C) but all 3 // voices are silent because no track pointers are loaded. // stAudioPlayMusic / stAudioStopMusic stay as stubs for the eventual // title-screen and score-screen jingle dispatch; do not call them // from gameplay state transitions. #include "spacetaxi.h" JOEYLIB_SEGMENT("STAXI") #define ST_SFX_VOICE 2u #define ST_THRUST_VOICE 1u #define ST_SFX_PICKUP_HZ 720u #define ST_SFX_PICKUP_TICKS 20u #define ST_SFX_DROPOFF_HZ 1100u #define ST_SFX_DROPOFF_TICKS 25u #define ST_SFX_LAND_HZ 90u #define ST_SFX_LAND_TICKS 12u // C64-matched SFX behavior (see MECHANICS.md "Sound (SID)"): // // Thrust: voice 1 freq sweep. $6A63 writes $A0 = 160 to $721B at // thrust-engage; the phase handler decrements $721B once per tick, // LSRs it, and writes the result to both bytes of $D400/$D401 -- // giving a SID freq word that drops over the burst. PAL C64 maps // freq_word=$5050 -> ~1207 Hz at the start; the sweep ends near // silence. Reproduce with per-frame freq updates on ST_THRUST_VOICE. #define ST_THRUST_SWEEP_INIT 160u // Atten scale on the JoeyLib audio HAL is SN76489-style: 0 = loud, // 15 = silent. Anything >= 15 keys-off the voice (treated as silent // by halAudioVoice on DOS). Keep all SFX in 0..14 to actually be // audible. 6 = moderately loud; 10 = quieter. #define ST_THRUST_ATTEN 8u // Audible-Hz scale factor for the sweep counter -- chosen so initial // (counter=160) lands ~1200 Hz to match the C64 PAL freq mapping. #define ST_THRUST_HZ_PER_TICK 8u // Crash audio is TWO simultaneous events on the C64: // 1. impact "bang": voice 3 noise burst ($D412 = $81 then $80 a // short time later). Approximated here with rapid pseudo-random // freq jumps on ST_SFX_VOICE. // 2. descending "scream": $721B initialised to $A0 at $6A61, then // the phase-1 handler ($6A72) LSRs it each tick into $D400/$D401 // -- voice 1 freq drops from ~1.2 kHz toward silence over the // death anim. Reproduced here on ST_THRUST_VOICE (same voice the // C64 uses; the cab can't be thrusting during a crash anyway). #define ST_SFX_CRASH_TICKS 30u #define ST_CRASH_SCREAM_INIT 120u // matches stEngine ST_CRASH_ANIM_FRAMES #define ST_SFX_DEFAULT_ATTEN 6u static uint16_t gSfxTicksLeft; static bool gThrustOn; static uint16_t gThrustSweep; // counts down to zero; per-frame freq static uint16_t gCrashTicks; // noise burst countdown (impact "bang") static uint16_t gCrashScreamSweep; // voice-1 descending sweep during crash anim static uint16_t gNoiseRng; // xorshift state for crash noise static void armSfx(uint16_t freq, uint8_t atten, uint16_t ticks); static void silenceSfx(void); void stAudioInit(void) { (void)jlAudioInit(); gThrustOn = false; gSfxTicksLeft = 0u; gThrustSweep = 0u; gCrashTicks = 0u; gCrashScreamSweep = 0u; gNoiseRng = 0xACE1u; // arbitrary nonzero xorshift seed jlAudioVoice(ST_SFX_VOICE, 0u, 0u); jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); } void stAudioShutdown(void) { silenceSfx(); if (gThrustOn) { jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); gThrustOn = false; } jlAudioStopMod(); jlAudioShutdown(); } // Called every host frame from spacetaxi.c's main loop (alongside // jlAudioFrameTick which advances the mixer). Drives: // - thrust sweep: per-frame freq decrement on ST_THRUST_VOICE // mirroring the C64's $721B sweep // - crash noise: per-frame xorshift -> freq jump on ST_SFX_VOICE // approximating voice-3 noise // - transient SFX countdown (pickup/dropoff/land tones) void stAudioFrameTick(void) { // Thrust voice has two modes: normal thrust (gThrustOn) sweeps // freq down while held, OR crash scream sweep takes over when // active (mutually exclusive -- the cab can't crash and thrust // simultaneously). Crash scream wins if both are armed. if (gCrashScreamSweep > 0u) { gCrashScreamSweep--; if (gCrashScreamSweep == 0u) { jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); } else { jlAudioVoice(ST_THRUST_VOICE, (uint16_t)(gCrashScreamSweep * ST_THRUST_HZ_PER_TICK), ST_THRUST_ATTEN); } } else if (gThrustOn && gThrustSweep > 0u) { gThrustSweep--; if (gThrustSweep == 0u) { jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); } else { jlAudioVoice(ST_THRUST_VOICE, (uint16_t)(gThrustSweep * ST_THRUST_HZ_PER_TICK), ST_THRUST_ATTEN); } } // Noise-burst "bang" on the SFX voice, separate from the scream. if (gCrashTicks > 0u) { uint16_t pitchHz; // xorshift LFSR -- 16-bit, period 65535. gNoiseRng ^= (uint16_t)(gNoiseRng << 7); gNoiseRng ^= (uint16_t)(gNoiseRng >> 9); gNoiseRng ^= (uint16_t)(gNoiseRng << 8); // Map random word to a noisy-feeling pitch range 100..700 Hz. pitchHz = (uint16_t)(100u + (gNoiseRng % 600u)); jlAudioVoice(ST_SFX_VOICE, pitchHz, ST_SFX_DEFAULT_ATTEN); gCrashTicks--; if (gCrashTicks == 0u) { jlAudioVoice(ST_SFX_VOICE, 0u, 0u); } } else if (gSfxTicksLeft > 0u) { gSfxTicksLeft--; if (gSfxTicksLeft == 0u) { silenceSfx(); } } } void stAudioPlayMusic(uint8_t musicId) { // TODO: per-target music dispatch. Silent for now so the // engine bring-up isn't blocked on writing per-platform music // assets. (void)musicId; } void stAudioStopMusic(void) { jlAudioStopMod(); } // Engine thrust: starts a new freq-sweep burst on each rising edge. // Mirrors the C64's $6A63 reset of $721B to $A0 -> phase handler // decrements and writes to $D400/$D401 each tick. void stAudioSfxThrust(bool on) { if (on == gThrustOn) { return; } gThrustOn = on; if (on) { gThrustSweep = ST_THRUST_SWEEP_INIT; jlAudioVoice(ST_THRUST_VOICE, (uint16_t)(gThrustSweep * ST_THRUST_HZ_PER_TICK), ST_THRUST_ATTEN); } else { gThrustSweep = 0u; jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); } } void stAudioSfxLand(void) { armSfx(ST_SFX_LAND_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_LAND_TICKS); } void stAudioSfxPickup(void) { armSfx(ST_SFX_PICKUP_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_PICKUP_TICKS); } void stAudioSfxDropoff(void) { armSfx(ST_SFX_DROPOFF_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_DROPOFF_TICKS); } // Crash: kicks off TWO simultaneous events on the C64 (see header // comment near ST_SFX_CRASH_TICKS): // 1. impact "bang" -- noise burst on ST_SFX_VOICE // 2. descending "scream" -- voice-1 freq sweep on ST_THRUST_VOICE // Force gThrustOn false so the scream takes over the thrust voice // cleanly even if the player was thrust-holding into the wall. void stAudioSfxCrash(void) { gThrustOn = false; gCrashTicks = ST_SFX_CRASH_TICKS; gCrashScreamSweep = ST_CRASH_SCREAM_INIT; gSfxTicksLeft = 0u; } // ----- internal ----- static void armSfx(uint16_t freq, uint8_t atten, uint16_t ticks) { jlAudioVoice(ST_SFX_VOICE, freq, atten); gSfxTicksLeft = ticks; } static void silenceSfx(void) { jlAudioVoice(ST_SFX_VOICE, 0u, 0u); gSfxTicksLeft = 0u; }