Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. 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, NULL));
|
|
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, NULL));
|
|
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, 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, 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;
|
|
}
|