/** * @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 #include #include #include #include #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"); }