SOUND, ENVELOPE, VOL, PLAY and TEMPO against the akbasic_AudioBackend record, plus FILTER, which is in the table only so that it can be refused with a reason. The PLAY note-string parser and TEMPO are here rather than in libakgl because that is where its audio commit says they belong: a tone generator synthesises pitches, but deciding that O4CDEFG is five quarter notes starting at middle C is a language question. PLAY does not block. On a C128 it holds the program until the last note ends, which section 1.6 forbids outright, so it parses the string into a fixed queue and returns; akbasic_runtime_step() releases one note at a time against whatever time the host last passed to akbasic_runtime_settime(). The driver now steps one at a time with the clock refreshed in between rather than making a single unbounded run() call -- a tune whose notes all measured themselves against a frozen zero would rush out at once. That loop is its own function because CATCH expands to a break and PASS expands to a return of the context, and main() returns an int; wrapping the loop is what the protocol prescribes for that shape. src/audio_tables.c holds the three conversions, laid out as tables because each is somewhere a wrong constant produces a plausible wrong pitch rather than an error anybody would notice. Two are transcriptions -- the SID frequency formula and its non-linear ADSR rate tables, where decay is exactly three times attack. The third is not: BASIC 7.0 never published what a whole note lasts at a given TEMPO, so 16000 ms at TEMPO 1 is a calibration choice putting a default quarter note at 120 bpm, and it is labelled as a choice where it is made. SOUND's frequency sweep is refused rather than faked, and filed upstream as akgl_audio_sweep. The only way to fake it here is to re-issue tones from step(), which ties audible pitch to how often the host calls us -- a tune that changes key with the frame rate. FILTER is refused for the reason upstream already gave: there is no filter stage and SDL3 has no primitive to build one from. The PLAY parser is tested through akbasic_play_parse() directly rather than through a program, because running one also runs the queue service -- and with the clock at zero every duration has already expired, so the queue empties before an assertion can look at it. Draining is correct behaviour and is tested on its own; the parse tests ask a different question. 68/68 ctest, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
372 lines
16 KiB
C
372 lines
16 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);
|
|
|
|
/* 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;
|
|
}
|