joeyagi/agiSound.c
2026-10-06 16:59:41 -05:00

262 lines
7.4 KiB
C

// SOUND resources on JoeyLib's three tone voices and noise voice (AGI
// specification 9.2 and 9.5).
//
// A sound is four voices -- three tones and the noise -- each a list of
// 5-byte notes: a 16-bit duration in 1/60 s ticks, then the PCjr sound
// chip bytes in reverse order: the high six bits of the tone's 10-bit
// divisor, a command byte with its low four bits (or, for the noise
// voice, the noise type and rate), and the attenuation (0 loudest, 15
// off). The tone is 111860 / divisor Hz. A voice ends at an FF FF
// duration. The sound's flag is set when the first voice ends, as in
// Sierra's PC interpreter, whose speaker plays only that voice (measured:
// longer second, third and noise voices do not delay it); the others play
// on here until they end or another sound starts. With sound turned off
// (f9 clear) the flag is set at once.
//
// soundPump runs the notes from the millisecond clock every frame, and
// re-asserts the voices (the IIgs's voices are one-shots that must be
// re-triggered about once a frame).
#include "agi.h"
#include <stddef.h>
#include "joey/audio.h"
#include "joey/core.h"
#define SOUND_VOICES 4u
#define SOUND_TONES 3u
#define SOUND_NOISE 3u
#define SOUND_HEADER_BYTES 8u
#define NOTE_BYTES 5u
#define NOTE_END 0xFFFFu
#define TICKS_PER_SECOND 60u
#define MS_PER_SECOND 1000u
#define PSG_CLOCK_HZ 111860u
#define DIVISOR_HIGH_MASK 0x3Fu
#define DIVISOR_LOW_MASK 0x0Fu
#define DIVISOR_HIGH_SHIFT 4u
#define ATTEN_MASK 0x0Fu
#define ATTEN_OFF 15u
#define NOISE_RATE_MASK 0x03u
#define NOISE_RATE_TONE3 3u
// JoeyLib's noise pitch runs 0 (brightest) to 31: the chip's three fixed
// rates an octave apart, and the third tone's divisor scaled down.
#define NOISE_PITCH_FIRST 4u
#define NOISE_PITCH_MAX 31u
#define NOISE_DIVISOR_SHIFT 5u
typedef struct {
uint16_t pos;
uint16_t ticksLeft;
bool done;
} VoiceT;
// ----- Prototypes -----
static void playNote(uint8_t voice, const uint8_t *note);
static void raiseFlag(void);
static void release(void);
static void silence(void);
// ----- Module state -----
static uint8_t *gData;
static uint16_t gLength;
static VoiceT gVoices[SOUND_VOICES];
static uint16_t gToneHz[SOUND_TONES];
static uint8_t gToneAtten[SOUND_TONES];
static uint16_t gToneDivisor[SOUND_TONES];
static uint8_t gNoisePitch;
static uint8_t gNoiseAtten;
static uint32_t gTickStartMs;
static uint32_t gTicksDone;
// The flag sound() named, still to be set while the first voice plays.
static uint8_t gFlag;
static bool gFlagPending;
// ----- Internal helpers (alphabetical) -----
static void playNote(uint8_t voice, const uint8_t *note) {
uint16_t divisor;
uint8_t atten;
atten = (uint8_t)(note[4] & ATTEN_MASK);
if (voice == SOUND_NOISE) {
uint8_t rate;
rate = (uint8_t)(note[3] & NOISE_RATE_MASK);
if (rate == NOISE_RATE_TONE3) {
gNoisePitch = (uint8_t)(gToneDivisor[SOUND_TONES - 1u] >> NOISE_DIVISOR_SHIFT);
} else {
gNoisePitch = (uint8_t)(NOISE_PITCH_FIRST << rate);
}
if (gNoisePitch > NOISE_PITCH_MAX) {
gNoisePitch = NOISE_PITCH_MAX;
}
gNoiseAtten = atten;
jlAudioNoise(gNoisePitch, gNoiseAtten);
return;
}
divisor = (uint16_t)(((uint16_t)(note[2] & DIVISOR_HIGH_MASK) << DIVISOR_HIGH_SHIFT) | (note[3] & DIVISOR_LOW_MASK));
gToneDivisor[voice] = divisor;
gToneHz[voice] = (divisor == 0u) ? 0u : (uint16_t)(PSG_CLOCK_HZ / divisor);
gToneAtten[voice] = atten;
jlAudioVoice(voice, gToneHz[voice], atten);
}
static void raiseFlag(void) {
if (gFlagPending) {
gFlagPending = false;
gAgi->flags[gFlag] = 1u;
}
}
// The sound's data goes and every voice falls silent.
static void release(void) {
silence();
jlFree(gData);
gData = NULL;
}
static void silence(void) {
uint8_t v;
for (v = 0u; v < SOUND_TONES; v++) {
gToneHz[v] = 0u;
gToneAtten[v] = ATTEN_OFF;
jlAudioVoice(v, 0u, ATTEN_OFF);
}
gNoiseAtten = ATTEN_OFF;
jlAudioNoise(0u, ATTEN_OFF);
}
// ----- Public API (alphabetical) -----
void soundInit(void) {
(void)jlAudioInit();
silence();
}
// Advance the notes to the clock; called every frame.
void soundPump(void) {
uint32_t now;
uint32_t due;
uint8_t v;
bool allDone;
if (gData == NULL) {
return;
}
now = jlMillisElapsed();
due = (uint32_t)((now - gTickStartMs) * TICKS_PER_SECOND / MS_PER_SECOND);
while (gTicksDone < due && gData != NULL) {
gTicksDone++;
allDone = true;
for (v = 0u; v < SOUND_VOICES; v++) {
VoiceT *vc = &gVoices[v];
while (!vc->done && vc->ticksLeft == 0u) {
const uint8_t *note;
uint16_t dur;
if ((uint32_t)vc->pos + NOTE_BYTES > gLength) {
vc->done = true;
break;
}
note = &gData[vc->pos];
dur = (uint16_t)(note[0] | ((uint16_t)note[1] << 8));
if (dur == NOTE_END) {
vc->done = true;
break;
}
playNote(v, note);
vc->pos = (uint16_t)(vc->pos + NOTE_BYTES);
vc->ticksLeft = dur;
}
if (!vc->done) {
vc->ticksLeft--;
allDone = false;
} else if (v < SOUND_TONES) {
jlAudioVoice(v, 0u, ATTEN_OFF);
gToneHz[v] = 0u;
} else {
gNoiseAtten = ATTEN_OFF;
jlAudioNoise(0u, ATTEN_OFF);
}
}
if (gVoices[0].done) {
raiseFlag();
}
if (allDone) {
release();
}
}
#if defined(JOEYLIB_PLATFORM_IIGS)
// The IIgs voices are one-shots: hold the notes by re-triggering.
if (gData != NULL) {
for (v = 0u; v < SOUND_TONES; v++) {
if (gToneHz[v] != 0u && gToneAtten[v] < ATTEN_OFF) {
jlAudioVoice(v, gToneHz[v], gToneAtten[v]);
}
}
if (gNoiseAtten < ATTEN_OFF) {
jlAudioNoise(gNoisePitch, gNoiseAtten);
}
}
#endif
}
void soundShutdown(void) {
release();
jlAudioShutdown();
}
// sound(n, f): start a sound; f is set when it ends (or is stopped).
void soundStart(uint8_t soundId, uint8_t flag) {
uint8_t v;
soundStop();
gFlag = flag;
gAgi->flags[flag] = 0u;
if (gAgi->flags[FLAG_SOUND] == 0u) {
gAgi->flags[flag] = 1u;
return;
}
gData = agiResLoad(&gAgi->game, AGI_RES_SOUND, soundId, &gLength);
if (gData == NULL || gLength < SOUND_HEADER_BYTES) {
jlFree(gData);
gData = NULL;
gAgi->flags[flag] = 1u;
return;
}
for (v = 0u; v < SOUND_VOICES; v++) {
gVoices[v].pos = (uint16_t)(gData[v * 2u] | ((uint16_t)gData[v * 2u + 1u] << 8));
gVoices[v].ticksLeft = 0u;
gVoices[v].done = gVoices[v].pos == 0u || gVoices[v].pos >= gLength;
}
gFlagPending = true;
gTickStartMs = jlMillisElapsed();
gTicksDone = 0u;
}
// stop.sound: silence, and the playing sound's flag is set.
void soundStop(void) {
raiseFlag();
if (gData != NULL) {
release();
}
}