Reach hardware through backend records, and take the time from the host
Groups G, I and E are unblocked but cannot be written yet: the core library is free of SDL and builds with no libakgl present, so a graphics verb cannot call akgl_draw_* and a sound verb cannot call akgl_audio_*. This adds what they call instead. Three records of function pointers -- akbasic_GraphicsBackend, _AudioBackend and _InputBackend -- in the same shape as akbasic_TextSink, and the same shape libakgl uses for akgl_RenderBackend. akbasic_runtime_set_devices() attaches any subset; all three may be NULL and that is the standalone driver's normal state, so a runtime with no backends still comes up and still prints. A verb that needs one it was not given raises the new AKBASIC_ERR_DEVICE rather than dereferencing a NULL vtable. Two decisions worth stating. The graphics record has no circle entry point: BASIC 7.0's CIRCLE takes two radii, an arc range, a rotation and a degree increment, which makes it a polygon by definition, so it will be built from line calls rather than from akgl_draw_circle. And coordinates are double rather than an integer pixel address, because SCALE makes them fractional and rounding at each verb rather than once at the backend accumulates drift along a polyline. akbasic_runtime_settime() is how SOUND, PLAY and TEMPO get a clock without the library reading one. Section 1.6 forbids blocking or owning a loop, so the caller that owns the frame owns the time -- which is what libakgl already does, since akgl_actor_logic_changeframe takes curtimems as an argument. Left unset it is zero and every duration expires immediately: audible, but never a hang. AKBASIC_ERR_LAST is a sentinel rather than a status, so tests/error_codes.c walks every code looking for an unnamed one without anybody remembering to widen the loop when a code is added. tests/mockdevice.h records every backend call as a formatted line. The graphics and audio verbs emit nothing a golden file can compare, so that log is where their assertions have to live -- and since it needs no SDL, the whole of groups G, I and E stays testable in the default build. 62/62 ctest, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
71
include/akbasic/audio.h
Normal file
71
include/akbasic/audio.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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 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);
|
||||
/** 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;
|
||||
|
||||
#endif // _AKBASIC_AUDIO_H_
|
||||
Reference in New Issue
Block a user