Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
/**
|
|
|
|
|
* @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,
|
Finish the language: every remaining verb group, and the defects that blocked them
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>
2026-07-31 21:50:37 -04:00
|
|
|
&MOCK_AUDIO, &MOCK_INPUT, NULL));
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
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);
|
|
|
|
|
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
/*
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(16, true, &ms), AKBASIC_ERR_BOUNDS);
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
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);
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 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();
|
|
|
|
|
|
|
|
|
|
/*
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
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>
2026-07-31 13:25:14 -04:00
|
|
|
* 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.
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
*/
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
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>
2026-07-31 13:25:14 -04:00
|
|
|
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);
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
harness_stop();
|
|
|
|
|
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
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>
2026-07-31 13:25:14 -04:00
|
|
|
/* A sweep direction of zero is "no sweep" and must still play a held note. */
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
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();
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
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>
2026-07-31 13:25:14 -04:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 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,
|
Finish the language: every remaining verb group, and the defects that blocked them
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>
2026-07-31 21:50:37 -04:00
|
|
|
&MOCK_AUDIO, &MOCK_INPUT, NULL));
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
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>
2026-07-31 13:25:14 -04:00
|
|
|
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();
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @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();
|
Finish the language: every remaining verb group, and the defects that blocked them
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL, NULL));
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
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();
|
Finish the language: every remaining verb group, and the defects that blocked them
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>
2026-07-31 21:50:37 -04:00
|
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL, NULL));
|
Sound: implement the BASIC 7.0 sound verbs and the PLAY parser
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>
2026-07-31 08:29:04 -04:00
|
|
|
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;
|
|
|
|
|
}
|