307 lines
12 KiB
C
307 lines
12 KiB
C
|
|
/**
|
||
|
|
* @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");
|
||
|
|
}
|