joeylib2/examples/spacetaxi/stAudio.c

186 lines
5.2 KiB
C

// Space Taxi -- audio sink for the simulation's SID events.
//
// The C64 drives three SID voices: voice 1 carries the SFX programs
// (landing, gear, crash, fuel) and the thrust/crash frequency sweep,
// voice 2 the level gimmick tones (laser hum, puzzle chime), voice 3
// the jet noise. JoeyLib's audio HAL is a PSG-style tone/noise
// interface, so each SID program is mapped to its pitch and release
// time; envelopes and waveforms are approximated with an attenuation
// ramp. The speech samples of the original are not extracted; each
// spoken character becomes a short chirp so the cues still land.
#include "spacetaxi.h"
// SID voice index in a program's byte 8 -> JoeyLib voice slot.
#define ST_VOICE_SFX 1u // SID voice 1
#define ST_VOICE_TONE 2u // SID voice 2
#define ST_VOICE_NOISE 3u // SID voice 3 (noise)
#define ST_ATTEN_LOUD 4u
#define ST_ATTEN_SOFT 8u
#define ST_ATTEN_OFF 15u
// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096.
#define ST_SID_HZ_NUM 1022727UL
#define ST_SID_HZ_DEN 16777216UL
// Speech chirp: one short tone per character, pitched by the character.
#define ST_SPEECH_TICKS 3u
#define ST_SPEECH_BASE_HZ 300u
#define ST_SPEECH_STEP_HZ 9u
// Thrust sweep register value -> Hz: the C64 writes the same byte to
// both frequency bytes, so the word is value * 257.
#define ST_SWEEP_WORD_SCALE 257u
typedef struct {
uint16_t ticksLeft; // release countdown (game ticks); 0 = idle
uint8_t voice;
} StSfxSlotT;
static StSfxSlotT gSfx; // SID voice 1 (programs + sweep)
static StSfxSlotT gTone; // SID voice 2
static bool gNoiseOn;
static uint16_t gSpeechTicks;
static uint16_t sidHz(uint16_t word);
static void voiceOff(StSfxSlotT *slot);
static uint16_t sidHz(uint16_t word) {
uint32_t hz = ((uint32_t)word * ST_SID_HZ_NUM) / ST_SID_HZ_DEN;
if (hz > 20000u) {
hz = 20000u;
}
return (uint16_t)hz;
}
static void voiceOff(StSfxSlotT *slot) {
if (slot->ticksLeft != 0u) {
jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF);
slot->ticksLeft = 0u;
}
}
// Once per game tick: run the release timers ($4320 sfxEnvelopeTick).
void stAudioTick(void) {
if (gSfx.ticksLeft != 0u) {
gSfx.ticksLeft--;
if (gSfx.ticksLeft == 0u) {
jlAudioVoice(gSfx.voice, 0u, ST_ATTEN_OFF);
}
}
if (gTone.ticksLeft != 0u) {
gTone.ticksLeft--;
if (gTone.ticksLeft == 0u) {
jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF);
}
}
if (gSpeechTicks != 0u) {
gSpeechTicks--;
if (gSpeechTicks == 0u) {
jlAudioVoice(ST_VOICE_TONE, 0u, ST_ATTEN_OFF);
}
}
}
void stAudioInit(void) {
(void)jlAudioInit();
gSfx.voice = ST_VOICE_SFX;
gSfx.ticksLeft = 0u;
gTone.voice = ST_VOICE_TONE;
gTone.ticksLeft = 0u;
gNoiseOn = false;
gSpeechTicks = 0u;
}
void stAudioShutdown(void) {
voiceOff(&gSfx);
voiceOff(&gTone);
jlAudioNoise(0u, ST_ATTEN_OFF);
jlAudioShutdown();
}
// $6D6A / $6A3B -- voice 3 jet noise gate.
void stAudioNoise(bool on) {
if (on == gNoiseOn) {
return;
}
gNoiseOn = on;
if (on) {
jlAudioNoise(31u, ST_ATTEN_SOFT);
} else {
jlAudioNoise(0u, ST_ATTEN_OFF);
}
}
// $42E9 -- a 9-byte SID program: freq lo/hi, pulse lo/hi, control,
// AD, SR, release ticks, voice.
void stAudioSfx(const uint8_t *program9) {
uint16_t word = (uint16_t)(program9[0] | ((uint16_t)program9[1] << 8));
uint8_t ctrl = program9[4];
uint8_t ticks = program9[7];
StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx;
if ((ctrl & 0x80u) != 0u) {
// Noise waveform: a burst on the noise generator.
jlAudioNoise(31u, ST_ATTEN_LOUD);
gNoiseOn = true;
return;
}
jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD);
slot->ticksLeft = (uint16_t)(ticks + 1u);
}
// $6C7F / $6CAA -- voices 1 and 2 gated off.
void stAudioSilence(void) {
voiceOff(&gSfx);
voiceOff(&gTone);
}
// $9802 -- one spoken character (a chirp here).
void stAudioSpeech(uint8_t ch) {
jlAudioVoice(ST_VOICE_TONE, (uint16_t)(ST_SPEECH_BASE_HZ + (uint16_t)(ch & 0x1Fu) * ST_SPEECH_STEP_HZ), ST_ATTEN_LOUD);
gSpeechTicks = ST_SPEECH_TICKS;
}
// $6A8A -- the crash scream: voice 1 frequency written from the sweep.
void stAudioThrustSweep(uint8_t value) {
jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD);
gSfx.ticksLeft = 3u;
}
// $6E71 -- the fuel pump's rising note on voice 1.
void stAudioVoice1Freq(uint8_t value) {
jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD);
gSfx.ticksLeft = 2u;
}
// Level hooks poke voice 2 directly (laser hum, puzzle chime).
void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) {
uint16_t word = (uint16_t)(freqLo | ((uint16_t)freqHi << 8));
if ((ctrl & 0x80u) != 0u) {
jlAudioNoise((uint8_t)(freqHi & 31u), ST_ATTEN_SOFT);
gNoiseOn = true;
return;
}
jlAudioVoice(ST_VOICE_TONE, sidHz(word), ST_ATTEN_SOFT);
gTone.ticksLeft = 2u;
}