Phase 6: SID audio (5 cues + fault stinger)
This commit is contained in:
+413
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user