Phase 6: SID audio (5 cues + fault stinger)

This commit is contained in:
ballz
2026-07-17 02:15:54 +02:00
parent d538cf46e2
commit a621494a21
5 changed files with 548 additions and 51 deletions
+413
View File
@@ -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 <c64/sid.h>
#include <c64/types.h>
// --- 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;
}
}
+53
View File
@@ -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
+33 -44
View File
@@ -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;
+11 -7
View File
@@ -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();
+38
View File
@@ -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