// DVX audio HAL: JoeyLib's sound through DVX's sound library (audio.lib) // and music library (music.lib), which own the sound card for every // program on the desktop and mix them all together. // // MOD music.lib (libxmp-lite inside DVX: no second copy here), // loaded from memory; music.lib plays it through audio.lib. // SFX five slots, each at most one sound: a one-shot is an // audio.lib sample made from the program's signed 8-bit PCM // (the top bit flipped: DVX's 8-bit samples are unsigned); a // stream is an audio.lib pull stream whose fill calls the // program's, flipping the same bit. JoeyLib's end of stream // (a fill returning 0) closes the stream once what it queued // has played. Pan sticks to the slot, as on every port. // Tone, audio.lib tones that play until changed: jlAudioTone a // voices, square, the three jlAudioVoice voices squares, jlAudioNoise // noise a noise tone. DVX puts squares on the FM chip when there is // one and mixes them otherwise, and keeps noise in the mixer. // // None of this may run inside an interrupt (audio.lib's rule), so // jlAudioTickRegister's callback runs in program time: a DVX timer counts // the ticks at the asked rate, and jlAudioFrameTick delivers the ones that // came due, in order. A callback can therefore do anything; what DVX loses // against DOS is the sub-frame placement of what it does. jlAudioCritical* // stay the generic no-ops: nothing the callback touches can be interrupted // by it. // // jlAudioFrameTick is also where DVX's music and streams are fed // (musicPump, which pumps audio.lib too) when the program keeps the // desktop busy. A context that could not be opened -- DVX without a sound // library -- leaves everything silent; DVX without a sound card still opens // one (its null driver plays nothing). #include "dvxSdk.h" #include #include #include "port.h" #include "joey/audio.h" #include "dvxPort.h" // ----- Constants ----- #define NO_VOICE 0 // audio.lib voice ids are > 0 #define NO_TIMER (-1) // JoeyLib SFX are signed 8-bit; DVX's 8-bit PCM is unsigned. #define PCM8_SIGN_FLIP 0x80u #define SFX_BITS 8 #define SFX_CHANNELS 1 #define SFX_VOLUME AUDIO_VOLUME_MAX // A stream's own buffer: DVX refills it every desktop frame or so. #define STREAM_BUFFER_MS 100 // Tone voices at half scale: the three voices, the noise and the tone // together stay clear of the mixer's clipping at full volume. #define TONE_VOLUME (AUDIO_VOLUME_MAX / 2) #define TONE_UNTIL_STOPPED 0 // Attenuation 15 (and up) is silence (joey/audio.h). #define ATTEN_SILENT 15u // jlAudioNoise pitch 0..31, spread linearly over the noise rates like the // shared curve the other ports follow (26000 - pitch * 750 Hz, whose top // audio.lib cannot reach): from the fastest rate a tone may have down to // that curve's slowest. #define NOISE_PITCH_MAX 31u #define NOISE_CURVE_TOP_HZ 26000u #define NOISE_CURVE_STEP_HZ 750u #define NOISE_MAX_HZ AUDIO_TONE_MAX_HZ #define NOISE_MIN_HZ (NOISE_CURVE_TOP_HZ - NOISE_PITCH_MAX * NOISE_CURVE_STEP_HZ) // Pinned for the timer interrupt: the tick counter's code (one small // function; a page is as fine a grain as pinning has) and data. #define TICK_ISR_LOCK_BYTES 4096u // ----- Types ----- // One SFX slot: a one-shot sample or a stream (or neither), its voice and // its sticky pan. typedef struct { AudioSampleT *sample; AudioStreamT *stream; jlAudioStreamFillT fill; void *fillCtx; int32_t voice; int32_t pan; bool streamEnded; } SfxSlotT; // One tone channel (the tone, a voice, the noise): an audio.lib tone that // plays until changed, or NO_VOICE while silent. typedef struct { AudioWaveE wave; int32_t voice; } ToneChannelT; // ----- Prototypes ----- static AudioCtxT *audioContext(void); static void sfxFormat(uint16_t rateHz, AudioFormatT *fmt, int32_t *pitch); static void sfxStart(uint8_t slot, int32_t voice, uint16_t rateHz); static void sfxStop(uint8_t slot); static void streamEndReap(void); static uint32_t streamFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData); static void tickDeliver(void); static void tickIsr(void *userData); static void tickStop(void); static void toneSet(ToneChannelT *channel, uint32_t hz, int32_t volume); static int32_t voiceVolume(uint8_t atten); // ----- Module state ----- static AudioCtxT *gCtx = NULL; static MusicT *gMusic = NULL; static SfxSlotT gSlots[JOEY_AUDIO_SFX_SLOTS]; // DVX pan for each JoeyLib pan. static const int32_t gPanToDvx[AUDIO_PAN_COUNT] = { [AUDIO_PAN_LEFT] = DVX_PAN_LEFT, [AUDIO_PAN_CENTER] = DVX_PAN_CENTER, [AUDIO_PAN_RIGHT] = DVX_PAN_RIGHT }; static ToneChannelT gTone = { AUDIO_WAVE_SQUARE, NO_VOICE }; static ToneChannelT gVoices[JOEY_AUDIO_VOICES] = { { AUDIO_WAVE_SQUARE, NO_VOICE }, { AUDIO_WAVE_SQUARE, NO_VOICE }, { AUDIO_WAVE_SQUARE, NO_VOICE } }; static ToneChannelT gNoise = { AUDIO_WAVE_NOISE, NO_VOICE }; // Volume for an SN76489 / AGI attenuation below silence: 2 dB per step, // AUDIO_VOLUME_MAX * 10^(-atten / 10). static const uint16_t gAttenVolume[ATTEN_SILENT] = { 256, 203, 162, 128, 102, 81, 64, 51, 41, 32, 26, 20, 16, 13, 10 }; // The converted copy of the last one-shot's PCM (audio.lib copies it again). static uint8_t *gPcm = NULL; static uint32_t gPcmSize = 0; // jlAudioTickRegister: the callback, its DVX timer and the ticks the timer // has counted that are not delivered yet. static jlAudioTickFnT gTickFn = NULL; static int32_t gTickTimer = NO_TIMER; static volatile uint32_t gTicksDue = 0; static bool gTickPinned = false; // ----- Internal helpers (alphabetical) ----- // The program's audio context, opened on first use: the voice layer may be // used without jlAudioInit, as on the other ports. static AudioCtxT *audioContext(void) { if (gCtx == NULL) { gCtx = audioOpen(); } return gCtx; } // A one-shot's PCM format, and the pitch that makes a rate audio.lib cannot // take play right: it is created at the nearest rate it takes and played // slower or faster. static void sfxFormat(uint16_t rateHz, AudioFormatT *fmt, int32_t *pitch) { uint32_t rate; rate = rateHz < AUDIO_RATE_MIN ? AUDIO_RATE_MIN : (rateHz > AUDIO_RATE_MAX ? AUDIO_RATE_MAX : rateHz); fmt->rate = rate; fmt->bits = SFX_BITS; fmt->channels = SFX_CHANNELS; *pitch = (int32_t)(AUDIO_PITCH_NORMAL * (uint32_t)rateHz / rate); } // A slot's new voice: its pan, and its pitch for a rate audio.lib cannot // take directly. static void sfxStart(uint8_t slot, int32_t voice, uint16_t rateHz) { AudioFormatT fmt; int32_t pitch; if (voice <= 0) { return; } gSlots[slot].voice = voice; sfxFormat(rateHz, &fmt, &pitch); if (pitch != AUDIO_PITCH_NORMAL) { (void)audioVoiceSetPitch(gCtx, voice, pitch); } (void)audioVoiceSetPan(gCtx, voice, gSlots[slot].pan); } // Stop a slot: its stream closed or its sample freed (either stops the // voice). static void sfxStop(uint8_t slot) { SfxSlotT *s; s = &gSlots[slot]; audioStreamClose(s->stream); audioSampleFree(s->sample); s->stream = NULL; s->sample = NULL; s->fill = NULL; s->voice = NO_VOICE; s->streamEnded = false; } // Close the streams whose program fill has ended, once what they queued // has played. static void streamEndReap(void) { uint8_t slot; for (slot = 0; slot < JOEY_AUDIO_SFX_SLOTS; slot++) { if (gSlots[slot].streamEnded && audioStreamQueued(gSlots[slot].stream) == 0) { sfxStop(slot); } } } // audio.lib asks a slot's stream for PCM: the program's fill writes signed // bytes in place, which then become unsigned. Its 0 is the end of the // stream; audio.lib's 0 only means "nothing yet", so the slot remembers. static uint32_t streamFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) { SfxSlotT *s; uint8_t *pcm; uint32_t got; uint32_t i; (void)stream; s = (SfxSlotT *)userData; if (s->streamEnded) { return 0; } got = s->fill(s->fillCtx, (int8_t *)buf, frames); if (got == 0) { s->streamEnded = true; return 0; } got = got < frames ? got : frames; pcm = (uint8_t *)buf; for (i = 0; i < got; i++) { pcm[i] ^= PCM8_SIGN_FLIP; } return got; } // Run the tick callback once for every tick the timer counted since last // time. static void tickDeliver(void) { uint32_t state; uint32_t ticks; if (gTickFn == NULL) { return; } state = platformIrqSave(); ticks = gTicksDue; gTicksDue = 0; platformIrqRestore(state); while (ticks > 0 && gTickFn != NULL) { ticks--; gTickFn(); } } // The DVX timer callback, inside the timer interrupt: count, nothing more. static void tickIsr(void *userData) { (void)userData; gTicksDue++; } static void tickStop(void) { if (gTickTimer != NO_TIMER) { platformTimerRemove(gTickTimer); gTickTimer = NO_TIMER; } gTickFn = NULL; gTicksDue = 0; if (gTickPinned) { (void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true); (void)platformUnlockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false); gTickPinned = false; } } // A tone channel at hz and volume, 0 for either being silence. A playing // tone is changed in place; one audio.lib dropped (its voice limit) or // never started is started. static void toneSet(ToneChannelT *channel, uint32_t hz, int32_t volume) { AudioCtxT *ctx; // Silence needs no context: a channel only has a voice when there is one. if (hz == 0 || volume == 0) { if (channel->voice != NO_VOICE) { (void)audioVoiceStop(gCtx, channel->voice); channel->voice = NO_VOICE; } return; } ctx = audioContext(); if (ctx == NULL) { return; } hz = hz < AUDIO_TONE_MIN_HZ ? AUDIO_TONE_MIN_HZ : (hz > AUDIO_TONE_MAX_HZ ? AUDIO_TONE_MAX_HZ : hz); if (channel->voice != NO_VOICE && audioVoiceSetFrequency(ctx, channel->voice, hz) == AUDIO_OK && audioVoiceSetVolume(ctx, channel->voice, volume) == AUDIO_OK) { return; } channel->voice = audioTone(ctx, channel->wave, hz, volume, TONE_UNTIL_STOPPED); if (channel->voice < 0) { channel->voice = NO_VOICE; } } static int32_t voiceVolume(uint8_t atten) { if (atten >= ATTEN_SILENT) { return 0; } return (int32_t)gAttenVolume[atten] * TONE_VOLUME / AUDIO_VOLUME_MAX; } // ----- HAL API (alphabetical) ----- void jlpAudioFrameTick(void) { tickDeliver(); streamEndReap(); // Feeds the music and the streams; pumps audio.lib too. musicPump(); } bool jlpAudioInit(void) { uint8_t slot; for (slot = 0; slot < JOEY_AUDIO_SFX_SLOTS; slot++) { gSlots[slot].pan = DVX_PAN_CENTER; } return audioContext() != NULL; } bool jlpAudioIsPlayingMod(void) { return gMusic != NULL && musicIsPlaying(gMusic); } void jlpAudioNoise(uint8_t pitch, uint8_t atten) { uint32_t hz; hz = NOISE_MAX_HZ - (uint32_t)pitch * (NOISE_MAX_HZ - NOISE_MIN_HZ) / NOISE_PITCH_MAX; toneSet(&gNoise, hz, voiceVolume(atten)); } void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { jlpAudioStopMod(); if (musicLoadMem(data, length, &gMusic) != MUSIC_OK) { gMusic = NULL; return; } (void)musicPlay(gMusic, loop); } void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { AudioFormatT fmt; int32_t pitch; uint8_t *grown; uint32_t i; sfxStop(slot); if (length > gPcmSize) { grown = (uint8_t *)realloc(gPcm, length); if (grown == NULL) { return; } gPcm = grown; gPcmSize = length; } for (i = 0; i < length; i++) { gPcm[i] = (uint8_t)(sample[i] ^ PCM8_SIGN_FLIP); } sfxFormat(rateHz, &fmt, &pitch); if (audioSampleCreate(gCtx, gPcm, length, &fmt, &gSlots[slot].sample) != AUDIO_OK) { gSlots[slot].sample = NULL; return; } sfxStart(slot, audioPlay(gSlots[slot].sample, SFX_VOLUME, gSlots[slot].pan, false), rateHz); } void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { AudioFormatT fmt; int32_t pitch; SfxSlotT *s; sfxStop(slot); s = &gSlots[slot]; s->fill = fill; s->fillCtx = ctx; sfxFormat(rateHz, &fmt, &pitch); if (audioStreamOpen(gCtx, &fmt, STREAM_BUFFER_MS, streamFill, s, &s->stream) != AUDIO_OK) { s->stream = NULL; s->fill = NULL; return; } sfxStart(slot, audioStreamVoice(s->stream), rateHz); } void jlpAudioSetPan(uint8_t slot, uint8_t pan) { gSlots[slot].pan = gPanToDvx[pan]; if (gSlots[slot].voice != NO_VOICE) { (void)audioVoiceSetPan(gCtx, gSlots[slot].voice, gSlots[slot].pan); } } void jlpAudioShutdown(void) { uint8_t i; tickStop(); jlpAudioStopMod(); for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) { sfxStop(i); } toneSet(&gTone, 0, 0); toneSet(&gNoise, 0, 0); for (i = 0; i < JOEY_AUDIO_VOICES; i++) { toneSet(&gVoices[i], 0, 0); } free(gPcm); gPcm = NULL; gPcmSize = 0; audioClose(gCtx); gCtx = NULL; } void jlpAudioStopMod(void) { musicFree(gMusic); gMusic = NULL; } void jlpAudioStopSfx(uint8_t slot) { sfxStop(slot); } bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { tickStop(); if (fn == NULL) { return true; } if (platformLockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true) != 0) { return false; } if (platformLockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false) != 0) { (void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true); return false; } gTickPinned = true; gTickFn = fn; gTickTimer = gameTimerAdd(gDvxPortProgram->shellCtx, hz, tickIsr, NULL); if (gTickTimer < 0) { gTickTimer = NO_TIMER; tickStop(); return false; } return true; } void jlpAudioTone(uint16_t freqHz) { toneSet(&gTone, freqHz, TONE_VOLUME); } void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { toneSet(&gVoices[voice], freqHz, voiceVolume(atten)); }