/** * @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 #include #include #include #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); /* Attack and decay are different tables, and decay is three times attack. */ TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(0, true, &ms)); TEST_REQUIRE_INT(ms, 2); TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(0, false, &ms)); TEST_REQUIRE_INT(ms, 6); TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(15, true, &ms)); TEST_REQUIRE_INT(ms, 8000); TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(15, false, &ms)); TEST_REQUIRE_INT(ms, 24000); TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(16, true, &ms), AKBASIC_ERR_BOUNDS); /* * 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 is refused rather than faked. Faking it would mean * re-issuing tones from step(), which would tie audible pitch to how often * the host calls us -- a tune that changes key with the frame rate. */ TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 1, 500, 10\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "frequency sweep") != NULL, "a sweep should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* A sweep direction of zero is "no sweep" and must still play. */ 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(); } /** @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; }