Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
519 lines
16 KiB
C
519 lines
16 KiB
C
/**
|
|
* @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 *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);
|
|
}
|