262 lines
7.4 KiB
C
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();
|
|
}
|
|
}
|