/**
 * @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);
}

/** @brief Mix @p ticks whole sweep steps, one step per call. */
static akerr_ErrorContext *mix_sweep_ticks(int ticks)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    ATTEMPT {
	for ( i = 0; i < ticks; i++ ) {
	    CATCH(errctx, akgl_audio_mix(samples, AKGL_AUDIO_SWEEP_TICK_FRAMES));
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_audio_sweep(void)
{
    PREPARE_ERROR(errctx);
    bool active = false;

    ATTEMPT {
	CATCH(errctx, reset_audio());

	// 400 Hz climbing to 500 in 25 Hz steps: four steps, at 60 steps per
	// second. The pitch is advanced by the mixer's own frame counter, so
	// these assertions are about exact frames rather than about how often
	// the caller happened to ask for samples.
	TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 1000),
		       "sounding a rising sweep");
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
			"a sweep starts at %f, expected its start frequency",
			akgl_audio_voices[0].hz);

	// One tick's worth of frames is not yet a step: the last frame of that
	// window is still the first tick.
	CATCH(errctx, mix_sweep_ticks(1));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
			"the pitch moved at %f before the first step was due",
			akgl_audio_voices[0].hz);
	TEST_ASSERT(errctx, peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES) > 0.5f,
		    "a sweep is not making a sound (peak %f)",
		    peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES));

	// The frame after it is.
	CATCH(errctx, akgl_audio_mix(samples, 1));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 425.0f,
			"one step in, the pitch is %f, expected 425",
			akgl_audio_voices[0].hz);

	// It stops when it arrives rather than running past the target.
	CATCH(errctx, mix_sweep_ticks(8));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 500.0f,
			"a finished sweep sits at %f, expected its target 500",
			akgl_audio_voices[0].hz);

	// Downward is the same sweep with the frequencies the other way round.
	// The step stays positive; the direction is not its sign.
	CATCH(errctx, reset_audio());
	TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 500.0f, 400.0f, 25.0f, 1000),
		       "sounding a falling sweep");
	CATCH(errctx, mix_sweep_ticks(1));
	CATCH(errctx, akgl_audio_mix(samples, 1));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 475.0f,
			"one step down, the pitch is %f, expected 475",
			akgl_audio_voices[0].hz);
	CATCH(errctx, mix_sweep_ticks(8));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
			"a finished falling sweep sits at %f, expected its target 400",
			akgl_audio_voices[0].hz);

	// Equal frequencies are a held tone, not an error: a caller translating
	// a statement that computes its own limits does not have to special
	// case them.
	CATCH(errctx, reset_audio());
	TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 440.0f, 440.0f, 10.0f, 1000),
		       "sweeping between two identical frequencies");
	CATCH(errctx, mix_sweep_ticks(4));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 440.0f,
			"a sweep with nowhere to go moved to %f", akgl_audio_voices[0].hz);

	// The gate still governs: a sweep with further to go than the note lasts
	// ends with the note, part way up. 30 ms is 1323 frames, which is one
	// whole step and most of a second.
	CATCH(errctx, reset_audio());
	TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 4000.0f, 25.0f, 30),
		       "sounding a sweep longer than its gate");
	CATCH(errctx, mix_sweep_ticks(3));
	CATCH(errctx, akgl_audio_voice_active(0, &active));
	TEST_ASSERT(errctx, active == false, "a swept voice outlived its gate");
	TEST_ASSERT(errctx, akgl_audio_voices[0].hz > 400.0f,
		    "a sweep cut off by its gate never stepped at all (%f)",
		    akgl_audio_voices[0].hz);
	TEST_ASSERT(errctx, akgl_audio_voices[0].hz < 4000.0f,
		    "a sweep cut off by its gate reached %f anyway",
		    akgl_audio_voices[0].hz);

	// A voice reused for a plain tone stops sweeping.
	CATCH(errctx, reset_audio());
	CATCH(errctx, akgl_audio_sweep(0, 400.0f, 800.0f, 25.0f, 1000));
	CATCH(errctx, mix_sweep_ticks(2));
	TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000),
		       "sounding a plain tone on a voice that was sweeping");
	CATCH(errctx, mix_sweep_ticks(4));
	TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, TEST_TONE_HZ,
			"a plain tone on a swept voice drifted to %f",
			akgl_audio_voices[0].hz);

	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_audio_sweep(AKGL_AUDIO_MAX_VOICES, 400.0f, 500.0f, 25.0f, 100),
			   "sweeping a voice past the last one");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_audio_sweep(0, 0.0f, 500.0f, 25.0f, 100),
			   "sweeping from zero hertz");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_audio_sweep(0, 400.0f, -1.0f, 25.0f, 100),
			   "sweeping to a negative frequency");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_audio_sweep(0, 400.0f, 500.0f, 0.0f, 100),
			   "sweeping with no step");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_audio_sweep(0, 500.0f, 400.0f, -25.0f, 100),
			   "sweeping down with a negative step");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 0),
			   "sweeping with no duration");
    } 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_sweep());
	CATCH(errctx, test_audio_volume_and_mixing());
	CATCH(errctx, test_audio_device());
    } CLEANUP {
	SDL_Quit();
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
