diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..414a651 --- /dev/null +++ b/src/audio.c @@ -0,0 +1,413 @@ +// audio.c — SID audio service for the Whack Hare! state machine. +// +// The audio layer is a thin state-driven service. The game's +// per-state "enter" actions call audio_state_enter() to set up the +// SID for that state's cue, and the per-frame game_step() calls +// audio_state_step() to advance the cue's schedule. A short fault +// stinger on voice 1 is also exposed via audio_play_fault_stinger() +// and cleans itself up over 10 frames via the per-frame tick. +// +// **SID cue table (from GAME.md §6).** +// +// STATE_TITLE silent (Phase 6 leaves TITLE silent; the optional +// low idle pulse from the spec is not implemented). +// STATE_READY voice 0 triangle A4 (8 frames) -> A5 (8 frames), +// attack=0, decay=9, no sustain, release=0. +// STATE_WAIT voice 0 square C2 + voice 1 triangle C3 (1 octave +// up), both with attack=0, decay=15, sustain=8, +// release=15. Voice 0 retriggered every 16 frames, +// voice 1 every 16 frames offset by 8 (so voice 1 +// sounds on frames 8-15, 24-31, ...). +// STATE_DRAW voice 2 noise, attack=0, decay=1, sustain=0, +// release=2; gated on for 4 frames. +// STATE_WIN_* voice 0 triangle C5-E5-G5-C6 arpeggio, 10 frames +// per note (~0.8s total); voice 1 triangle a major +// third below (A4, C5, E5, A5) with sustain=4 to +// keep it "very quiet". +// STATE_GAMEOVER voice 0 triangle G4-C5-E5-G5-C6 fanfare, 30 +// frames per note (~3s total); voice 1 sustained C4 +// (a perfect fifth below G4) throughout. +// +// **No leakage between states.** audio_state_enter() calls +// audio_stop() first, then sets up the new state. This is critical +// for the WAIT -> DRAW transition: WAIT's looping voice 0/1 must +// stop before DRAW's voice 2 noise stab starts. +// +// **Fault stinger.** A 10-frame low square-wave burst on voice 1 +// (SID_FREQ_PAL(100), sustain=15). Triggered from game_step_draw() +// AFTER game_enter_title() (so the state transition doesn't silence +// it). Cleans itself up via audio_state_step(). + +#include "audio.h" +#include "notes.h" +#include +#include + +// --- internal state ----------------------------------------------------- + +// Current audio state. Set by audio_state_enter() and matched +// against the state argument of audio_state_step(). 0xff = "none". +#define AUDIO_STATE_NONE 0xff +static byte audio_state; + +// Per-state frame counter. Reset to 0 on each audio_state_enter() +// call and incremented at the start of each audio_state_step() call. +// 16 bits is enough for the longest cue (GAMEOVER is 150 frames). +static unsigned short audio_step; + +// Fault stinger counter. > 0 means the stinger is currently playing +// on voice 1; audio_state_step() decrements it once per frame and +// gates voice 1 off when it hits 0. Also cancelled by +// audio_state_enter() so the stinger doesn't bleed into a new state +// that uses voice 1 (WAIT, WIN, GAMEOVER). +static byte fault_stinger_ticks; + +// --- public API --------------------------------------------------------- + +void audio_init(void) +{ + // Master volume 15, no filter modes, no voice filtering. Filter + // cutoff + resonance are also zeroed so no surprises from reset + // state. See SID register map at $D415-$D418. + sid.ffreq = 0; + sid.resfilt = 0; + sid.fmodevol = 0x0f; + + audio_state = AUDIO_STATE_NONE; + audio_step = 0; + fault_stinger_ticks = 0; + + // Silence all 3 voices to clear any leftover SID state. + audio_stop(); +} + +void audio_stop(void) +{ + // Gate off all 3 voices and clear their registers. Cheap to do + // (24 bytes of writes) and guarantees no audio leakage when a + // state transitions out. + sid.voices[0].ctrl = 0; + sid.voices[1].ctrl = 0; + sid.voices[2].ctrl = 0; + sid.voices[0].attdec = 0; + sid.voices[1].attdec = 0; + sid.voices[2].attdec = 0; + sid.voices[0].susrel = 0; + sid.voices[1].susrel = 0; + sid.voices[2].susrel = 0; + sid.voices[0].freq = 0; + sid.voices[1].freq = 0; + sid.voices[2].freq = 0; + sid.voices[0].pwm = 0; + sid.voices[1].pwm = 0; + sid.voices[2].pwm = 0; +} + +// --- internal helpers --------------------------------------------------- + +// Configure a voice's freq/ADSR/PWM/waveform but do NOT gate on. The +// caller is responsible for setting SID_CTRL_GATE when the note +// should sound. This split lets us write a voice's parameters +// without producing an audible click (the gate stays off until +// everything else is set up). +static void audio_setup_voice(int v, unsigned freq, byte waveform, + byte attdec, byte susrel, unsigned pwm) +{ + sid.voices[v].freq = freq; + sid.voices[v].pwm = pwm; + sid.voices[v].attdec = attdec; + sid.voices[v].susrel = susrel; + sid.voices[v].ctrl = waveform; +} + +// Gate a voice on (re-trigger the ADSR envelope). The voice's +// waveform must already be selected (audio_setup_voice does this). +static void audio_gate_on(int v, byte waveform) +{ + sid.voices[v].ctrl = waveform; + sid.voices[v].ctrl = waveform | SID_CTRL_GATE; +} + +// Gate a voice off (start the release phase of the envelope). +static void audio_gate_off(int v, byte waveform) +{ + sid.voices[v].ctrl = waveform; +} + +// Per-frame fault stinger tick. Decrements fault_stinger_ticks; when +// it hits 0, gate voice 1 off. Cheap to call when no stinger is +// active (single byte compare, branch not taken). +static void audio_advance_fault_stinger(void) +{ + if (fault_stinger_ticks > 0) { + fault_stinger_ticks--; + if (fault_stinger_ticks == 0) + sid.voices[1].ctrl = SID_CTRL_RECT; + } +} + +void audio_play_fault_stinger(void) +{ + // Low square wave on voice 1. ~100 Hz is in the "low buzz" + // range, not a musical note. Attack=0, decay=0, sustain=15 + // (max), release=0 means: the note is at full volume the + // instant the gate goes on and stays at full volume until gated + // off. The 10-frame duration is set here; audio_state_step() + // will gate it off. + sid.voices[1].freq = SID_FREQ_PAL(100); + sid.voices[1].pwm = 0x0800; + sid.voices[1].attdec = SID_ATK_2; + sid.voices[1].susrel = 0xf0; + sid.voices[1].ctrl = SID_CTRL_RECT | SID_CTRL_GATE; + fault_stinger_ticks = 10; +} + +void audio_state_enter(int state) +{ + // First silence the SID — this is what prevents the WAIT loop + // from leaking into DRAW, and what cancels a mid-play fault + // stinger when transitioning to a state that uses voice 1. + audio_stop(); + fault_stinger_ticks = 0; + audio_step = 0; + audio_state = (byte)state; + + switch (state) { + case STATE_TITLE: + // Silent per the Phase 6 spec. The fault stinger + // (triggered separately by game.c after this call) + // is the only audio that may play in TITLE. + break; + + case STATE_READY: + // Ping-ping: triangle A4 then A5, 8 frames each. + // ADSR: attack=0 (2ms), decay=9 (~114ms), no + // sustain, release=0 (6ms). With sustain=0 the + // envelope drops to 0 once decay finishes, but + // audio_state_step() retriggers the gate every 8 + // frames so the second note plays. + audio_setup_voice(0, NOTE_A4, SID_CTRL_TRI, + SID_ATK_2 | SID_DKY_114, + 0x00, + 0x0800); + audio_gate_on(0, SID_CTRL_TRI); + break; + + case STATE_WAIT: + // Square C2 on voice 0, triangle C3 on voice 1. + // ADSR: attack=0, decay=15 (24s), sustain=8 + // (half-volume hold), release=15. Decay 15 is + // intentionally very slow — between retriggers + // (every 16 frames) the envelope barely drops, so + // the cue sounds more like a sustained pair of + // voices with a periodic retrigger than a true + // "pulse-pulse" pattern. The retrigger cadence + // is what gives the suspense its rhythm. + audio_setup_voice(0, NOTE_C2, SID_CTRL_RECT, + SID_ATK_2 | SID_DKY_24000, + (8 << 4) | 0x0f, + 0x0800); + audio_gate_on(0, SID_CTRL_RECT); + + audio_setup_voice(1, NOTE_C3, SID_CTRL_TRI, + SID_ATK_2 | SID_DKY_24000, + (8 << 4) | 0x0f, + 0x0800); + // Voice 1 not gated yet — audio_state_step() + // gates it on at audio_step == 8 (so it's offset + // by half a cycle from voice 0). + break; + + case STATE_DRAW: + // Noise on voice 2. The noise waveform's actual + // pitch is taken from voice 3's oscillator + // frequency (a SID hardware quirk); we set the + // freq to max for a "hissy" stab. ADSR: + // attack=0, decay=1 (6ms), sustain=0, release=2. + // Sustain=0 means the envelope drops to 0 almost + // immediately, but the gate is held on for 4 + // frames so the noise is at full volume the whole + // time — the "sharp stab" is the 4-frame gate, + // not the ADSR. + audio_setup_voice(2, 0xffff, SID_CTRL_NOISE, + SID_ATK_2 | SID_DKY_6, + 0x02, + 0x0800); + audio_gate_on(2, SID_CTRL_NOISE); + break; + + case STATE_WIN_P1: + case STATE_WIN_P2: + // Voice 0: triangle C5-E5-G5-C6 arpeggio. + // Voice 1: triangle a major third below each + // note (A4, C5, E5, A5) with sustain=4 to make + // it "very quiet" per the spec. + // ADSR: attack=0, decay=9 (~114ms), sustain=15 + // (voice 0) or 4 (voice 1), release=15. Note + // durations are 10 frames per note — total 40 + // frames = 0.8s at 50 Hz. + audio_setup_voice(0, NOTE_C5, SID_CTRL_TRI, + SID_ATK_2 | SID_DKY_114, + (15 << 4) | 0x0f, + 0x0800); + audio_gate_on(0, SID_CTRL_TRI); + + audio_setup_voice(1, NOTE_A4, SID_CTRL_TRI, + SID_ATK_2 | SID_DKY_114, + (4 << 4) | 0x0f, + 0x0800); + audio_gate_on(1, SID_CTRL_TRI); + break; + + case STATE_GAMEOVER: + // Voice 0: triangle G4-C5-E5-G5-C6 fanfare. + // Voice 1: sustained C4 (a perfect fifth below + // G4) throughout. Note durations are 30 frames + // — total 150 frames = 3.0s at 50 Hz. + // ADSR: attack=0, decay=9, sustain=15, release=15 + // (a long held chord that rings out). + audio_setup_voice(0, NOTE_G4, SID_CTRL_TRI, + SID_ATK_2 | SID_DKY_114, + (15 << 4) | 0x0f, + 0x0800); + audio_gate_on(0, SID_CTRL_TRI); + + audio_setup_voice(1, NOTE_C4, SID_CTRL_TRI, + SID_ATK_2 | SID_DKY_114, + (15 << 4) | 0x0f, + 0x0800); + audio_gate_on(1, SID_CTRL_TRI); + break; + } +} + +void audio_state_step(int state) +{ + // Per-frame work that must happen regardless of state: tick + // down the fault-stinger counter. Doing this here (not in any + // per-state step) means the stinger cleans up even if we + // transition out of TITLE during the 0.2s window. + audio_advance_fault_stinger(); + + // If the audio layer hasn't been initialized for this state + // yet (e.g. audio_state_enter() wasn't called between + // game_step() and audio_state_step()), skip the per-state + // schedule. This shouldn't happen in normal flow but is a + // safety net. + if (state != audio_state) + return; + + audio_step++; + + switch (state) { + case STATE_TITLE: + // Silent + break; + + case STATE_READY: + // Frame 8: switch A4 -> A5 + // Frame 16: gate off (end of cue) + if (audio_step == 8) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_A5; + audio_gate_on(0, SID_CTRL_TRI); + } else if (audio_step == 16) { + audio_gate_off(0, SID_CTRL_TRI); + } + break; + + case STATE_WAIT: + // Looping retrigger. The cue is entered with + // voice 0 already gated on (frame 0 of the + // schedule) and voice 1 gated off. Every 16 + // frames we retrigger voice 0; at the half-way + // mark (mod 16 == 8) we retrigger voice 1. + // That gives 16-frame cycles of: + // frames 0-7 : voice 0 only + // frames 8-15 : voice 0 + voice 1 + // repeating until the state exits. + if ((audio_step & 15) == 0) { + audio_gate_off(0, SID_CTRL_RECT); + sid.voices[0].freq = NOTE_C2; + audio_gate_on(0, SID_CTRL_RECT); + } + if ((audio_step & 15) == 8) { + audio_gate_off(1, SID_CTRL_TRI); + sid.voices[1].freq = NOTE_C3; + audio_gate_on(1, SID_CTRL_TRI); + } + break; + + case STATE_DRAW: + // Gate off voice 2 after 4 frames. The cue + // is entered with voice 2 already gated on + // (frame 0), so the first 4 frames of the + // state have noise; from frame 4 onward it's + // silent (gate off, release phase is short). + if (audio_step >= 4) { + audio_gate_off(2, SID_CTRL_NOISE); + } + break; + + case STATE_WIN_P1: + case STATE_WIN_P2: + // Arpeggio every 10 frames. Both voices + // advance in lockstep. Voice 0 plays the + // melody (C5, E5, G5, C6); voice 1 plays + // the same notes a major third below + // (A4, C5, E5, A5). At frame 40 we gate + // both off (end of cue). + if (audio_step == 10) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_E5; + audio_gate_on(0, SID_CTRL_TRI); + audio_gate_off(1, SID_CTRL_TRI); + sid.voices[1].freq = NOTE_C5; + audio_gate_on(1, SID_CTRL_TRI); + } else if (audio_step == 20) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_G5; + audio_gate_on(0, SID_CTRL_TRI); + audio_gate_off(1, SID_CTRL_TRI); + sid.voices[1].freq = NOTE_E5; + audio_gate_on(1, SID_CTRL_TRI); + } else if (audio_step == 30) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_C6; + audio_gate_on(0, SID_CTRL_TRI); + audio_gate_off(1, SID_CTRL_TRI); + sid.voices[1].freq = NOTE_A5; + audio_gate_on(1, SID_CTRL_TRI); + } else if (audio_step >= 40) { + audio_gate_off(0, SID_CTRL_TRI); + audio_gate_off(1, SID_CTRL_TRI); + } + break; + + case STATE_GAMEOVER: + // Fanfare every 30 frames on voice 0; voice 1 + // sustains C4 throughout the entire cue and is + // only gated off at the end (frame 150+). + if (audio_step == 30) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_C5; + audio_gate_on(0, SID_CTRL_TRI); + } else if (audio_step == 60) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_E5; + audio_gate_on(0, SID_CTRL_TRI); + } else if (audio_step == 90) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_G5; + audio_gate_on(0, SID_CTRL_TRI); + } else if (audio_step == 120) { + audio_gate_off(0, SID_CTRL_TRI); + sid.voices[0].freq = NOTE_C6; + audio_gate_on(0, SID_CTRL_TRI); + } else if (audio_step >= 150) { + audio_gate_off(0, SID_CTRL_TRI); + audio_gate_off(1, SID_CTRL_TRI); + } + break; + } +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..c8ede46 --- /dev/null +++ b/src/audio.h @@ -0,0 +1,53 @@ +#ifndef WHACK_HARE_AUDIO_H +#define WHACK_HARE_AUDIO_H + +// audio.h — SID audio service for the Whack Hare! state machine. +// +// The audio layer is a thin state-driven service: the game's per-state +// "enter" actions call audio_state_enter() to set up the SID for that +// state's cue, and the raster IRQ handler's per-frame tick calls +// audio_state_step() to advance the cue's schedule. +// +// audio_init() must be called once at startup, after memmap_setup() +// and before rasterirq_setup(). audio_play_fault_stinger() is called +// from the DRAW state's fault handler (in game.c) to start a short +// low square-wave stinger on voice 1; the stinger cleans itself up +// over the next 10 frames via audio_state_step(). +// +// **No leakage between states.** audio_state_enter() calls +// audio_stop() internally before setting up the new state, so the +// previous state's SID writes (e.g. the WAIT voice 0/1 loop) cannot +// bleed into the next state (e.g. DRAW's voice 2 noise stab). +// +// **The audio_state argument.** The STATE_* constants are defined in +// game.h. We include it here so audio_state_enter() and +// audio_state_step() can switch on the same values the game uses. + +#include "game.h" + +void audio_init(void); + +// audio_stop() — silence all 3 SID voices and reset their registers. +// Called internally by audio_state_enter() and also available to the +// game for explicit silencing (currently unused at the game layer). +void audio_stop(void); + +// audio_state_enter(state) — silence the SID, then set up the cue +// for the given state. Resets the per-state schedule. See audio.c +// for the per-state cue definitions. +void audio_state_enter(int state); + +// audio_state_step(state) — advance the per-state schedule by one +// frame. Called from game_step() once per frame. Also ticks down +// the fault-stinger counter and gates voice 1 off when it expires. +void audio_state_step(int state); + +// audio_play_fault_stinger() — start a 10-frame low square-wave +// stinger on voice 1. Called from game_step_draw() when the DRAW +// fault timeout fires (after audio_state_enter(TITLE) so the stinger +// isn't immediately silenced by the state transition). +void audio_play_fault_stinger(void); + +#pragma compile("audio.c") + +#endif diff --git a/src/game.c b/src/game.c index ec195b4..efdcae1 100644 --- a/src/game.c +++ b/src/game.c @@ -22,6 +22,7 @@ // on entry. #include "game.h" +#include "audio.h" #include "screens.h" #include "input.h" #include "score.h" @@ -80,13 +81,11 @@ static char draw_was_pressed[2]; // --- fault stinger ----------------------------------------------------- // -// When the DRAW state's 500-frame fault timeout fires, we trigger a -// short low-square-wave stinger on SID voice 1. This counter is -// decremented every frame in game_step() (not in any per-state step, -// so it cleans up even if we transition out of TITLE before the -// stinger would naturally end). When it reaches 0, we gate voice 1 -// off. ~10 frames = ~0.2 sec at 50 Hz. -static byte fault_stinger_ticks; +// When the DRAW state's 500-frame fault timeout fires, game_step_draw +// transitions to TITLE and then calls audio_play_fault_stinger() to +// start a 10-frame low square-wave burst on SID voice 1. The +// cleanup logic lives in audio.c (audio_advance_fault_stinger, called +// from audio_state_step). See audio.c for details. // --- per-state enter functions ----------------------------------------- // @@ -99,6 +98,15 @@ static byte fault_stinger_ticks; // other states are black) // - samples the SID random for WAIT duration (in READY enter, not // WAIT enter, per the Phase 5 spec) +// - calls audio_state_enter(state) to set up the SID for this +// state's cue. This is the last step so the previous state's +// audio (if any) is still playing while we set up the screen, +// and the new state's audio takes over atomically. +// +// The fault stinger (a 10-frame low square-wave burst on voice 1) is +// triggered from game_step_draw() AFTER game_enter_title() returns, +// not from game_enter_title() itself — otherwise audio_state_enter() +// would silence it immediately. static void game_enter_title(void) { @@ -109,6 +117,7 @@ static void game_enter_title(void) title_first_frame = 0; // Border starts white (the "PRESS FIRE" prompt is visible). vic.color_border = 1; + audio_state_enter(STATE_TITLE); } static void game_enter_ready(void) @@ -122,6 +131,7 @@ static void game_enter_ready(void) // driven by an LFSR and effectively random between reads. wait_duration_frames = 100 + (sid.random % 150); vic.color_border = 0; + audio_state_enter(STATE_READY); } static void game_enter_wait(void) @@ -130,6 +140,7 @@ static void game_enter_wait(void) score_render(); enter_frame = frame_count; vic.color_border = 0; + audio_state_enter(STATE_WAIT); } static void game_enter_draw(void) @@ -140,6 +151,7 @@ static void game_enter_draw(void) vic.color_border = 1; // white border matches the white screen draw_was_pressed[0] = 0; draw_was_pressed[1] = 0; + audio_state_enter(STATE_DRAW); } static void game_enter_win_p1(void) @@ -149,6 +161,7 @@ static void game_enter_win_p1(void) score_render(); enter_frame = frame_count; vic.color_border = 0; + audio_state_enter(STATE_WIN_P1); } static void game_enter_win_p2(void) @@ -158,6 +171,7 @@ static void game_enter_win_p2(void) score_render(); enter_frame = frame_count; vic.color_border = 0; + audio_state_enter(STATE_WIN_P2); } static void game_enter_gameover(void) @@ -171,36 +185,7 @@ static void game_enter_gameover(void) score_render(); enter_frame = frame_count; vic.color_border = 0; -} - -// --- fault stinger helpers --------------------------------------------- - -static void game_trigger_fault_stinger(void) -{ - // Low square wave on voice 1. ~100 Hz is in the "low buzz" - // range, not a musical note — appropriate for an "aborted - // round" sting. Attack=0, decay=0, sustain=15 (max), release=0 - // means: the note is full volume the instant the gate goes on - // and stays full volume until gated off (no decay). We gate - // off after ~10 frames from game_step()'s cleanup counter. - sid.voices[1].freq = SID_FREQ_PAL(100); - sid.voices[1].attdec = 0x00; - sid.voices[1].susrel = 0xf0; - sid.voices[1].ctrl = SID_CTRL_RECT | SID_CTRL_GATE; - fault_stinger_ticks = 10; -} - -static void game_advance_fault_stinger(void) -{ - // Called once per frame from game_step(). Decrements the - // counter and gates voice 1 off when it reaches zero. Putting - // this here (rather than in game_step_title) means the stinger - // cleans up even if we leave TITLE during the 0.2 sec window. - if (fault_stinger_ticks > 0) { - fault_stinger_ticks--; - if (fault_stinger_ticks == 0) - sid.voices[1].ctrl = SID_CTRL_RECT; - } + audio_state_enter(STATE_GAMEOVER); } // --- per-state step functions ------------------------------------------ @@ -285,9 +270,11 @@ static void game_step_draw(void) // fires in 10 sec, abort the round, trigger a short stinger on // SID voice 1, and go back to TITLE. No point awarded. if (elapsed > 500) { - game_trigger_fault_stinger(); state = STATE_TITLE; game_enter_title(); + // Trigger the stinger AFTER audio_state_enter(TITLE) + // so it isn't silenced by the state transition. + audio_play_fault_stinger(); return; } @@ -363,12 +350,14 @@ void game_init(void) void game_step(void) { - // Per-frame work that must happen regardless of state: tick down - // the fault-stinger counter and gate voice 1 off when it - // reaches zero. Doing this here (not in any per-state step) - // guarantees the stinger cleans up even if we leave TITLE - // during its 0.2 sec lifetime. - game_advance_fault_stinger(); + // Advance the audio state machine. audio_state_step() handles + // the per-frame fault-stinger cleanup AND the per-state + // schedule (READY ping-pong, WAIT loop, DRAW stab cleanup, + // WIN arpeggio, GAMEOVER fanfare). It must be called every + // frame regardless of state, and after any state transition + // that happened in this frame's per-state step (so the new + // state's audio advances by one frame). + audio_state_step(state); switch (state) { case STATE_TITLE: game_step_title(); break; diff --git a/src/main.c b/src/main.c index f120bc2..ef801f0 100644 --- a/src/main.c +++ b/src/main.c @@ -2,17 +2,19 @@ // // Flow: // 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM. -// 2. score_init() — zero both player scores. -// 3. game_init() — enter the TITLE state (which also loads -// the title screen and renders the score -// bar). -// 4. rasterirq_setup() — install the single RIRQ at line 311. +// 2. audio_init() — set SID master volume, no filter, silence +// all 3 voices. Phase 6 addition. +// 3. score_init() — zero both player scores. +// 4. game_init() — enter the TITLE state (which also loads +// the title screen, renders the score bar, +// and calls audio_state_enter(STATE_TITLE)). +// 5. rasterirq_setup() — install the single RIRQ at line 311. // The IRQ handler increments frame_count // and calls game_step() once per frame. -// 5. while (1) {} — idle. All per-frame work happens in +// 6. while (1) {} — idle. All per-frame work happens in // the IRQ handler. // -// In Phase 4 step 5 was `while (1) game_step();` — a busy-wait that +// In Phase 4 step 6 was `while (1) game_step();` — a busy-wait that // called game_step as fast as the CPU could, so the "frame counter" // was CPU-bound. In Phase 5 the busy-wait is gone: game_step is // called from the raster IRQ at exactly 50 Hz, so state durations @@ -20,6 +22,7 @@ // what the CPU is doing between IRQs. #include "memmap.h" +#include "audio.h" #include "game.h" #include "score.h" #include "tick.h" @@ -31,6 +34,7 @@ int main(void) { memmap_setup(); + audio_init(); score_init(); game_init(); rasterirq_setup(); diff --git a/src/notes.h b/src/notes.h new file mode 100644 index 0000000..539d207 --- /dev/null +++ b/src/notes.h @@ -0,0 +1,38 @@ +#ifndef WHACK_HARE_NOTES_H +#define WHACK_HARE_NOTES_H + +// notes.h — 16-bit SID frequency values for the musical notes used by +// the game. PAL C64 (985248 Hz system clock); see the oscar64 sid.h +// header for the formula. The values below are pre-computed with +// the SID_FREQ_PAL() macro (i.e. note_hz * 1115974 / 65536, rounded +// to nearest int). +// +// Equal-tempered scale, A4 = 440 Hz reference. Only the notes we +// actually play are listed — the game uses ~9 specific notes (plus a +// few helpers for the WIN voice-2 thirds and the GAMEOVER voice-2 +// fifth), so a flat list is more readable than a full octave table. + +// Octave 1 +#define NOTE_G1 834 // 49.00 Hz + +// Octave 2 +#define NOTE_C2 1114 // 65.41 Hz (WAIT voice 0 square) + +// Octave 3 +#define NOTE_C3 2227 // 130.81 Hz (WAIT voice 1 triangle, 1 octave up) + +// Octave 4 +#define NOTE_C4 4455 // 261.63 Hz (GAMEOVER voice 1 fifth below) +#define NOTE_G4 6675 // 392.00 Hz (GAMEOVER voice 0 fanfare start) +#define NOTE_A4 7493 // 440.00 Hz (READY A4, WIN voice 1 third below C5) + +// Octave 5 +#define NOTE_C5 8909 // 523.25 Hz (WIN voice 0 arpeggio start) +#define NOTE_E5 11227 // 659.25 Hz +#define NOTE_G5 13350 // 783.99 Hz +#define NOTE_A5 14985 // 880.00 Hz (READY A5, WIN voice 1 third below C6) + +// Octave 6 +#define NOTE_C6 17821 // 1046.50 Hz (WIN arpeggio top note) + +#endif