0.3.0 closed all ten API gaps this port had filed. The four workarounds go with them: the CMake block that declared libakgl's vendored dependencies by hand, the akgl/actor.h include in three files, and the six vtable pointers assigned by hand in two more, now akgl_render_bind2d(). Two gaps were capabilities rather than inconveniences, and both are now real: The line editor takes the composed UTF-8 text the ring carries in preference to the keycode, so shifted characters, keyboard layouts, compose keys and dead keys all work. A double quote can be typed, which means a BASIC string literal can be typed -- the sharp end of the old limitation. Letters are no longer folded to upper case. SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3 sweeps once rather than oscillating and TODO.md section 5 says so. A backend with no sweep still refuses the swept note and plays the held one. The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by temporarily demanding 0.4.0 and watching it fire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
435 lines
18 KiB
C
435 lines
18 KiB
C
/**
|
|
* @file audio_verbs.c
|
|
* @brief Tests the group I verbs and the PLAY queue against the mock backend.
|
|
*
|
|
* Two things here need saying about method. The conversion tables are asserted
|
|
* against numbers derived independently -- the SID clock formula, equal
|
|
* temperament from A440 -- rather than against whatever the code happens to
|
|
* produce, because a table's whole failure mode is being plausibly wrong.
|
|
*
|
|
* And the PLAY queue is driven by hand: akbasic_runtime_settime() then
|
|
* akbasic_runtime_step(), so the test owns the clock the same way a host does.
|
|
* That is the only way to assert that a note is held for its written length
|
|
* without the test sleeping through it.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include <akbasic/audio.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "harness.h"
|
|
#include "mockdevice.h"
|
|
#include "testutil.h"
|
|
|
|
/** @brief Bring up a runtime with the mock devices attached and a program loaded. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
mock_devices_init();
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
|
&MOCK_AUDIO, &MOCK_INPUT));
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The conversion tables, against numbers worked out independently.
|
|
*/
|
|
static void test_tables(void)
|
|
{
|
|
double hz = 0.0;
|
|
int ms = 0;
|
|
|
|
/*
|
|
* The SID computes Fout = Fn * Fclk / 2^24. At Fn = 16777 and an NTSC clock
|
|
* of 1022730 that is 1022.7 Hz, which is the arithmetic and not a reading of
|
|
* the implementation.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_audio_register_to_hz(16777, &hz));
|
|
TEST_REQUIRE(hz > 1022.0 && hz < 1023.5, "register 16777 should be about 1022 Hz, got %f", hz);
|
|
TEST_REQUIRE_OK(akbasic_audio_register_to_hz(0, &hz));
|
|
TEST_REQUIRE_FEQ(hz, 0.0);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_register_to_hz(65536, &hz), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_register_to_hz(-1, &hz), AKBASIC_ERR_BOUNDS);
|
|
|
|
/*
|
|
* Every entry of both ADSR tables, not just the ends.
|
|
*
|
|
* Mutation testing is why: checking only rate 0 and rate 15 left the twelve
|
|
* entries between them free to be any number at all, and the failure mode of
|
|
* this file is a constant that is plausibly wrong rather than obviously
|
|
* wrong. A corrupted middle entry makes one envelope decay at the wrong
|
|
* speed, which nothing else in the suite would notice and no listener could
|
|
* attribute.
|
|
*
|
|
* The values are the 6581/8580 datasheet's, transcribed here independently
|
|
* of src/audio_tables.c rather than read back from it.
|
|
*/
|
|
{
|
|
static const int attack[16] = {
|
|
2, 8, 16, 24, 38, 56, 68, 80,
|
|
100, 250, 500, 800, 1000, 3000, 5000, 8000
|
|
};
|
|
int rate = 0;
|
|
|
|
for ( rate = 0; rate < 16; rate++ ) {
|
|
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(rate, true, &ms));
|
|
TEST_REQUIRE_INT(ms, attack[rate]);
|
|
/*
|
|
* Decay and release are exactly three times attack. That is a
|
|
* property of the chip's envelope generator rather than a
|
|
* coincidence, so it is asserted as the relationship it is.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(rate, false, &ms));
|
|
TEST_REQUIRE_INT(ms, attack[rate] * 3);
|
|
}
|
|
}
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(16, true, &ms), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(-1, false, &ms), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(0, true, NULL), AKERR_NULLPOINTER);
|
|
|
|
/*
|
|
* Equal temperament, checked at the three points anybody would know: A4 is
|
|
* 440, the A an octave up is exactly double, and middle C is 261.63.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_audio_note_hz(9, 4, &hz));
|
|
TEST_REQUIRE_FEQ(hz, 440.0);
|
|
TEST_REQUIRE_OK(akbasic_audio_note_hz(9, 5, &hz));
|
|
TEST_REQUIRE_FEQ(hz, 880.0);
|
|
TEST_REQUIRE_OK(akbasic_audio_note_hz(0, 4, &hz));
|
|
TEST_REQUIRE(hz > 261.5 && hz < 261.7, "C4 should be about 261.63 Hz, got %f", hz);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_note_hz(12, 4, &hz), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_note_hz(0, 7, &hz), AKBASIC_ERR_BOUNDS);
|
|
|
|
/* TEMPO 8 should put a quarter note at 500 ms, which is 120 bpm. */
|
|
TEST_REQUIRE_OK(akbasic_audio_whole_note_ms(AKBASIC_TEMPO_DEFAULT, &ms));
|
|
TEST_REQUIRE_INT(ms / 4, 500);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_whole_note_ms(0, &ms), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_whole_note_ms(256, &ms), AKBASIC_ERR_BOUNDS);
|
|
}
|
|
|
|
/** @brief SOUND converts its register and its jiffies, and checks its voice. */
|
|
static void test_sound(void)
|
|
{
|
|
/* 60 jiffies is one second. */
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 16777, 60\n"));
|
|
TEST_REQUIRE(strstr(MOCK.log, "1000ms") != NULL,
|
|
"60 jiffies should be 1000 ms, got \"%s\"", MOCK.log);
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 ") != NULL,
|
|
"BASIC voice 1 should reach backend voice 0, got \"%s\"", MOCK.log);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 4, 1000, 10\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SOUND voice 4") != NULL,
|
|
"voice 4 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* The frequency sweep reaches the device, which it could not until libakgl
|
|
* 0.3.0 grew akgl_audio_sweep -- it was refused outright before, because
|
|
* faking it would have meant re-issuing tones from step() and tying audible
|
|
* pitch to how often the host calls us.
|
|
*
|
|
* Sweeping *down*: register 1000 is below register 16777, so the endpoints
|
|
* decide the direction and both the SID and akgl_audio_sweep read them that
|
|
* way.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 16777, 60, 2, 1000, 10\n"));
|
|
TEST_REQUIRE(strstr(MOCK.log, "sweep v0 ") != NULL,
|
|
"a sweep should reach the device, got \"%s\"", MOCK.log);
|
|
TEST_REQUIRE(strstr(MOCK.log, "1000ms") != NULL,
|
|
"60 jiffies should still be 1000 ms on a sweep, got \"%s\"", MOCK.log);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "") != NULL && HARNESS_OUTPUT[0] == '\0',
|
|
"a sweep should not be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* A direction outside 0..3 is a typo worth catching. */
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 9, 500, 10\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SOUND direction 9") != NULL,
|
|
"direction 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* A sweep needs all three of its arguments. */
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 1\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "direction, a minimum and a step") != NULL,
|
|
"a partial sweep should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* A sweep direction of zero is "no sweep" and must still play a held note. */
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 0\n"));
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 ") != NULL,
|
|
"direction 0 is not a sweep and should play, got \"%s\"", MOCK.log);
|
|
harness_stop();
|
|
|
|
/*
|
|
* A backend with no sweep is still a valid backend -- that is what a host
|
|
* written against libakgl 0.2.0 looks like -- and SOUND refuses the swept
|
|
* note rather than dereferencing a NULL entry point.
|
|
*/
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
MOCK_AUDIO.sweep = NULL;
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
|
&MOCK_AUDIO, &MOCK_INPUT));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SOUND 1, 1000, 60, 1, 500, 10\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "audio device that can sweep") != NULL,
|
|
"a backend without sweep should refuse, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief ENVELOPE maps the SID rate numbers, and sustain is a level not a time. */
|
|
static void test_envelope(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 3, 0, 0, 15, 0, 1\n"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[3].attack, 2);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[3].decay, 6);
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.envelopes[3].sustain, 1.0);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[3].waveform, AKBASIC_WAVE_SAWTOOTH);
|
|
harness_stop();
|
|
|
|
/* Sustain 0 is silence, and is a legal thing to ask for. */
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 0, 0, 0, 0\n"));
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.envelopes[0].sustain, 0.0);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 10\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "ENVELOPE 10") != NULL,
|
|
"preset 10 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 0, 16\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "ENVELOPE rate 16") != NULL,
|
|
"rate 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* Defining an instrument needs no device; a host may not have lent one yet. */
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 ENVELOPE 1, 5\n20 VOL 8\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[1].attack, 56);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief VOL and TEMPO record their setting and range-check it. */
|
|
static void test_vol_and_tempo(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 VOL 15\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "vol 1.00\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 VOL 0\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "vol 0.00\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 VOL 16\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "VOL 16") != NULL,
|
|
"VOL 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* A tempo change rescales the *current* note length rather than resetting it
|
|
* to a quarter note: a program that set eighth notes and then doubled the
|
|
* tempo means faster eighth notes.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 TEMPO 16\n"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.tempo, 16);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.notems, 250);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 TEMPO 0\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "TEMPO 0") != NULL,
|
|
"TEMPO 0 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Bring up a runtime and parse a PLAY string without draining the queue.
|
|
*
|
|
* akbasic_play_parse() is called directly rather than through a program, because
|
|
* running one would also run akbasic_play_service() -- and with the clock left at
|
|
* zero every note's duration has already expired, so the queue would be empty
|
|
* again before the assertions could look at it. That draining is correct
|
|
* behaviour, tested on its own in test_play_is_not_blocking(); here the question
|
|
* is only what a string parses *to*.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *parse_notes(const char *notes)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
mock_devices_init();
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL));
|
|
PASS(errctx, akbasic_play_parse(&HARNESS_RUNTIME, notes));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief The PLAY string parser: notes, octaves, accidentals, rests and durations. */
|
|
static void test_play_parse(void)
|
|
{
|
|
TEST_REQUIRE_OK(parse_notes("O4C"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 1);
|
|
TEST_REQUIRE(HARNESS_RUNTIME.audio_state.queue[0].hz > 261.5 &&
|
|
HARNESS_RUNTIME.audio_state.queue[0].hz < 261.7,
|
|
"O4C should be middle C, got %f", HARNESS_RUNTIME.audio_state.queue[0].hz);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[0].ms, 500);
|
|
harness_stop();
|
|
|
|
/* A sharp raises by a semitone; C# and D$ are the same pitch. */
|
|
TEST_REQUIRE_OK(parse_notes("O4#CO4$D"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 2);
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.queue[0].hz,
|
|
HARNESS_RUNTIME.audio_state.queue[1].hz);
|
|
harness_stop();
|
|
|
|
/*
|
|
* B# belongs in the octave above and C$ in the one below. Wrapping the
|
|
* semitone without moving the octave would put B# an octave too low, which
|
|
* is the classic way to get this wrong.
|
|
*/
|
|
TEST_REQUIRE_OK(parse_notes("O4#BO5C"));
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.queue[0].hz,
|
|
HARNESS_RUNTIME.audio_state.queue[1].hz);
|
|
harness_stop();
|
|
|
|
/* Durations persist until changed, and a dot adds half again. */
|
|
TEST_REQUIRE_OK(parse_notes("HCD.E"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 3);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[0].ms, 1000);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[1].ms, 1000);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[2].ms, 1500);
|
|
harness_stop();
|
|
|
|
/* A rest occupies its time and makes no sound. */
|
|
TEST_REQUIRE_OK(parse_notes("CRC"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 3);
|
|
TEST_REQUIRE(HARNESS_RUNTIME.audio_state.queue[1].rest, "R should queue a rest");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[1].ms, 500);
|
|
harness_stop();
|
|
|
|
/* V selects the voice for everything after it. */
|
|
TEST_REQUIRE_OK(parse_notes("CV2D"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[0].voice, 0);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[1].voice, 1);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PLAY \"Z\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "does not understand") != NULL,
|
|
"an unknown PLAY letter should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PLAY \"V9C\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PLAY V9") != NULL,
|
|
"voice 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* X selects the filter, which is the one thing here with nothing behind it. */
|
|
TEST_REQUIRE_OK(run_program("10 PLAY \"X1C\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "filter") != NULL,
|
|
"PLAY X should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief PLAY does not block, and the queue drains against the host's clock.
|
|
*
|
|
* This is the assertion that matters most in the file. A C128 holds the program
|
|
* inside PLAY until the last note ends; here PLAY returns immediately and the
|
|
* notes come out of step(). The test owns the clock, so it can assert a note was
|
|
* held for its full length without waiting through it.
|
|
*/
|
|
static void test_play_is_not_blocking(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PLAY \"QCD\"\n20 PRINT \"AFTER\"\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
|
|
/*
|
|
* The program ran to completion -- PRINT on line 20 happened -- with a
|
|
* second of music still outstanding. That is the whole point: on a C128 the
|
|
* program would have sat inside PLAY for that second.
|
|
*
|
|
* One note has already gone out. The clock was never set, so it still reads
|
|
* zero, and the step that followed PLAY found the head note's start time
|
|
* (also zero) already reached. Both notes are still counted; only the head
|
|
* has moved.
|
|
*/
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 2);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.head, 1);
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 261") != NULL,
|
|
"the first note should have gone out during the run, got \"%s\"", MOCK.log);
|
|
mock_log_reset();
|
|
|
|
/* Halfway through it, nothing new comes out. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 250));
|
|
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
|
|
TEST_REQUIRE_STR(MOCK.log, "");
|
|
|
|
/* At its end, the second note starts. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 500));
|
|
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 293") != NULL,
|
|
"the second note should have been released, got \"%s\"", MOCK.log);
|
|
|
|
/* Once drained, the queue resets so the next PLAY starts from the front. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
|
|
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 0);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief FILTER is refused rather than ignored, and says why. */
|
|
static void test_filter(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 FILTER 1000, 1, 0, 0, 5\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "filter stage") != NULL,
|
|
"FILTER should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief The verbs that need a device name themselves when there is none. */
|
|
static void test_no_device(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SOUND 1, 1000, 10\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SOUND needs an audio device") != NULL,
|
|
"SOUND without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PLAY \"C\"\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PLAY needs an audio device") != NULL,
|
|
"PLAY without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_tables();
|
|
test_sound();
|
|
test_envelope();
|
|
test_vol_and_tempo();
|
|
test_play_parse();
|
|
test_play_is_not_blocking();
|
|
test_filter();
|
|
test_no_device();
|
|
return akbasic_test_failures;
|
|
}
|