235 lines
8.1 KiB
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;
|
|
}
|