278 lines
8.4 KiB
C
278 lines
8.4 KiB
C
|
|
/**
|
||
|
|
* @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);
|
||
|
|
}
|