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.
|
// on entry.
|
||||||
|
|
||||||
#include "game.h"
|
#include "game.h"
|
||||||
|
#include "audio.h"
|
||||||
#include "screens.h"
|
#include "screens.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
#include "score.h"
|
#include "score.h"
|
||||||
@@ -80,13 +81,11 @@ static char draw_was_pressed[2];
|
|||||||
|
|
||||||
// --- fault stinger -----------------------------------------------------
|
// --- fault stinger -----------------------------------------------------
|
||||||
//
|
//
|
||||||
// When the DRAW state's 500-frame fault timeout fires, we trigger a
|
// When the DRAW state's 500-frame fault timeout fires, game_step_draw
|
||||||
// short low-square-wave stinger on SID voice 1. This counter is
|
// transitions to TITLE and then calls audio_play_fault_stinger() to
|
||||||
// decremented every frame in game_step() (not in any per-state step,
|
// start a 10-frame low square-wave burst on SID voice 1. The
|
||||||
// so it cleans up even if we transition out of TITLE before the
|
// cleanup logic lives in audio.c (audio_advance_fault_stinger, called
|
||||||
// stinger would naturally end). When it reaches 0, we gate voice 1
|
// from audio_state_step). See audio.c for details.
|
||||||
// off. ~10 frames = ~0.2 sec at 50 Hz.
|
|
||||||
static byte fault_stinger_ticks;
|
|
||||||
|
|
||||||
// --- per-state enter functions -----------------------------------------
|
// --- per-state enter functions -----------------------------------------
|
||||||
//
|
//
|
||||||
@@ -99,6 +98,15 @@ static byte fault_stinger_ticks;
|
|||||||
// other states are black)
|
// other states are black)
|
||||||
// - samples the SID random for WAIT duration (in READY enter, not
|
// - samples the SID random for WAIT duration (in READY enter, not
|
||||||
// WAIT enter, per the Phase 5 spec)
|
// 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)
|
static void game_enter_title(void)
|
||||||
{
|
{
|
||||||
@@ -109,6 +117,7 @@ static void game_enter_title(void)
|
|||||||
title_first_frame = 0;
|
title_first_frame = 0;
|
||||||
// Border starts white (the "PRESS FIRE" prompt is visible).
|
// Border starts white (the "PRESS FIRE" prompt is visible).
|
||||||
vic.color_border = 1;
|
vic.color_border = 1;
|
||||||
|
audio_state_enter(STATE_TITLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_ready(void)
|
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.
|
// driven by an LFSR and effectively random between reads.
|
||||||
wait_duration_frames = 100 + (sid.random % 150);
|
wait_duration_frames = 100 + (sid.random % 150);
|
||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
|
audio_state_enter(STATE_READY);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_wait(void)
|
static void game_enter_wait(void)
|
||||||
@@ -130,6 +140,7 @@ static void game_enter_wait(void)
|
|||||||
score_render();
|
score_render();
|
||||||
enter_frame = frame_count;
|
enter_frame = frame_count;
|
||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
|
audio_state_enter(STATE_WAIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_draw(void)
|
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
|
vic.color_border = 1; // white border matches the white screen
|
||||||
draw_was_pressed[0] = 0;
|
draw_was_pressed[0] = 0;
|
||||||
draw_was_pressed[1] = 0;
|
draw_was_pressed[1] = 0;
|
||||||
|
audio_state_enter(STATE_DRAW);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_win_p1(void)
|
static void game_enter_win_p1(void)
|
||||||
@@ -149,6 +161,7 @@ static void game_enter_win_p1(void)
|
|||||||
score_render();
|
score_render();
|
||||||
enter_frame = frame_count;
|
enter_frame = frame_count;
|
||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
|
audio_state_enter(STATE_WIN_P1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_win_p2(void)
|
static void game_enter_win_p2(void)
|
||||||
@@ -158,6 +171,7 @@ static void game_enter_win_p2(void)
|
|||||||
score_render();
|
score_render();
|
||||||
enter_frame = frame_count;
|
enter_frame = frame_count;
|
||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
|
audio_state_enter(STATE_WIN_P2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_gameover(void)
|
static void game_enter_gameover(void)
|
||||||
@@ -171,36 +185,7 @@ static void game_enter_gameover(void)
|
|||||||
score_render();
|
score_render();
|
||||||
enter_frame = frame_count;
|
enter_frame = frame_count;
|
||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
}
|
audio_state_enter(STATE_GAMEOVER);
|
||||||
|
|
||||||
// --- 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- per-state step functions ------------------------------------------
|
// --- 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
|
// fires in 10 sec, abort the round, trigger a short stinger on
|
||||||
// SID voice 1, and go back to TITLE. No point awarded.
|
// SID voice 1, and go back to TITLE. No point awarded.
|
||||||
if (elapsed > 500) {
|
if (elapsed > 500) {
|
||||||
game_trigger_fault_stinger();
|
|
||||||
state = STATE_TITLE;
|
state = STATE_TITLE;
|
||||||
game_enter_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,12 +350,14 @@ void game_init(void)
|
|||||||
|
|
||||||
void game_step(void)
|
void game_step(void)
|
||||||
{
|
{
|
||||||
// Per-frame work that must happen regardless of state: tick down
|
// Advance the audio state machine. audio_state_step() handles
|
||||||
// the fault-stinger counter and gate voice 1 off when it
|
// the per-frame fault-stinger cleanup AND the per-state
|
||||||
// reaches zero. Doing this here (not in any per-state step)
|
// schedule (READY ping-pong, WAIT loop, DRAW stab cleanup,
|
||||||
// guarantees the stinger cleans up even if we leave TITLE
|
// WIN arpeggio, GAMEOVER fanfare). It must be called every
|
||||||
// during its 0.2 sec lifetime.
|
// frame regardless of state, and after any state transition
|
||||||
game_advance_fault_stinger();
|
// 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) {
|
switch (state) {
|
||||||
case STATE_TITLE: game_step_title(); break;
|
case STATE_TITLE: game_step_title(); break;
|
||||||
|
|||||||
+11
-7
@@ -2,17 +2,19 @@
|
|||||||
//
|
//
|
||||||
// Flow:
|
// Flow:
|
||||||
// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM.
|
// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM.
|
||||||
// 2. score_init() — zero both player scores.
|
// 2. audio_init() — set SID master volume, no filter, silence
|
||||||
// 3. game_init() — enter the TITLE state (which also loads
|
// all 3 voices. Phase 6 addition.
|
||||||
// the title screen and renders the score
|
// 3. score_init() — zero both player scores.
|
||||||
// bar).
|
// 4. game_init() — enter the TITLE state (which also loads
|
||||||
// 4. rasterirq_setup() — install the single RIRQ at line 311.
|
// 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
|
// The IRQ handler increments frame_count
|
||||||
// and calls game_step() once per frame.
|
// 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.
|
// 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"
|
// 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
|
// 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
|
// called from the raster IRQ at exactly 50 Hz, so state durations
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
// what the CPU is doing between IRQs.
|
// what the CPU is doing between IRQs.
|
||||||
|
|
||||||
#include "memmap.h"
|
#include "memmap.h"
|
||||||
|
#include "audio.h"
|
||||||
#include "game.h"
|
#include "game.h"
|
||||||
#include "score.h"
|
#include "score.h"
|
||||||
#include "tick.h"
|
#include "tick.h"
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
memmap_setup();
|
memmap_setup();
|
||||||
|
audio_init();
|
||||||
score_init();
|
score_init();
|
||||||
game_init();
|
game_init();
|
||||||
rasterirq_setup();
|
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