/** * @file audio.c * @brief Implements the audio subsystem. */ #include #include #include #include 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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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); }