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>
159 lines
5.0 KiB
C
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);
|
|
}
|