A mutation smoke run scored src/audio.c at 50% and src/draw.c at 70% and named three real holes: - Nothing asserted that an unconfigured voice is audible, which is the entire reason the voice table defaults to a square wave at full level instead of a zeroed struct. Deleting the defaults survived. - No envelope test used a non-zero attack and decay together, so the decay measuring from the start of the note rather than from the end of the attack survived. - The circle was only checked at its four axis points, which a mis-signed octant reflection survives. It now checks that every plotted pixel has a mirror in the other three quadrants. Also adds a backend that exists but was never given an SDL_Renderer, which is the state a host is in between allocating one and initializing it; every draw entry point has to report it rather than dereference it. audio 50% -> 75%, draw 70% -> 90%. The survivors are recorded in TODO.md and are honestly untestable from here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
492 lines
18 KiB
C
492 lines
18 KiB
C
/**
|
|
* @file audio.c
|
|
* @brief Unit tests for the tone generator.
|
|
*
|
|
* Almost everything here drives akgl_audio_mix() by hand rather than opening a
|
|
* device. That is not a workaround: a device pulls samples on SDL's audio
|
|
* thread at whatever rate it likes, so a test that opened one and then asserted
|
|
* on voice state would be racing the callback. Pulling the samples ourselves
|
|
* makes the synthesis deterministic -- the same input produces the same
|
|
* waveform, sample for sample, every run.
|
|
*
|
|
* The device is opened once, at the end, to prove akgl_audio_init() and
|
|
* akgl_audio_shutdown() work against the dummy driver.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/audio.h>
|
|
#include <akgl/error.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/**
|
|
* @brief Frames of output most tests generate at a time.
|
|
*
|
|
* Enough to hold a 10 ms attack and a 10 ms decay back to back at 44100 frames
|
|
* per second, which is the longest single stretch any test here inspects.
|
|
*/
|
|
#define TEST_MIX_FRAMES 1024
|
|
|
|
/** @brief 441 Hz at 44100 frames per second is exactly 100 frames per cycle. */
|
|
#define TEST_TONE_HZ 441.0f
|
|
/** @brief Frames in one cycle of TEST_TONE_HZ. */
|
|
#define TEST_TONE_PERIOD 100
|
|
|
|
/** @brief Somewhere to mix into. */
|
|
static float32_t samples[TEST_MIX_FRAMES];
|
|
|
|
/** @brief Silence every voice and put the master level back. */
|
|
static akerr_ErrorContext *reset_audio(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
|
CATCH(errctx, akgl_audio_stop(i));
|
|
CATCH(errctx, akgl_audio_waveform(i, AKGL_AUDIO_WAVE_SQUARE));
|
|
CATCH(errctx, akgl_audio_envelope(i, 0, 0, 1.0f, 0));
|
|
}
|
|
CATCH(errctx, akgl_audio_volume(1.0f));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Report the largest absolute sample in the first @p frames. */
|
|
static float32_t peak_of(int frames)
|
|
{
|
|
float32_t peak = 0.0f;
|
|
int i = 0;
|
|
|
|
for ( i = 0; i < frames; i++ ) {
|
|
if ( fabsf(samples[i]) > peak ) {
|
|
peak = fabsf(samples[i]);
|
|
}
|
|
}
|
|
return peak;
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_defaults(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
// Deliberately the first thing this file does, and deliberately without
|
|
// a call to reset_audio(): a voice nobody has configured has to make a
|
|
// sound. A zeroed voice would have a sustain of 0.0 and be silent, with
|
|
// no error to say why, which is exactly the trap this defends against.
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
|
"an unconfigured voice mixed to %f, expected a full-level square wave",
|
|
samples[0]);
|
|
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
|
"an unconfigured voice is not a square wave (%f half a cycle in)",
|
|
samples[50]);
|
|
} CLEANUP {
|
|
IGNORE(reset_audio());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_silence(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool silent = true;
|
|
bool active = true;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, reset_audio());
|
|
|
|
// With nothing sounding, the mixer produces silence rather than
|
|
// whatever was left in the buffer.
|
|
SDL_memset((void *)&samples, 0xff, sizeof(samples));
|
|
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing an idle voice table");
|
|
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
|
TEST_ASSERT_FLAG(silent, samples[i] == 0.0f);
|
|
}
|
|
TEST_ASSERT(errctx, silent == true, "an idle voice table did not mix to silence");
|
|
|
|
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
|
CATCH(errctx, akgl_audio_voice_active(i, &active));
|
|
TEST_ASSERT(errctx, active == false, "voice %d reports active with nothing playing", i);
|
|
}
|
|
|
|
// Zero frames is a legal request that writes nothing.
|
|
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, 0), "mixing zero frames");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_mix(NULL, 16),
|
|
"mixing into a NULL destination");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_mix(samples, -1),
|
|
"mixing a negative number of frames");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_square_tone(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool firsthalf = true;
|
|
bool secondhalf = true;
|
|
bool active = false;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, reset_audio());
|
|
|
|
// A square wave with a flat envelope is the one waveform whose every
|
|
// sample is known exactly: +1 for the first half of each cycle, -1 for
|
|
// the second.
|
|
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000), "sounding a tone");
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == true, "a sounded voice does not report itself active");
|
|
|
|
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing a square wave");
|
|
for ( i = 0; i < (TEST_TONE_PERIOD / 2); i++ ) {
|
|
TEST_ASSERT_FLAG(firsthalf, samples[i] == 1.0f);
|
|
}
|
|
for ( i = (TEST_TONE_PERIOD / 2); i < TEST_TONE_PERIOD; i++ ) {
|
|
TEST_ASSERT_FLAG(secondhalf, samples[i] == -1.0f);
|
|
}
|
|
TEST_ASSERT(errctx, firsthalf == true,
|
|
"the first half cycle of a square wave is not at full positive level");
|
|
TEST_ASSERT(errctx, secondhalf == true,
|
|
"the second half cycle of a square wave is not at full negative level");
|
|
|
|
// The wave repeats: the second cycle matches the first.
|
|
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD] == samples[0],
|
|
"the wave did not repeat after one period");
|
|
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD + 60] == samples[60],
|
|
"the wave did not repeat after one period");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, 0.0f, 100),
|
|
"sounding a tone at zero hertz");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, -100.0f, 100),
|
|
"sounding a tone at a negative frequency");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, TEST_TONE_HZ, 0),
|
|
"sounding a tone with no duration");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(-1, TEST_TONE_HZ, 100),
|
|
"sounding a tone on a negative voice");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_audio_tone(AKGL_AUDIO_MAX_VOICES, TEST_TONE_HZ, 100),
|
|
"sounding a tone on a voice past the last one");
|
|
} CLEANUP {
|
|
IGNORE(reset_audio());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_waveforms(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool inrange = true;
|
|
int i = 0;
|
|
int w = 0;
|
|
akgl_AudioWaveform shapes[4] = {
|
|
AKGL_AUDIO_WAVE_TRIANGLE,
|
|
AKGL_AUDIO_WAVE_SAWTOOTH,
|
|
AKGL_AUDIO_WAVE_NOISE,
|
|
AKGL_AUDIO_WAVE_SINE,
|
|
};
|
|
|
|
ATTEMPT {
|
|
// Every shape has to stay inside full scale and actually move. The
|
|
// exact sample values differ per shape; these two properties do not.
|
|
for ( w = 0; w < 4; w++ ) {
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_waveform(0, shapes[w]));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
|
TEST_ASSERT_FLAG(inrange, (samples[i] >= -1.0f) && (samples[i] <= 1.0f));
|
|
}
|
|
TEST_ASSERT_FLAG(inrange, peak_of(TEST_MIX_FRAMES) > 0.5f);
|
|
}
|
|
TEST_ASSERT(errctx, inrange == true,
|
|
"a waveform either left full scale or produced nothing");
|
|
|
|
// The triangle is symmetric about the middle of its cycle, which the
|
|
// square and sawtooth are not -- enough to tell it was really selected.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_waveform(0, AKGL_AUDIO_WAVE_TRIANGLE));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[25], 0.0f,
|
|
"a triangle wave is at %f a quarter of the way up, expected 0",
|
|
samples[25]);
|
|
TEST_ASSERT_FEQ(errctx, samples[50], 1.0f,
|
|
"a triangle wave is at %f at its peak, expected 1", samples[50]);
|
|
TEST_ASSERT_FEQ(errctx, samples[75], 0.0f,
|
|
"a triangle wave is at %f three quarters through, expected 0",
|
|
samples[75]);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_audio_waveform(0, (akgl_AudioWaveform)(AKGL_AUDIO_WAVE_SINE + 1)),
|
|
"selecting a waveform past the last one");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_audio_waveform(AKGL_AUDIO_MAX_VOICES, AKGL_AUDIO_WAVE_SINE),
|
|
"selecting a waveform on a voice past the last one");
|
|
} CLEANUP {
|
|
IGNORE(reset_audio());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_envelope(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool rising = true;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, reset_audio());
|
|
|
|
// A 10 ms attack at 44100 is 441 frames, so the level should climb from
|
|
// nothing to full across the first 441 samples of a square wave and
|
|
// then hold at the sustain level.
|
|
CATCH(errctx, akgl_audio_envelope(0, 10, 0, 1.0f, 0));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
|
|
TEST_ASSERT_FEQ(errctx, samples[0], 0.0f,
|
|
"an attack does not start from silence (first sample %f)", samples[0]);
|
|
TEST_ASSERT(errctx, fabsf(samples[100]) > fabsf(samples[10]),
|
|
"the attack is not climbing (%f at frame 10, %f at frame 100)",
|
|
samples[10], samples[100]);
|
|
// Frames 0..49 are the positive half of the square, so their level is
|
|
// the envelope value directly.
|
|
for ( i = 1; i < 50; i++ ) {
|
|
TEST_ASSERT_FLAG(rising, samples[i] > samples[i - 1]);
|
|
}
|
|
TEST_ASSERT(errctx, rising == true, "the attack ramp is not monotonic");
|
|
|
|
// Half a millisecond of decay to a half sustain level, no attack: the
|
|
// very first sample is at full level and it settles at the sustain.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_envelope(0, 0, 10, 0.5f, 0));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
|
"with no attack the first sample is %f, expected full level", samples[0]);
|
|
TEST_ASSERT_FEQ(errctx, samples[450], -0.5f,
|
|
"after the decay the level is %f, expected the 0.5 sustain", samples[450]);
|
|
|
|
// Attack and decay together, which is the case where the decay has to
|
|
// measure from the end of the attack rather than from the start of the
|
|
// note. 10 ms of each is 441 frames of each at 44100.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_envelope(0, 10, 10, 0.5f, 0));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[441], 1.0f,
|
|
"at the end of the attack the level is %f, expected full", samples[441]);
|
|
TEST_ASSERT_FEQ(errctx, samples[661], -(1.0f - (0.5f * (220.0f / 441.0f))),
|
|
"halfway through the decay the level is %f, expected the halfway ramp",
|
|
samples[661]);
|
|
TEST_ASSERT_FEQ(errctx, samples[900], 0.5f,
|
|
"after the decay the level is %f, expected the 0.5 sustain", samples[900]);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, -0.1f, 1),
|
|
"setting a negative sustain level");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, 1.5f, 1),
|
|
"setting a sustain level past full");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_audio_envelope(AKGL_AUDIO_MAX_VOICES, 1, 1, 1.0f, 1),
|
|
"setting an envelope on a voice past the last one");
|
|
} CLEANUP {
|
|
IGNORE(reset_audio());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_duration_and_release(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool active = false;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, reset_audio());
|
|
|
|
// A 10 ms gate is 441 frames. The voice is still sounding while they
|
|
// are being generated and goes quiet on its own afterwards, without the
|
|
// caller having to stop it.
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
|
CATCH(errctx, akgl_audio_mix(samples, 440));
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == true, "the voice went quiet before its gate closed");
|
|
|
|
CATCH(errctx, akgl_audio_mix(samples, 8));
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == false, "the voice is still sounding past its gate");
|
|
CATCH(errctx, akgl_audio_mix(samples, 64));
|
|
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
|
"a finished voice is still producing sound (peak %f)", peak_of(64));
|
|
|
|
// With a release, the voice outlives its gate and fades rather than
|
|
// stopping dead.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 10));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
|
CATCH(errctx, akgl_audio_mix(samples, 441));
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == true, "a voice with a release stopped when its gate closed");
|
|
// The release starts from the level the gate left off at and falls from
|
|
// there, so the end of this window has to be quieter than its start.
|
|
CATCH(errctx, akgl_audio_mix(samples, 220));
|
|
TEST_ASSERT(errctx, fabsf(samples[219]) < fabsf(samples[0]),
|
|
"the release is not attenuating (%f at its start, %f 220 frames later)",
|
|
samples[0], samples[219]);
|
|
TEST_ASSERT(errctx, peak_of(220) > 0.0f, "the release went silent immediately");
|
|
CATCH(errctx, akgl_audio_mix(samples, 250));
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == false, "the voice outlived its gate and its release");
|
|
|
|
// Stopping cuts a voice off where it stands, release and all.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 1000));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, 64));
|
|
TEST_EXPECT_OK(errctx, akgl_audio_stop(0), "stopping a sounding voice");
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == false, "a stopped voice still reports itself active");
|
|
CATCH(errctx, akgl_audio_mix(samples, 64));
|
|
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
|
"a stopped voice is still producing sound (peak %f)", peak_of(64));
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_stop(AKGL_AUDIO_MAX_VOICES),
|
|
"stopping a voice past the last one");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_audio_voice_active(AKGL_AUDIO_MAX_VOICES, &active),
|
|
"asking about a voice past the last one");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_voice_active(0, NULL),
|
|
"asking about a voice with nowhere to put the answer");
|
|
} CLEANUP {
|
|
IGNORE(reset_audio());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_volume_and_mixing(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, reset_audio());
|
|
|
|
// The master level scales everything.
|
|
CATCH(errctx, akgl_audio_volume(0.25f));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[0], 0.25f,
|
|
"at a quarter volume the first sample is %f, expected 0.25", samples[0]);
|
|
|
|
CATCH(errctx, akgl_audio_volume(0.0f));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, peak_of(TEST_MIX_FRAMES), 0.0f,
|
|
"at zero volume the peak is %f, expected silence",
|
|
peak_of(TEST_MIX_FRAMES));
|
|
|
|
// Voices sum, and the sum is clamped rather than wrapped: three square
|
|
// waves in phase at full level would be 3.0 without the clamp.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_tone(2, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
|
"three voices in phase mixed to %f, expected a clamp at 1.0", samples[0]);
|
|
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
|
"three voices in phase mixed to %f, expected a clamp at -1.0", samples[50]);
|
|
|
|
// Two voices at different levels sum to their total rather than to
|
|
// either one of them, which is only visible below the clamp.
|
|
CATCH(errctx, reset_audio());
|
|
CATCH(errctx, akgl_audio_volume(0.5f));
|
|
CATCH(errctx, akgl_audio_envelope(1, 0, 0, 0.5f, 0));
|
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
|
TEST_ASSERT_FEQ(errctx, samples[0], 0.75f,
|
|
"a full voice and a half voice at half volume mixed to %f, expected 0.75",
|
|
samples[0]);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(-0.1f),
|
|
"setting a negative master volume");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(1.1f),
|
|
"setting a master volume past full");
|
|
} CLEANUP {
|
|
IGNORE(reset_audio());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_audio_device(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool active = true;
|
|
|
|
ATTEMPT {
|
|
// Everything above ran without a device. This is the one test that
|
|
// opens one, so nothing that asserts on voice state runs after it.
|
|
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device");
|
|
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device a second time");
|
|
|
|
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10), "sounding a tone on a device");
|
|
|
|
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing the audio device");
|
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
|
TEST_ASSERT(errctx, active == false, "shutting down left a voice sounding");
|
|
|
|
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing an audio device that is not open");
|
|
} CLEANUP {
|
|
IGNORE(akgl_audio_shutdown());
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_Init(SDL_INIT_AUDIO),
|
|
AKGL_ERR_SDL,
|
|
"Couldn't initialize SDL: %s",
|
|
SDL_GetError());
|
|
|
|
CATCH(errctx, test_audio_defaults());
|
|
CATCH(errctx, test_audio_silence());
|
|
CATCH(errctx, test_audio_square_tone());
|
|
CATCH(errctx, test_audio_waveforms());
|
|
CATCH(errctx, test_audio_envelope());
|
|
CATCH(errctx, test_audio_duration_and_release());
|
|
CATCH(errctx, test_audio_volume_and_mixing());
|
|
CATCH(errctx, test_audio_device());
|
|
} CLEANUP {
|
|
SDL_Quit();
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|