Files
libakgl/src/audio.c

519 lines
16 KiB
C
Raw Normal View History

/**
* @file audio.c
* @brief Implements the audio subsystem.
*/
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/audio.h>
#include <akgl/error.h>
akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
/*
* The device akgl_audio_init() opened, or NULL when the voice table is not
* connected to one. Everything that mutates a voice locks this stream when it
* is open, because the device callback reads the same table on SDL's audio
* thread. When it is NULL there is no other thread to race with.
*/
static SDL_AudioStream *audiostream = NULL;
/** @brief Level every voice is scaled by before the mix is clamped. */
static float32_t mastervolume = 1.0f;
/** @brief Scratch the device callback mixes into before handing it to SDL. */
static float32_t mixbuffer[AKGL_AUDIO_MIX_FRAMES];
/*
* State for the noise waveform. A 32-bit xorshift rather than rand(): it needs
* no allocation, no locking beyond what already guards the voice table, and it
* produces the same sequence every run, so a test can assert on noise output.
*/
static uint32_t noisestate = 0x13579bdfu;
/** @brief Whether the voice table has been given its defaults yet. */
static bool voicesready = false;
/**
* @brief Put every voice back to a flat, audible default.
*
* A zeroed voice has a sustain of 0.0, which is silence -- so a caller who
* sounded a note without first describing an envelope would get nothing and no
* error saying why. The default is instead the simplest thing that makes noise:
* a square wave with no attack, no decay and no release, held at full level for
* as long as the gate is open.
*/
static void reset_voices(void)
{
int i = 0;
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
akgl_audio_voices[i].active = false;
akgl_audio_voices[i].waveform = AKGL_AUDIO_WAVE_SQUARE;
akgl_audio_voices[i].hz = 0.0f;
akgl_audio_voices[i].phase = 0.0f;
akgl_audio_voices[i].duration_frames = 0;
akgl_audio_voices[i].elapsed_frames = 0;
akgl_audio_voices[i].attack_frames = 0;
akgl_audio_voices[i].decay_frames = 0;
akgl_audio_voices[i].release_frames = 0;
akgl_audio_voices[i].sustain = 1.0f;
akgl_audio_voices[i].sweep_from_hz = 0.0f;
akgl_audio_voices[i].sweep_to_hz = 0.0f;
akgl_audio_voices[i].sweep_step_hz = 0.0f;
}
mastervolume = 1.0f;
voicesready = true;
}
/**
* @brief Give the voice table its defaults the first time anything touches it.
*
* The table is process-wide static storage, so it is reachable before
* akgl_audio_init() has run. Rather than make every entry point document an
* ordering requirement, the first one to arrive sets the defaults.
*/
static void ensure_voices(void)
{
if ( voicesready == false ) {
reset_voices();
}
}
/** @brief Lock the voice table against the device callback, if one is running. */
static void lock_voices(void)
{
if ( audiostream != NULL ) {
SDL_LockAudioStream(audiostream);
}
}
/** @brief Release the lock taken by lock_voices(). */
static void unlock_voices(void)
{
if ( audiostream != NULL ) {
SDL_UnlockAudioStream(audiostream);
}
}
/** @brief Convert a duration in milliseconds to a whole number of frames. */
static uint32_t frames_for_ms(uint32_t ms)
{
return (uint32_t)(((uint64_t)ms * AKGL_AUDIO_SAMPLE_RATE) / 1000);
}
/** @brief Next value of the noise oscillator, in the range -1.0 to 1.0. */
static float32_t noise_sample(void)
{
noisestate ^= noisestate << 13;
noisestate ^= noisestate >> 17;
noisestate ^= noisestate << 5;
// The top 24 bits are the well-mixed ones; scale them to -1..1.
return (((float32_t)(noisestate >> 8) / 8388607.5f) - 1.0f);
}
/** @brief One sample of @p voice's oscillator at its current phase. */
static float32_t voice_oscillator(akgl_AudioVoice *voice)
{
float32_t value = 0.0f;
switch ( voice->waveform ) {
case AKGL_AUDIO_WAVE_TRIANGLE:
if ( voice->phase < 0.5f ) {
value = (4.0f * voice->phase) - 1.0f;
} else {
value = 3.0f - (4.0f * voice->phase);
}
break;
case AKGL_AUDIO_WAVE_SAWTOOTH:
value = (2.0f * voice->phase) - 1.0f;
break;
case AKGL_AUDIO_WAVE_SQUARE:
value = ( voice->phase < 0.5f ) ? 1.0f : -1.0f;
break;
case AKGL_AUDIO_WAVE_NOISE:
value = noise_sample();
break;
case AKGL_AUDIO_WAVE_SINE:
value = SDL_sinf(voice->phase * 2.0f * SDL_PI_F);
break;
}
return value;
}
/**
* @brief Envelope level @p frame frames into the gate, before the release.
*
* Split out because the release has to start from wherever the gate left off,
* which for a gate shorter than attack plus decay is partway up or down a
* ramp rather than at the sustain level.
*/
static float32_t voice_gate_level(akgl_AudioVoice *voice, uint32_t frame)
{
uint32_t elapsed = frame;
if ( elapsed < voice->attack_frames ) {
return (float32_t)elapsed / (float32_t)voice->attack_frames;
}
elapsed -= voice->attack_frames;
if ( elapsed < voice->decay_frames ) {
return 1.0f - ((1.0f - voice->sustain) * ((float32_t)elapsed / (float32_t)voice->decay_frames));
}
return voice->sustain;
}
/** @brief Envelope level for @p voice where it currently stands. */
static float32_t voice_envelope(akgl_AudioVoice *voice)
{
uint32_t released = 0;
float32_t gatelevel = 0.0f;
if ( voice->elapsed_frames < voice->duration_frames ) {
return voice_gate_level(voice, voice->elapsed_frames);
}
released = voice->elapsed_frames - voice->duration_frames;
if ( released >= voice->release_frames ) {
return 0.0f;
}
gatelevel = voice_gate_level(voice, voice->duration_frames);
return gatelevel * (1.0f - ((float32_t)released / (float32_t)voice->release_frames));
}
/**
* @brief Pitch @p voice sounds at where its sweep currently stands.
*
* Recomputed from the frame counter each time rather than added to as it goes,
* so a sweep lands on exactly the frequencies its step size describes however
* the caller happens to have chopped up its calls to akgl_audio_mix().
*
* A voice that is not sweeping keeps the frequency it was given.
*/
static float32_t voice_sweep_hz(akgl_AudioVoice *voice)
{
uint32_t ticks = 0;
float32_t moved = 0.0f;
float32_t hz = 0.0f;
if ( voice->sweep_step_hz <= 0.0f ) {
return voice->hz;
}
ticks = voice->elapsed_frames / AKGL_AUDIO_SWEEP_TICK_FRAMES;
moved = voice->sweep_step_hz * (float32_t)ticks;
if ( voice->sweep_to_hz < voice->sweep_from_hz ) {
hz = voice->sweep_from_hz - moved;
if ( hz < voice->sweep_to_hz ) {
hz = voice->sweep_to_hz;
}
} else {
hz = voice->sweep_from_hz + moved;
if ( hz > voice->sweep_to_hz ) {
hz = voice->sweep_to_hz;
}
}
return hz;
}
/**
* @brief Start a note on @p voice, sweeping or held.
*
* The whole of what akgl_audio_tone() and akgl_audio_sweep() do once their
* arguments have been checked. A held note is a sweep with a step of 0, which
* is also what makes a voice reused for a plain tone stop sweeping.
*/
static void start_note(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
ensure_voices();
lock_voices();
akgl_audio_voices[voice].hz = from_hz;
akgl_audio_voices[voice].phase = 0.0f;
akgl_audio_voices[voice].sweep_from_hz = from_hz;
akgl_audio_voices[voice].sweep_to_hz = to_hz;
akgl_audio_voices[voice].sweep_step_hz = step_hz;
akgl_audio_voices[voice].duration_frames = frames_for_ms(ms);
akgl_audio_voices[voice].elapsed_frames = 0;
akgl_audio_voices[voice].active = true;
unlock_voices();
}
/** @brief Refuse a voice index that is not in the table. */
static akerr_ErrorContext *check_voice(int voice)
{
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(
errctx,
((voice < 0) || (voice >= AKGL_AUDIO_MAX_VOICES)),
AKERR_OUTOFBOUNDS,
"Voice %d is not in the range 0 to %d",
voice,
AKGL_AUDIO_MAX_VOICES - 1);
SUCCEED_RETURN(errctx);
}
/**
* @brief Fill SDL's request from the voice table.
*
* SDL holds the stream lock for the duration of this callback, which is the
* same lock lock_voices() takes, so the voice table cannot change underneath a
* mix in progress.
*/
static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
akerr_ErrorContext *errctx = NULL;
int frames = 0;
while ( additional_amount > 0 ) {
frames = additional_amount / (int)sizeof(float32_t);
if ( frames > AKGL_AUDIO_MIX_FRAMES ) {
frames = AKGL_AUDIO_MIX_FRAMES;
}
if ( frames <= 0 ) {
return;
}
errctx = akgl_audio_mix(mixbuffer, frames);
if ( errctx != NULL ) {
// There is nobody to return an error to on the audio thread, and
// refusing to write leaves SDL underrunning. Report and go quiet.
LOG_ERROR_WITH_MESSAGE(errctx, "** AUDIO CALLBACK **");
errctx->handled = true;
errctx = akerr_release_error(errctx);
return;
}
SDL_PutAudioStreamData(stream, mixbuffer, frames * (int)sizeof(float32_t));
additional_amount -= frames * (int)sizeof(float32_t);
}
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
{
SDL_AudioSpec spec;
PREPARE_ERROR(errctx);
if ( audiostream != NULL ) {
SUCCEED_RETURN(errctx);
}
ensure_voices();
spec.format = SDL_AUDIO_F32;
spec.channels = 1;
spec.freq = AKGL_AUDIO_SAMPLE_RATE;
audiostream = SDL_OpenAudioDeviceStream(
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&spec,
&audio_stream_callback,
NULL);
FAIL_ZERO_RETURN(
errctx,
audiostream,
AKGL_ERR_SDL,
"Couldn't open an audio device: %s",
SDL_GetError());
// Devices open paused so a caller can set a stream up before it is heard.
// Nothing here needs that, and a caller who expected akgl_audio_tone() to
// make a sound would otherwise get silence with no error to explain it.
FAIL_ZERO_RETURN(
errctx,
SDL_ResumeAudioStreamDevice(audiostream),
AKGL_ERR_SDL,
"Couldn't start the audio device: %s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
{
SDL_AudioStream *closing = audiostream;
PREPARE_ERROR(errctx);
// Clear the pointer before destroying the stream: lock_voices() checks it,
// and SDL_DestroyAudioStream can run the callback one last time.
audiostream = NULL;
if ( closing != NULL ) {
SDL_DestroyAudioStream(closing);
}
reset_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(errctx, (hz <= 0.0f), AKERR_OUTOFBOUNDS, "Frequency %f is not positive", hz);
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
// A step of 0 is what says "one pitch, held", so this also clears a sweep
// left on the voice by an earlier akgl_audio_sweep().
start_note(voice, hz, hz, 0.0f, ms);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(errctx, (from_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Start frequency %f is not positive", from_hz);
FAIL_NONZERO_RETURN(errctx, (to_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Target frequency %f is not positive", to_hz);
FAIL_NONZERO_RETURN(
errctx,
(step_hz <= 0.0f),
AKERR_OUTOFBOUNDS,
"Step %f is not positive; the direction of a sweep comes from the two frequencies",
step_hz);
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
start_note(voice, from_hz, to_hz, step_hz, ms);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
ensure_voices();
lock_voices();
akgl_audio_voices[voice].active = false;
akgl_audio_voices[voice].elapsed_frames = 0;
akgl_audio_voices[voice].phase = 0.0f;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(
errctx,
((waveform < AKGL_AUDIO_WAVE_TRIANGLE) || (waveform > AKGL_AUDIO_WAVE_SINE)),
AKERR_OUTOFBOUNDS,
"Waveform %d is not one of the %d shapes",
(int)waveform,
(int)AKGL_AUDIO_WAVE_SINE + 1);
ensure_voices();
lock_voices();
akgl_audio_voices[voice].waveform = waveform;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(
errctx,
((sustain < 0.0f) || (sustain > 1.0f)),
AKERR_OUTOFBOUNDS,
"Sustain level %f is not between 0.0 and 1.0",
sustain);
ensure_voices();
lock_voices();
akgl_audio_voices[voice].attack_frames = frames_for_ms(attack);
akgl_audio_voices[voice].decay_frames = frames_for_ms(decay);
akgl_audio_voices[voice].release_frames = frames_for_ms(release);
akgl_audio_voices[voice].sustain = sustain;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
{
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(
errctx,
((level < 0.0f) || (level > 1.0f)),
AKERR_OUTOFBOUNDS,
"Volume level %f is not between 0.0 and 1.0",
level);
ensure_voices();
lock_voices();
mastervolume = level;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_ZERO_RETURN(errctx, active, AKERR_NULLPOINTER, "NULL activity destination");
ensure_voices();
lock_voices();
*active = akgl_audio_voices[voice].active;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames)
{
akgl_AudioVoice *voice = NULL;
float32_t sum = 0.0f;
bool sweeping = false;
int i = 0;
int v = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL sample destination");
FAIL_NONZERO_RETURN(errctx, (frames < 0), AKERR_OUTOFBOUNDS, "Frame count %d is negative", frames);
for ( i = 0; i < frames; i++ ) {
sum = 0.0f;
for ( v = 0; v < AKGL_AUDIO_MAX_VOICES; v++ ) {
voice = &akgl_audio_voices[v];
if ( voice->active == false ) {
continue;
}
if ( voice->elapsed_frames >= (voice->duration_frames + voice->release_frames) ) {
// Gate and release are both spent. The voice goes quiet on its
// own so a caller does not have to keep a clock to stop it.
voice->active = false;
voice->elapsed_frames = 0;
voice->phase = 0.0f;
continue;
}
sweeping = (voice->sweep_step_hz > 0.0f);
if ( sweeping ) {
voice->hz = voice_sweep_hz(voice);
} else {
// Derived from the frame counter rather than accumulated one
// increment at a time. A float increment of hz/rate is not exact,
// and adding it 44100 times a second walks the pitch off over the
// length of a held note.
voice->phase = (float32_t)SDL_fmod(
((double)voice->elapsed_frames * (double)voice->hz) / (double)AKGL_AUDIO_SAMPLE_RATE,
1.0);
}
sum += voice_oscillator(voice) * voice_envelope(voice);
if ( sweeping ) {
// The derived form above assumes one frequency for the whole note.
// Under a sweep it would jump the waveform at every step -- an
// audible click -- so a swept voice accumulates instead and takes
// the drift the derived form exists to avoid.
voice->phase = (float32_t)SDL_fmod(
(double)voice->phase + ((double)voice->hz / (double)AKGL_AUDIO_SAMPLE_RATE),
1.0);
}
voice->elapsed_frames += 1;
}
sum = sum * mastervolume;
// Three voices at full level can sum past full scale. Clamping rather
// than scaling by the voice count keeps a single voice at the level it
// was asked for instead of a third of it.
if ( sum > 1.0f ) {
sum = 1.0f;
} else if ( sum < -1.0f ) {
sum = -1.0f;
}
dest[i] = sum;
}
SUCCEED_RETURN(errctx);
}