Files
akbasic/src/audio_akgl.c
Andrew Kesterson fca9ad4a89 Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
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>
2026-07-31 13:25:14 -04:00

159 lines
5.0 KiB
C

/**
* @file audio_akgl.c
* @brief Wires the audio backend record to libakgl's tone generator.
*
* Another thin adaptor. The one thing in here worth reading is the waveform
* mapping: akbasic_Waveform and akgl_AudioWaveform currently agree on every
* value, and the mapping is still written out rather than cast across, because
* two enumerations agreeing by accident is not a contract and a future SOUND
* waveform number would break the coincidence silently.
*/
#include <string.h>
#include <akerror.h>
#include <akgl/audio.h>
#include <akgl/error.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
/**
* @brief Map a BASIC waveform onto libakgl's.
*
* @param waveform The BASIC-side selection.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_BOUNDS When the waveform is not one of the four SOUND defines.
*/
static akerr_ErrorContext *to_akgl_waveform(akbasic_Waveform waveform, akgl_AudioWaveform *dest)
{
PREPARE_ERROR(errctx);
switch ( waveform ) {
case AKBASIC_WAVE_TRIANGLE:
*dest = AKGL_AUDIO_WAVE_TRIANGLE;
break;
case AKBASIC_WAVE_SAWTOOTH:
*dest = AKGL_AUDIO_WAVE_SAWTOOTH;
break;
case AKBASIC_WAVE_SQUARE:
*dest = AKGL_AUDIO_WAVE_SQUARE;
break;
case AKBASIC_WAVE_NOISE:
*dest = AKGL_AUDIO_WAVE_NOISE;
break;
default:
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "No such waveform %d", (int)waveform);
}
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *snd_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
{
PREPARE_ERROR(errctx);
(void)self;
FAIL_ZERO_RETURN(errctx, (ms >= 0), AKERR_VALUE, "Negative note duration %d", ms);
PASS(errctx, akgl_audio_tone(voice, (float32_t)hz, (uint32_t)ms));
SUCCEED_RETURN(errctx);
}
/**
* @brief A note whose pitch moves, for SOUND's dir/min/step arguments.
*
* New in libakgl 0.3.0, which is why the record's entry may be NULL and why
* SOUND checks before using it. The sweep is advanced on the mixer's own frame
* counter -- doing it from akbasic_runtime_step() instead would tie audible
* pitch to how often a host happens to call us, which is the reason this was
* filed upstream rather than faked here.
*/
static akerr_ErrorContext *snd_sweep(akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_hz, int ms)
{
PREPARE_ERROR(errctx);
(void)self;
FAIL_ZERO_RETURN(errctx, (ms >= 0), AKERR_VALUE, "Negative note duration %d", ms);
PASS(errctx, akgl_audio_sweep(voice, (float32_t)from_hz, (float32_t)to_hz,
(float32_t)step_hz, (uint32_t)ms));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *snd_stop(akbasic_AudioBackend *self, int voice)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, akgl_audio_stop(voice));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *snd_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
{
PREPARE_ERROR(errctx);
akgl_AudioWaveform mapped = AKGL_AUDIO_WAVE_SQUARE;
(void)self;
PASS(errctx, to_akgl_waveform(waveform, &mapped));
PASS(errctx, akgl_audio_waveform(voice, mapped));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *snd_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
{
PREPARE_ERROR(errctx);
(void)self;
FAIL_ZERO_RETURN(errctx, (attack >= 0 && decay >= 0 && release >= 0), AKERR_VALUE,
"Negative envelope stage: %d/%d/%d", attack, decay, release);
PASS(errctx, akgl_audio_envelope(voice, (uint32_t)attack, (uint32_t)decay,
(float32_t)sustain, (uint32_t)release));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *snd_volume(akbasic_AudioBackend *self, double level)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, akgl_audio_volume((float32_t)level));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *snd_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, akgl_audio_voice_active(voice, active));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_audio_init_akgl(akbasic_AudioBackend *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL backend in audio_init_akgl");
PASS(errctx, akgl_error_init());
/*
* Opens an SDL audio device. A host that owns its own audio pipeline can
* skip this entirely and pull samples with akgl_audio_mix() instead -- the
* voice table works either way, which is the design upstream chose and the
* reason its own suite can be deterministic.
*/
PASS(errctx, akgl_audio_init());
obj->self = NULL; /* the voice table is libakgl's, not ours */
obj->tone = snd_tone;
obj->sweep = snd_sweep;
obj->stop = snd_stop;
obj->waveform = snd_waveform;
obj->envelope = snd_envelope;
obj->volume = snd_volume;
obj->voice_active = snd_voice_active;
SUCCEED_RETURN(errctx);
}