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>
This commit is contained in:
197
src/audio_tables.c
Normal file
197
src/audio_tables.c
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @file audio_tables.c
|
||||
* @brief The SOUND, ENVELOPE and PLAY conversions, laid out as the tables they are.
|
||||
*
|
||||
* BASIC 7.0's sound verbs speak in SID register values: SOUND takes a frequency
|
||||
* *register*, not a pitch, and ENVELOPE takes the SID's 0-15 rate numbers, whose
|
||||
* real durations are tabulated in silicon rather than computed. The backend takes
|
||||
* hertz and milliseconds. These are the conversions between the two, and they are
|
||||
* kept in one file because each is somewhere a wrong constant produces a
|
||||
* plausible wrong pitch rather than an error somebody would notice.
|
||||
*
|
||||
* Two of the three are transcriptions. The third -- TEMPO -- is a calibration
|
||||
* choice and is labelled as one where it is made.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/**
|
||||
* @brief The SID's attack rates, register value to milliseconds.
|
||||
*
|
||||
* Non-linear, and not a curve anybody would guess: the steps are small at the
|
||||
* bottom and enormous at the top. This is the 6581/8580 table.
|
||||
*/
|
||||
static const int ATTACK_MS[16] = {
|
||||
2, /* 0 */
|
||||
8, /* 1 */
|
||||
16, /* 2 */
|
||||
24, /* 3 */
|
||||
38, /* 4 */
|
||||
56, /* 5 */
|
||||
68, /* 6 */
|
||||
80, /* 7 */
|
||||
100, /* 8 */
|
||||
250, /* 9 */
|
||||
500, /* 10 */
|
||||
800, /* 11 */
|
||||
1000, /* 12 */
|
||||
3000, /* 13 */
|
||||
5000, /* 14 */
|
||||
8000 /* 15 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The SID's decay and release rates, register value to milliseconds.
|
||||
*
|
||||
* Exactly three times the attack table -- that is a property of the chip's
|
||||
* envelope generator, not a coincidence, so it is written out rather than
|
||||
* computed. A reader checking this against a datasheet should see the datasheet's
|
||||
* numbers.
|
||||
*/
|
||||
static const int DECAY_MS[16] = {
|
||||
6, /* 0 */
|
||||
24, /* 1 */
|
||||
48, /* 2 */
|
||||
72, /* 3 */
|
||||
114, /* 4 */
|
||||
168, /* 5 */
|
||||
204, /* 6 */
|
||||
240, /* 7 */
|
||||
300, /* 8 */
|
||||
750, /* 9 */
|
||||
1500, /* 10 */
|
||||
2400, /* 11 */
|
||||
3000, /* 12 */
|
||||
9000, /* 13 */
|
||||
15000, /* 14 */
|
||||
24000 /* 15 */
|
||||
};
|
||||
|
||||
/*
|
||||
* SOUND's frequency register to hertz.
|
||||
*
|
||||
* The SID computes Fout = (Fn * Fclk) / 2^24. Fclk is the system clock, which is
|
||||
* 1022730 Hz on an NTSC machine and 985248 on a PAL one -- so the same listing
|
||||
* plays very slightly sharp or flat depending on which side of the Atlantic it
|
||||
* was typed on. NTSC is used here; the difference is about 0.6%, well under a
|
||||
* semitone, and picking one is unavoidable.
|
||||
*/
|
||||
#define SID_CLOCK_HZ 1022730.0
|
||||
#define SID_ACCUMULATOR 16777216.0
|
||||
|
||||
/*
|
||||
* A whole note at TEMPO 1, in milliseconds.
|
||||
*
|
||||
* This one is a **calibration choice, not a transcription.** BASIC 7.0 documents
|
||||
* TEMPO as a relative duration from 1 to 255 with a default of 8 and does not
|
||||
* publish the constant. 16000 puts a default-tempo quarter note at 500 ms, which
|
||||
* is 120 beats per minute -- a moderate tempo, which is what a default should be.
|
||||
* If somebody turns up the real constant, this is the one line to change.
|
||||
*/
|
||||
#define WHOLE_NOTE_AT_TEMPO_1 16000
|
||||
|
||||
/** @brief Concert pitch, and the note the equal-tempered scale is derived from. */
|
||||
#define A4_HZ 440.0
|
||||
/** @brief Semitone offset of A within an octave that starts at C. */
|
||||
#define A_SEMITONE 9
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_register_to_hz(int value, double *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL destination in register_to_hz");
|
||||
FAIL_ZERO_RETURN(errctx, (value >= 0 && value <= 65535), AKBASIC_ERR_BOUNDS,
|
||||
"SOUND frequency %d out of range (0 to 65535)", value);
|
||||
*dest = ((double)value * SID_CLOCK_HZ) / SID_ACCUMULATOR;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_rate_to_ms(int rate, bool isattack, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL destination in rate_to_ms");
|
||||
FAIL_ZERO_RETURN(errctx, (rate >= 0 && rate <= 15), AKBASIC_ERR_BOUNDS,
|
||||
"ENVELOPE rate %d out of range (0 to 15)", rate);
|
||||
*dest = isattack ? ATTACK_MS[rate] : DECAY_MS[rate];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_note_hz(int semitone, int octave, double *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int steps = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL destination in note_hz");
|
||||
FAIL_ZERO_RETURN(errctx, (semitone >= 0 && semitone <= 11), AKBASIC_ERR_BOUNDS,
|
||||
"Note %d out of range (0 to 11)", semitone);
|
||||
FAIL_ZERO_RETURN(errctx, (octave >= 0 && octave <= 6), AKBASIC_ERR_BOUNDS,
|
||||
"PLAY octave %d out of range (0 to 6)", octave);
|
||||
|
||||
/*
|
||||
* Equal temperament from A4. Computed rather than tabulated because a table
|
||||
* of 84 frequencies is 84 chances to fat-finger one, and this is exact to
|
||||
* the precision anybody can hear.
|
||||
*/
|
||||
steps = ((octave - 4) * 12) + (semitone - A_SEMITONE);
|
||||
*dest = A4_HZ * pow(2.0, (double)steps / 12.0);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_whole_note_ms(int tempo, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL destination in whole_note_ms");
|
||||
FAIL_ZERO_RETURN(errctx, (tempo >= AKBASIC_TEMPO_MIN && tempo <= AKBASIC_TEMPO_MAX),
|
||||
AKBASIC_ERR_BOUNDS, "TEMPO %d out of range (%d to %d)",
|
||||
tempo, AKBASIC_TEMPO_MIN, AKBASIC_TEMPO_MAX);
|
||||
*dest = WHOLE_NOTE_AT_TEMPO_1 / tempo;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_state_init(akbasic_AudioState *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
int wholems = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL audio state in init");
|
||||
|
||||
obj->head = 0;
|
||||
obj->count = 0;
|
||||
obj->nextms = 0;
|
||||
obj->sounding = false;
|
||||
obj->tempo = AKBASIC_TEMPO_DEFAULT;
|
||||
obj->voice = 0;
|
||||
obj->octave = 4;
|
||||
obj->envelope = 0;
|
||||
obj->level = 1.0;
|
||||
|
||||
PASS(errctx, akbasic_audio_whole_note_ms(obj->tempo, &wholems));
|
||||
obj->notems = wholems / 4; /* PLAY starts on quarter notes */
|
||||
|
||||
/*
|
||||
* Every preset starts as a plain square wave held at full level. A zeroed
|
||||
* envelope would have a sustain of 0.0, which is silence with nothing to
|
||||
* explain it -- the same reasoning libakgl gives for its own voice defaults.
|
||||
*/
|
||||
for ( i = 0; i < AKBASIC_ENVELOPES; i++ ) {
|
||||
obj->envelopes[i].attack = 0;
|
||||
obj->envelopes[i].decay = 0;
|
||||
obj->envelopes[i].sustain = 1.0;
|
||||
obj->envelopes[i].release = 0;
|
||||
obj->envelopes[i].waveform = AKBASIC_WAVE_SQUARE;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
59
src/main.c
59
src/main.c
@@ -12,8 +12,12 @@
|
||||
* stack. An embedding game would place it in its own state for the same reason.
|
||||
*/
|
||||
|
||||
/* clock_gettime and CLOCK_MONOTONIC. C99 alone does not declare either. */
|
||||
#define _POSIX_C_SOURCE 199309L
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akstdlib.h>
|
||||
@@ -26,6 +30,58 @@ static akbasic_Runtime RUNTIME;
|
||||
static akbasic_TextSink SINK;
|
||||
static akbasic_StdioSink SINKSTATE;
|
||||
|
||||
/**
|
||||
* @brief Milliseconds off a monotonic clock, for akbasic_runtime_settime().
|
||||
*
|
||||
* The library reads no clock of its own -- it owns no loop and must not block --
|
||||
* so somebody has to tell it what time it is, and for a standalone program that
|
||||
* is this. A game does the same thing once a frame off whatever clock it already
|
||||
* keeps.
|
||||
*
|
||||
* Monotonic rather than wall-clock: the interpreter only ever compares these
|
||||
* values, and an NTP step backwards in the middle of a tune would hold a note
|
||||
* for however long the correction was.
|
||||
*/
|
||||
static int64_t monotonic_ms(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
if ( clock_gettime(CLOCK_MONOTONIC, &now) != 0 ) {
|
||||
/*
|
||||
* A clock that cannot be read leaves time frozen, which expires every
|
||||
* duration immediately: the program still runs and the music simply
|
||||
* rushes. Better than refusing to start over a note length.
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
return ((int64_t)now.tv_sec * 1000) + (now.tv_nsec / 1000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Step the interpreter to completion, refreshing the clock as it goes.
|
||||
*
|
||||
* One step at a time rather than a single unbounded run(), because the driver is
|
||||
* the thing that owns the clock: a PLAY string whose notes all measured
|
||||
* themselves against a frozen zero would rush the whole tune out at once.
|
||||
*
|
||||
* A function of its own rather than a loop inside main()'s ATTEMPT, for two
|
||||
* reasons that both come from the error protocol. CATCH expands to a `break`, so
|
||||
* inside a loop it would escape only the loop and leave the rest of the ATTEMPT
|
||||
* running with an error pending. And PASS expands to a `return` of the context,
|
||||
* which main() cannot do -- it returns an int. Wrapping the loop is what the
|
||||
* protocol prescribes for exactly this shape.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *drive(akbasic_Runtime *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
while ( obj->mode != AKBASIC_MODE_QUIT ) {
|
||||
PASS(errctx, akbasic_runtime_settime(obj, monotonic_ms()));
|
||||
PASS(errctx, akbasic_runtime_run(obj, 1));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -47,8 +103,7 @@ int main(int argc, char **argv)
|
||||
CATCH(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
|
||||
CATCH(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_REPL));
|
||||
}
|
||||
/* Unbounded: this is the driver, and it has nothing else to do. */
|
||||
CATCH(errctx, akbasic_runtime_run(&RUNTIME, 0));
|
||||
CATCH(errctx, drive(&RUNTIME));
|
||||
} CLEANUP {
|
||||
if ( program != NULL ) {
|
||||
IGNORE(aksl_fclose(program));
|
||||
|
||||
277
src/play.c
Normal file
277
src/play.c
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* @file play.c
|
||||
* @brief The PLAY note-string parser and the queue that paces it.
|
||||
*
|
||||
* libakgl's audio commit says where this belongs: *"TEMPO and the PLAY
|
||||
* note-string parser ... belong in the interpreter rather than here."* A tone
|
||||
* generator synthesises pitches; deciding that `O4CDEFG` means five quarter notes
|
||||
* starting at middle C is a language question.
|
||||
*
|
||||
* On a C128, PLAY holds the program until the string finishes. Section 1.6
|
||||
* forbids that, so PLAY parses the whole string into a fixed queue and returns
|
||||
* immediately; akbasic_play_service() releases one note at a time against the
|
||||
* host's clock and is called from akbasic_runtime_step(). A host keeps its frame
|
||||
* rate and the notes still come out in order at their written lengths.
|
||||
*/
|
||||
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
|
||||
#include "verbs.h"
|
||||
|
||||
/**
|
||||
* @brief Semitone offset of each note letter within an octave that starts at C.
|
||||
*
|
||||
* Indexed by letter minus 'A', so it reads out of order. That is the price of
|
||||
* indexing directly instead of searching, and the alternative -- a search over a
|
||||
* seven-entry table -- would not be clearer.
|
||||
*/
|
||||
static const int NOTE_SEMITONE[7] = {
|
||||
9, /* A */
|
||||
11, /* B */
|
||||
0, /* C */
|
||||
2, /* D */
|
||||
4, /* E */
|
||||
5, /* F */
|
||||
7 /* G */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief What each duration letter divides a whole note by.
|
||||
*
|
||||
* BASIC 7.0 spells them Whole, Half, Quarter, eIghth and Sixteenth -- the eighth
|
||||
* is I rather than E because E is already a note.
|
||||
*/
|
||||
static const struct
|
||||
{
|
||||
char letter;
|
||||
int divisor;
|
||||
} DURATIONS[] = {
|
||||
{ 'W', 1 }, /* whole */
|
||||
{ 'H', 2 }, /* half */
|
||||
{ 'Q', 4 }, /* quarter */
|
||||
{ 'I', 8 }, /* eighth */
|
||||
{ 'S', 16 } /* sixteenth */
|
||||
};
|
||||
|
||||
#define DURATION_COUNT ((int)(sizeof(DURATIONS) / sizeof(DURATIONS[0])))
|
||||
|
||||
/** @brief Append one note to the queue, or report that it is full. */
|
||||
static akerr_ErrorContext *enqueue(akbasic_AudioState *audio, int voice, double hz, int ms, bool rest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (audio->count < AKBASIC_MAX_PLAY_NOTES), AKBASIC_ERR_BOUNDS,
|
||||
"PLAY queue is full at %d notes", AKBASIC_MAX_PLAY_NOTES);
|
||||
audio->queue[audio->count].voice = voice;
|
||||
audio->queue[audio->count].hz = hz;
|
||||
audio->queue[audio->count].ms = ms;
|
||||
audio->queue[audio->count].rest = rest;
|
||||
audio->count += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_play_parse(akbasic_Runtime *obj, const char *notes)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AudioState *audio = NULL;
|
||||
size_t i = 0;
|
||||
size_t len = 0;
|
||||
int accidental = 0;
|
||||
int semitone = 0;
|
||||
int wholems = 0;
|
||||
int ms = 0;
|
||||
int value = 0;
|
||||
int d = 0;
|
||||
double hz = 0.0;
|
||||
char c = '\0';
|
||||
bool dotted = false;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && notes != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in play_parse");
|
||||
audio = &obj->audio_state;
|
||||
len = strlen(notes);
|
||||
|
||||
/*
|
||||
* A loop, so no CATCH and no _BREAK macros in here -- they expand to a C
|
||||
* break and would leave the loop with an error still pending. PASS and the
|
||||
* _RETURN forms only.
|
||||
*/
|
||||
for ( i = 0; i < len; i++ ) {
|
||||
c = (char)toupper((unsigned char)notes[i]);
|
||||
|
||||
/* Accidentals and dots are prefixes; they modify the next note letter. */
|
||||
if ( c == '#' ) {
|
||||
accidental = 1;
|
||||
continue;
|
||||
}
|
||||
if ( c == '$' ) {
|
||||
accidental = -1;
|
||||
continue;
|
||||
}
|
||||
if ( c == '.' ) {
|
||||
dotted = true;
|
||||
continue;
|
||||
}
|
||||
if ( c == ' ' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* A duration letter changes the length of every note after it. */
|
||||
for ( d = 0; d < DURATION_COUNT; d++ ) {
|
||||
if ( DURATIONS[d].letter == c ) {
|
||||
PASS(errctx, akbasic_audio_whole_note_ms(audio->tempo, &wholems));
|
||||
audio->notems = wholems / DURATIONS[d].divisor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( d < DURATION_COUNT ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* The single-argument settings all take one decimal digit. */
|
||||
if ( c == 'V' || c == 'O' || c == 'T' || c == 'U' || c == 'X' ) {
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
(i + 1 < len && isdigit((unsigned char)notes[i + 1])),
|
||||
AKBASIC_ERR_SYNTAX,
|
||||
"PLAY %c expected a digit", c);
|
||||
value = notes[i + 1] - '0';
|
||||
i += 1;
|
||||
switch ( c ) {
|
||||
case 'V':
|
||||
FAIL_ZERO_RETURN(errctx, (value >= 1 && value <= AKBASIC_AUDIO_VOICES),
|
||||
AKBASIC_ERR_BOUNDS,
|
||||
"PLAY V%d out of range (1 to %d)", value, AKBASIC_AUDIO_VOICES);
|
||||
audio->voice = value - 1;
|
||||
break;
|
||||
case 'O':
|
||||
FAIL_ZERO_RETURN(errctx, (value >= 0 && value <= 6), AKBASIC_ERR_BOUNDS,
|
||||
"PLAY O%d out of range (0 to 6)", value);
|
||||
audio->octave = value;
|
||||
break;
|
||||
case 'T':
|
||||
FAIL_ZERO_RETURN(errctx, (value >= 0 && value < AKBASIC_ENVELOPES),
|
||||
AKBASIC_ERR_BOUNDS,
|
||||
"PLAY T%d out of range (0 to %d)", value, AKBASIC_ENVELOPES - 1);
|
||||
audio->envelope = value;
|
||||
break;
|
||||
case 'U':
|
||||
FAIL_ZERO_RETURN(errctx, (value >= 0 && value <= 9), AKBASIC_ERR_BOUNDS,
|
||||
"PLAY U%d out of range (0 to 9)", value);
|
||||
audio->level = (double)value / 9.0;
|
||||
break;
|
||||
default:
|
||||
/*
|
||||
* X selects the filter, which there is nothing behind -- see
|
||||
* TODO.md section 7. Refused rather than ignored, for the same
|
||||
* reason FILTER is: a program that asked for a filtered sound
|
||||
* and got an unfiltered one has been lied to.
|
||||
*/
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
||||
"PLAY X selects the filter, which this device does not have");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ms = audio->notems;
|
||||
if ( dotted ) {
|
||||
/* A dot adds half the note's length again. */
|
||||
ms = ms + (ms / 2);
|
||||
dotted = false;
|
||||
}
|
||||
|
||||
if ( c == 'R' ) {
|
||||
PASS(errctx, enqueue(audio, audio->voice, 0.0, ms, true));
|
||||
accidental = 0;
|
||||
continue;
|
||||
}
|
||||
if ( c == 'M' ) {
|
||||
/*
|
||||
* A measure bar. On a C128 it synchronises the voices; with one
|
||||
* sequential queue there is nothing to synchronise, so it is a
|
||||
* no-op rather than an error -- a listing full of them should still
|
||||
* play. TODO.md section 5.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (c >= 'A' && c <= 'G'), AKBASIC_ERR_SYNTAX,
|
||||
"PLAY does not understand '%c'", notes[i]);
|
||||
|
||||
semitone = NOTE_SEMITONE[c - 'A'] + accidental;
|
||||
accidental = 0;
|
||||
/*
|
||||
* B# wraps to the next octave and C$ to the previous one. Wrapping the
|
||||
* semitone without moving the octave would put B# a full octave below
|
||||
* where it belongs.
|
||||
*/
|
||||
if ( semitone > 11 ) {
|
||||
semitone -= 12;
|
||||
PASS(errctx, akbasic_audio_note_hz(semitone,
|
||||
(audio->octave < 6) ? audio->octave + 1 : 6,
|
||||
&hz));
|
||||
} else if ( semitone < 0 ) {
|
||||
semitone += 12;
|
||||
PASS(errctx, akbasic_audio_note_hz(semitone,
|
||||
(audio->octave > 0) ? audio->octave - 1 : 0,
|
||||
&hz));
|
||||
} else {
|
||||
PASS(errctx, akbasic_audio_note_hz(semitone, audio->octave, &hz));
|
||||
}
|
||||
PASS(errctx, enqueue(audio, audio->voice, hz, ms, false));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_play_service(akbasic_Runtime *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AudioState *audio = NULL;
|
||||
akbasic_PlayNote *note = NULL;
|
||||
akbasic_Envelope *envelope = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in play_service");
|
||||
audio = &obj->audio_state;
|
||||
|
||||
if ( audio->head >= audio->count ) {
|
||||
/* Nothing queued. Reset so the next PLAY starts from the front. */
|
||||
audio->head = 0;
|
||||
audio->count = 0;
|
||||
audio->sounding = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
if ( obj->timems < audio->nextms ) {
|
||||
/* The note holding the queue has not run out yet. */
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
/*
|
||||
* No audio backend means the queue drains silently rather than jamming. A
|
||||
* host that took the device away mid-tune should not leave the script stuck
|
||||
* behind a note that can never finish.
|
||||
*/
|
||||
note = &audio->queue[audio->head];
|
||||
audio->head += 1;
|
||||
audio->nextms = obj->timems + note->ms;
|
||||
audio->sounding = true;
|
||||
|
||||
if ( obj->audio == NULL ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
if ( note->rest ) {
|
||||
PASS(errctx, obj->audio->stop(obj->audio, note->voice));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
envelope = &audio->envelopes[audio->envelope];
|
||||
PASS(errctx, obj->audio->waveform(obj->audio, note->voice, envelope->waveform));
|
||||
PASS(errctx, obj->audio->envelope(obj->audio, note->voice, envelope->attack,
|
||||
envelope->decay, envelope->sustain, envelope->release));
|
||||
PASS(errctx, obj->audio->tone(obj->audio, note->voice, note->hz, note->ms));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -141,6 +141,7 @@ akerr_ErrorContext *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink
|
||||
obj->inputEof = false;
|
||||
|
||||
PASS(errctx, akbasic_graphics_state_init(&obj->gfx));
|
||||
PASS(errctx, akbasic_audio_state_init(&obj->audio_state));
|
||||
PASS(errctx, akbasic_valuepool_init(&obj->valuepool));
|
||||
PASS(errctx, akbasic_value_zero(&obj->staticTrueValue));
|
||||
PASS(errctx, akbasic_value_zero(&obj->staticFalseValue));
|
||||
@@ -826,6 +827,14 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in step");
|
||||
|
||||
/*
|
||||
* Release the next queued note if its predecessor's time is up. PLAY does not
|
||||
* block -- section 1.6 forbids it -- so this is what paces a tune, and it
|
||||
* runs before the QUIT check so a program's last notes still come out while
|
||||
* a host keeps calling step().
|
||||
*/
|
||||
PASS(errctx, akbasic_play_service(obj));
|
||||
|
||||
if ( obj->mode == AKBASIC_MODE_QUIT ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
306
src/runtime_audio.c
Normal file
306
src/runtime_audio.c
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* @file runtime_audio.c
|
||||
* @brief The group I verb implementations: SOUND, ENVELOPE, VOL, PLAY, TEMPO
|
||||
* and the refusal that is FILTER.
|
||||
*
|
||||
* Same rule as src/runtime_graphics.c -- no SDL and no libakgl header here.
|
||||
* Everything goes through the akbasic_AudioBackend record.
|
||||
*
|
||||
* The reference implements none of these, so the semantics are Commodore BASIC
|
||||
* 7.0's. Two of its behaviours are deliberately not reproduced and both are
|
||||
* recorded in TODO.md section 5: PLAY does not block, and SOUND's frequency-sweep
|
||||
* arguments are refused rather than faked.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
|
||||
#include "verbs.h"
|
||||
|
||||
/* Most verbs answer "did something happen"; this is that answer. */
|
||||
#define SUCCEED_TRUE(__obj, __dest) \
|
||||
do { \
|
||||
*(__dest) = &(__obj)->staticTrueValue; \
|
||||
} while ( 0 )
|
||||
|
||||
/** @brief Refuse politely when the host lent us no audio device. */
|
||||
static akerr_ErrorContext *require_audio(akbasic_Runtime *obj, const char *verb)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in require_audio");
|
||||
FAIL_ZERO_RETURN(errctx, (obj->audio != NULL), AKBASIC_ERR_DEVICE,
|
||||
"%s needs an audio device and this runtime has none", verb);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Collect a verb's numeric arguments, as the graphics verbs do.
|
||||
*
|
||||
* Duplicated rather than shared with src/runtime_graphics.c on purpose: sharing
|
||||
* it would mean a third file existing only to hold one static helper, and the
|
||||
* two copies answer to different verbs. If a third group wants it, that is the
|
||||
* point at which it earns a home of its own.
|
||||
*/
|
||||
static akerr_ErrorContext *collect_numbers(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, double *values, int max, int *count)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_ASTLeaf *arg = NULL;
|
||||
akbasic_Value *value = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && values != NULL && count != NULL),
|
||||
AKERR_NULLPOINTER, "NULL argument in collect_numbers");
|
||||
*count = 0;
|
||||
arg = akbasic_leaf_first_argument(expr);
|
||||
while ( arg != NULL ) {
|
||||
FAIL_NONZERO_RETURN(errctx, (*count >= max), AKBASIC_ERR_SYNTAX,
|
||||
"%s takes at most %d arguments", verb, max);
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING),
|
||||
AKBASIC_ERR_TYPE,
|
||||
"%s expected a number in argument %d", verb, *count + 1);
|
||||
values[*count] = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
||||
? value->floatval : (double)value->intval;
|
||||
*count += 1;
|
||||
arg = arg->right;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- SOUND --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_sound(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
double args[8];
|
||||
int count = 0;
|
||||
int voice = 0;
|
||||
int ms = 0;
|
||||
double hz = 0.0;
|
||||
akbasic_Envelope *envelope = NULL;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
PASS(errctx, require_audio(obj, "SOUND"));
|
||||
PASS(errctx, collect_numbers(obj, expr, "SOUND", args, 8, &count));
|
||||
FAIL_ZERO_RETURN(errctx, (count >= 3), AKBASIC_ERR_SYNTAX,
|
||||
"SOUND expected a voice, a frequency and a duration");
|
||||
|
||||
voice = (int)args[0];
|
||||
FAIL_ZERO_RETURN(errctx, (voice >= 1 && voice <= AKBASIC_AUDIO_VOICES),
|
||||
AKBASIC_ERR_BOUNDS, "SOUND voice %d out of range (1 to %d)",
|
||||
voice, AKBASIC_AUDIO_VOICES);
|
||||
voice -= 1; /* BASIC counts voices from 1; the backend from 0 */
|
||||
|
||||
PASS(errctx, akbasic_audio_register_to_hz((int)args[1], &hz));
|
||||
|
||||
/*
|
||||
* BASIC counts SOUND's duration in jiffies -- sixtieths of a second, the
|
||||
* PAL/NTSC vertical blank the KERNAL counts on. The backend takes
|
||||
* milliseconds.
|
||||
*/
|
||||
FAIL_ZERO_RETURN(errctx, (args[2] >= 0.0), AKBASIC_ERR_VALUE,
|
||||
"SOUND duration must not be negative");
|
||||
ms = (int)((args[2] * 1000.0) / 60.0);
|
||||
|
||||
/*
|
||||
* Arguments 4 through 6 are a frequency sweep: a direction, a floor and a
|
||||
* step. There is no akgl_audio_* equivalent, and faking one by re-issuing
|
||||
* tones from akbasic_runtime_step() would tie audible pitch to how often the
|
||||
* host happens to call us -- a tune that changes key with the frame rate.
|
||||
* Refused, and filed upstream as akgl_audio_sweep. TODO.md section 7.
|
||||
*/
|
||||
FAIL_NONZERO_RETURN(errctx, (count >= 4 && args[3] != 0.0), AKBASIC_ERR_DEVICE,
|
||||
"SOUND's frequency sweep needs a device capability that does not exist yet");
|
||||
|
||||
if ( count >= 7 ) {
|
||||
/* The seventh argument selects a waveform, 0 through 3. */
|
||||
FAIL_ZERO_RETURN(errctx, (args[6] >= 0.0 && args[6] <= 3.0), AKBASIC_ERR_BOUNDS,
|
||||
"SOUND waveform %d out of range (0 to 3)", (int)args[6]);
|
||||
PASS(errctx, obj->audio->waveform(obj->audio, voice, (akbasic_Waveform)(int)args[6]));
|
||||
} else {
|
||||
envelope = &obj->audio_state.envelopes[obj->audio_state.envelope];
|
||||
PASS(errctx, obj->audio->waveform(obj->audio, voice, envelope->waveform));
|
||||
}
|
||||
|
||||
PASS(errctx, obj->audio->tone(obj->audio, voice, hz, ms));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ ENVELOPE --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_envelope(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
double args[7];
|
||||
int count = 0;
|
||||
int preset = 0;
|
||||
int ms = 0;
|
||||
akbasic_Envelope *envelope = NULL;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
/*
|
||||
* ENVELOPE defines a preset; it does not sound anything, so it needs no
|
||||
* device and a program can set its instruments up before a host lends it one.
|
||||
*/
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in ENVELOPE");
|
||||
PASS(errctx, collect_numbers(obj, expr, "ENVELOPE", args, 7, &count));
|
||||
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
|
||||
"ENVELOPE expected a preset number");
|
||||
|
||||
preset = (int)args[0];
|
||||
FAIL_ZERO_RETURN(errctx, (preset >= 0 && preset < AKBASIC_ENVELOPES), AKBASIC_ERR_BOUNDS,
|
||||
"ENVELOPE %d out of range (0 to %d)", preset, AKBASIC_ENVELOPES - 1);
|
||||
envelope = &obj->audio_state.envelopes[preset];
|
||||
|
||||
/*
|
||||
* Attack, decay and release arrive as the SID's 0-15 rate numbers and become
|
||||
* milliseconds. Sustain is the odd one out: it is a *level*, not a time, so
|
||||
* 0-15 maps onto 0.0-1.0 rather than through the rate table.
|
||||
*/
|
||||
if ( count >= 2 ) {
|
||||
PASS(errctx, akbasic_audio_rate_to_ms((int)args[1], true, &ms));
|
||||
envelope->attack = ms;
|
||||
}
|
||||
if ( count >= 3 ) {
|
||||
PASS(errctx, akbasic_audio_rate_to_ms((int)args[2], false, &ms));
|
||||
envelope->decay = ms;
|
||||
}
|
||||
if ( count >= 4 ) {
|
||||
FAIL_ZERO_RETURN(errctx, (args[3] >= 0.0 && args[3] <= 15.0), AKBASIC_ERR_BOUNDS,
|
||||
"ENVELOPE sustain %d out of range (0 to 15)", (int)args[3]);
|
||||
envelope->sustain = args[3] / 15.0;
|
||||
}
|
||||
if ( count >= 5 ) {
|
||||
PASS(errctx, akbasic_audio_rate_to_ms((int)args[4], false, &ms));
|
||||
envelope->release = ms;
|
||||
}
|
||||
if ( count >= 6 ) {
|
||||
FAIL_ZERO_RETURN(errctx, (args[5] >= 0.0 && args[5] <= 3.0), AKBASIC_ERR_BOUNDS,
|
||||
"ENVELOPE waveform %d out of range (0 to 3)", (int)args[5]);
|
||||
envelope->waveform = (akbasic_Waveform)(int)args[5];
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- VOL --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_vol(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
double args[1];
|
||||
int count = 0;
|
||||
int level = 0;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in VOL");
|
||||
PASS(errctx, collect_numbers(obj, expr, "VOL", args, 1, &count));
|
||||
FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "VOL expected a level");
|
||||
|
||||
level = (int)args[0];
|
||||
FAIL_ZERO_RETURN(errctx, (level >= 0 && level <= 15), AKBASIC_ERR_BOUNDS,
|
||||
"VOL %d out of range (0 to 15)", level);
|
||||
obj->audio_state.level = (double)level / 15.0;
|
||||
|
||||
/*
|
||||
* Recorded whatever happens, but only pushed when there is somewhere to push
|
||||
* it. VOL before the host attaches a device should still take effect when
|
||||
* one arrives.
|
||||
*/
|
||||
if ( obj->audio != NULL ) {
|
||||
PASS(errctx, obj->audio->volume(obj->audio, obj->audio_state.level));
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- TEMPO --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_tempo(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
double args[1];
|
||||
int count = 0;
|
||||
int tempo = 0;
|
||||
int wholems = 0;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in TEMPO");
|
||||
PASS(errctx, collect_numbers(obj, expr, "TEMPO", args, 1, &count));
|
||||
FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "TEMPO expected a value");
|
||||
|
||||
tempo = (int)args[0];
|
||||
PASS(errctx, akbasic_audio_whole_note_ms(tempo, &wholems));
|
||||
obj->audio_state.tempo = tempo;
|
||||
|
||||
/*
|
||||
* The current note length rescales with the tempo rather than resetting to a
|
||||
* quarter note: a program that set eighth notes and then changed tempo means
|
||||
* faster eighth notes, not quarter notes again.
|
||||
*/
|
||||
PASS(errctx, akbasic_audio_whole_note_ms(AKBASIC_TEMPO_DEFAULT, &count));
|
||||
obj->audio_state.notems = (obj->audio_state.notems * wholems) / count;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- PLAY --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_play(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_ASTLeaf *arg = NULL;
|
||||
akbasic_Value *value = NULL;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
PASS(errctx, require_audio(obj, "PLAY"));
|
||||
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
|
||||
|
||||
arg = akbasic_leaf_first_argument(expr);
|
||||
if ( arg == NULL ) {
|
||||
arg = expr->right;
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "PLAY expected a string");
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
||||
FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||||
"PLAY expected a string");
|
||||
|
||||
/*
|
||||
* Parses and queues, then returns. A C128 would hold the program here until
|
||||
* the last note finished; section 1.6 forbids that, so the notes come out of
|
||||
* akbasic_runtime_step() against the host's clock instead. TODO.md section 5.
|
||||
*/
|
||||
PASS(errctx, akbasic_play_parse(obj, value->stringval));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- FILTER --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_filter(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)expr; (void)lval; (void)rval; (void)dest;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in FILTER");
|
||||
|
||||
/*
|
||||
* FILTER sets the SID's cutoff, its three band switches and its resonance.
|
||||
* akgl_audio_* synthesises raw waveforms and mixes them; there is no filter
|
||||
* stage to configure, and SDL3 supplies no primitive to build one from.
|
||||
*
|
||||
* Refused rather than accepted-and-ignored. A program that asks for a
|
||||
* low-pass and gets an unfiltered square wave has been lied to, and the whole
|
||||
* reason it is in the table at all is so the diagnostic names the real
|
||||
* problem instead of "undefined verb". TODO.md section 7.
|
||||
*/
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
||||
"FILTER needs a filter stage this device does not have");
|
||||
}
|
||||
@@ -50,7 +50,9 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
||||
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
|
||||
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
|
||||
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
|
||||
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
|
||||
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
|
||||
{ "GOSUB", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_gosub },
|
||||
{ "GOTO", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_goto },
|
||||
@@ -74,6 +76,7 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "OR", AKBASIC_TOK_OR, -1, NULL, NULL },
|
||||
{ "PAINT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_paint },
|
||||
{ "PEEK", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_peek },
|
||||
{ "PLAY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_play },
|
||||
{ "POINTER", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_pointer },
|
||||
{ "POINTERVAR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_pointervar },
|
||||
{ "POKE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_poke, akbasic_cmd_poke },
|
||||
@@ -90,15 +93,18 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "SHL", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_shl },
|
||||
{ "SHR", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_shr },
|
||||
{ "SIN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_sin },
|
||||
{ "SOUND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sound },
|
||||
{ "SPC", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_spc },
|
||||
{ "SSHAPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sshape },
|
||||
{ "STEP", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "STOP", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_stop },
|
||||
{ "STR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_str },
|
||||
{ "TAN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_tan },
|
||||
{ "TEMPO", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_tempo },
|
||||
{ "THEN", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "TO", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "VAL", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_val },
|
||||
{ "VOL", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_vol },
|
||||
{ "XOR", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_xor },
|
||||
};
|
||||
|
||||
|
||||
@@ -93,4 +93,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_paint(struct akbasic_Runtime *obj
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_scale(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sshape(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
/* Group I sound verbs -- src/runtime_audio.c */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_envelope(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_filter(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_play(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sound(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tempo(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_vol(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
#endif // _AKBASIC_SRC_VERBS_H_
|
||||
|
||||
Reference in New Issue
Block a user