joeylib2/examples/spacetaxi/stAudio.c

235 lines
8.1 KiB
C

// 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;
}