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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
229 lines
9.7 KiB
C
229 lines
9.7 KiB
C
/**
|
|
* @file audio.h
|
|
* @brief Declares the audio backend: where SOUND, PLAY, ENVELOPE and VOL land.
|
|
*
|
|
* Same reasoning as graphics.h -- the core library is free of SDL, so the sound
|
|
* verbs call through a record of function pointers and akbasic_audio_init_akgl()
|
|
* in the akbasic_akgl target wires that record to akgl_audio_*.
|
|
*
|
|
* The libakgl API this is modelled on is a tone generator, not a sample player:
|
|
* three voices, a waveform and an ADSR envelope each, mixed to one stream. That
|
|
* is the right shape, because BASIC 7.0's sound verbs describe notes rather than
|
|
* recordings. What it does not have is a filter stage, which is why FILTER is
|
|
* refused -- see TODO.md section 7.
|
|
*/
|
|
|
|
#ifndef _AKBASIC_AUDIO_H_
|
|
#define _AKBASIC_AUDIO_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/types.h>
|
|
|
|
/** @brief Number of independent voices, matching the SID and matching libakgl. */
|
|
#define AKBASIC_AUDIO_VOICES 3
|
|
|
|
/** @brief How many ENVELOPE presets a program may define. BASIC 7.0 numbers them 0 through 9. */
|
|
#define AKBASIC_ENVELOPES 10
|
|
|
|
/** @brief How many notes one PLAY string may queue. */
|
|
#define AKBASIC_MAX_PLAY_NOTES 128
|
|
|
|
/** @brief TEMPO's range, as BASIC 7.0 states it. */
|
|
#define AKBASIC_TEMPO_MIN 1
|
|
#define AKBASIC_TEMPO_MAX 255
|
|
#define AKBASIC_TEMPO_DEFAULT 8
|
|
|
|
/**
|
|
* @brief Waveform selection, numbered as BASIC 7.0's SOUND numbers it.
|
|
*
|
|
* Declared here rather than reused from akgl/audio.h so the core library does
|
|
* not include a libakgl header. The akgl backend maps these across; the values
|
|
* happen to agree today and the mapping is still written out, because two
|
|
* enumerations agreeing by accident is not a contract.
|
|
*/
|
|
typedef enum
|
|
{
|
|
AKBASIC_WAVE_TRIANGLE = 0, /* SOUND waveform 0 */
|
|
AKBASIC_WAVE_SAWTOOTH = 1, /* SOUND waveform 1 */
|
|
AKBASIC_WAVE_SQUARE = 2, /* SOUND waveform 2, and the power-on default */
|
|
AKBASIC_WAVE_NOISE = 3 /* SOUND waveform 3 */
|
|
} akbasic_Waveform;
|
|
|
|
/**
|
|
* @brief Where the sound verbs play.
|
|
*
|
|
* Voices here are numbered from zero. BASIC numbers them 1 through 3 and the
|
|
* conversion happens once, in the verb handler, so a backend never sees a BASIC
|
|
* voice number.
|
|
*
|
|
* Durations are milliseconds. BASIC counts in jiffies (1/60 s) and note lengths
|
|
* derived from TEMPO; both are converted in src/audio_tables.c before they get
|
|
* here, so a backend never sees a jiffy either.
|
|
*/
|
|
typedef struct akbasic_AudioBackend
|
|
{
|
|
void *self;
|
|
|
|
/** Start a note on a voice, for a bounded duration. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*tone)(struct akbasic_AudioBackend *self, int voice, double hz, int ms);
|
|
/**
|
|
* Start a note whose pitch moves, which is what SOUND's `dir`, `min` and
|
|
* `step` arguments ask for -- a siren, a laser, a falling bomb.
|
|
*
|
|
* May be NULL, and a host that leaves it so gets `SOUND` refused with
|
|
* AKBASIC_ERR_DEVICE for a swept note and served normally for a held one.
|
|
* That is deliberate: this arrived in libakgl 0.3.0, and a backend written
|
|
* against an older one is still a valid backend for everything else.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*sweep)(struct akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_hz, int ms);
|
|
/** Silence a voice immediately. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*stop)(struct akbasic_AudioBackend *self, int voice);
|
|
/** Select the waveform a voice synthesises. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*waveform)(struct akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform);
|
|
/** Set a voice's ADSR envelope. Sustain is a level from 0.0 to 1.0, not a time. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*envelope)(struct akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release);
|
|
/** Set the master output level, 0.0 to 1.0. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*volume)(struct akbasic_AudioBackend *self, double level);
|
|
/** Report whether a voice is still sounding. PLAY's queue uses this to pace itself. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*voice_active)(struct akbasic_AudioBackend *self, int voice, bool *active);
|
|
} akbasic_AudioBackend;
|
|
|
|
/** @brief One ENVELOPE preset, in the units the backend takes. */
|
|
typedef struct
|
|
{
|
|
int attack; /* milliseconds */
|
|
int decay; /* milliseconds */
|
|
double sustain; /* level from 0.0 to 1.0, not a time */
|
|
int release; /* milliseconds */
|
|
akbasic_Waveform waveform;
|
|
} akbasic_Envelope;
|
|
|
|
/** @brief One note waiting to be played. A rest occupies time and makes no sound. */
|
|
typedef struct
|
|
{
|
|
int voice;
|
|
double hz;
|
|
int ms;
|
|
bool rest;
|
|
} akbasic_PlayNote;
|
|
|
|
/**
|
|
* @brief The sound verbs' own state, which lives on the runtime.
|
|
*
|
|
* The queue is why PLAY does not block. On a C128, PLAY holds the program until
|
|
* the string finishes; section 1.6 forbids that, so PLAY parses the string into
|
|
* this queue and returns, and akbasic_runtime_step() releases one note at a time
|
|
* against the host's clock. A host keeps its frame rate and the notes still come
|
|
* out in order, at their written lengths.
|
|
*/
|
|
typedef struct
|
|
{
|
|
akbasic_Envelope envelopes[AKBASIC_ENVELOPES];
|
|
akbasic_PlayNote queue[AKBASIC_MAX_PLAY_NOTES];
|
|
int head; /* next note to start */
|
|
int count; /* notes queued */
|
|
int64_t nextms; /* host time the head note may start at */
|
|
bool sounding; /* a queued note is currently held */
|
|
|
|
int tempo; /* TEMPO, 1 to 255 */
|
|
int voice; /* PLAY's current voice, 0-based */
|
|
int octave; /* PLAY's current octave, 0 to 6 */
|
|
int envelope; /* PLAY's current ENVELOPE preset */
|
|
int notems; /* PLAY's current note length in milliseconds */
|
|
double level; /* VOL, 0.0 to 1.0 */
|
|
} akbasic_AudioState;
|
|
|
|
/**
|
|
* @brief Reset the sound state to its power-on values.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_state_init(akbasic_AudioState *obj);
|
|
|
|
/**
|
|
* @brief Convert a SOUND frequency register value to hertz.
|
|
*
|
|
* BASIC's SOUND takes the number that would go into a SID frequency register,
|
|
* not a pitch. The backend takes hertz.
|
|
*
|
|
* @param value Register value, 0 through 65535.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `dest` is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When `value` is outside 0..65535.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_register_to_hz(int value, double *dest);
|
|
|
|
/**
|
|
* @brief Convert an ENVELOPE rate number to milliseconds.
|
|
*
|
|
* BASIC's ENVELOPE takes the SID's 0-15 rate numbers, whose real durations are
|
|
* non-linear and tabulated in silicon rather than computed.
|
|
*
|
|
* @param rate Rate number, 0 through 15.
|
|
* @param isattack True for the attack table, false for the decay/release one.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `dest` is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When `rate` is outside 0..15.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_rate_to_ms(int rate, bool isattack, int *dest);
|
|
|
|
/**
|
|
* @brief Pitch of a note, by semitone offset from C and by octave.
|
|
* @param semitone Offset from C within the octave, 0 through 11.
|
|
* @param octave Octave number, 0 through 6, where 4 holds the A of 440 Hz.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `dest` is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When either argument is out of range.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_note_hz(int semitone, int octave, double *dest);
|
|
|
|
/**
|
|
* @brief How long a whole note lasts at a given TEMPO, in milliseconds.
|
|
* @param tempo TEMPO value, 1 through 255.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `dest` is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When `tempo` is outside 1..255.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_whole_note_ms(int tempo, int *dest);
|
|
|
|
/* --- Internal API: exposed for the PLAY verb, the step loop and the tests. --- */
|
|
|
|
struct akbasic_Runtime;
|
|
|
|
/**
|
|
* @brief Parse a PLAY string and append its notes to the queue.
|
|
*
|
|
* Split out from the verb so a test can assert what a string parses *to* without
|
|
* also having to drive the clock that drains it.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param notes A PLAY note string.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When either argument is NULL.
|
|
* @throws AKBASIC_ERR_SYNTAX When the string contains something PLAY does not define.
|
|
* @throws AKBASIC_ERR_BOUNDS When a setting is out of range or the queue is full.
|
|
* @throws AKBASIC_ERR_DEVICE When the string selects the filter, which does not exist.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_play_parse(struct akbasic_Runtime *obj, const char *notes);
|
|
|
|
/**
|
|
* @brief Release the next queued note if the one before it has run out.
|
|
*
|
|
* Called from akbasic_runtime_step(). This is what makes PLAY non-blocking: the
|
|
* verb queues and returns, and the notes come out here against whatever time the
|
|
* host last handed to akbasic_runtime_settime().
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_play_service(struct akbasic_Runtime *obj);
|
|
|
|
#endif // _AKBASIC_AUDIO_H_
|