491 lines
15 KiB
C
491 lines
15 KiB
C
// 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 <stdint.h>
|
|
#include <stdlib.h>
|
|
|
|
#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));
|
|
}
|