// 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 #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(); } }