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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-07-31 13:25:14 -04:00
parent 42ec0ec545
commit b751c1b600
16 changed files with 495 additions and 265 deletions

View File

@@ -73,6 +73,61 @@ static akerr_ErrorContext *collect_numbers(akbasic_Runtime *obj, akbasic_ASTLeaf
SUCCEED_RETURN(errctx);
}
/**
* @brief Issue SOUND's swept note, translating its arguments out of SID space.
*
* BASIC 7.0 spells the sweep as `dir`, `min` and `step`, all in the SID's
* 16-bit register units rather than in hertz, and `dir` picks the shape:
*
* 0 no sweep -- handled by the caller, which issues a held note instead
* 1 up, from the SOUND frequency toward min
* 2 down, from the SOUND frequency toward min
* 3 oscillate between the two
*
* **Direction 3 sweeps once rather than oscillating**, and that is a deviation
* worth knowing about: akgl_audio_sweep runs one pass from a start to an end,
* and a genuine oscillation needs the mixer to turn around at the endpoints.
* Sweeping once in the direction the endpoints imply is the closest honest
* approximation; TODO.md section 5 records it.
*
* Directions 1 and 2 both name `min` as the far end, so the *value* of min is
* what decides which way the pitch actually travels -- a `min` above the
* starting frequency rises whatever `dir` says. That is the SID's behaviour and
* akgl_audio_sweep works the same way, comparing its two endpoints, so the two
* agree without this having to second-guess either.
*/
static akerr_ErrorContext AKERR_NOIGNORE *sweep_note(akbasic_Runtime *obj, int voice, double hz, double *args, int ms)
{
PREPARE_ERROR(errctx);
double tohz = 0.0;
double stephz = 0.0;
double stepbase = 0.0;
PASS(errctx, akbasic_audio_register_to_hz((int)args[4], &tohz));
/*
* `step` is a register *delta*, not a register value, so it cannot go
* through register_to_hz on its own -- that maps a position, and a delta has
* no position. Convert it as the distance between register 0 and register
* `step`, which is the same linear scale the table applies to everything
* else.
*/
PASS(errctx, akbasic_audio_register_to_hz(0, &stepbase));
PASS(errctx, akbasic_audio_register_to_hz((int)args[5], &stephz));
stephz -= stepbase;
/*
* A zero step would never arrive and libakgl refuses it outright. One hertz
* is the smallest move that still gets there, and a program that asked for
* no movement asked for a held note -- which is what it gets.
*/
if ( stephz <= 0.0 ) {
stephz = 1.0;
}
PASS(errctx, obj->audio->sweep(obj->audio, voice, hz, tohz, stephz, ms));
SUCCEED_RETURN(errctx);
}
/* --------------------------------------------------------------- SOUND --- */
akerr_ErrorContext *akbasic_cmd_sound(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
@@ -110,13 +165,27 @@ akerr_ErrorContext *akbasic_cmd_sound(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
/*
* Arguments 4 through 6 are a frequency sweep: a direction, a floor and a
* step. There is no akgl_audio_* equivalent, and faking one by re-issuing
* tones from akbasic_runtime_step() would tie audible pitch to how often the
* host happens to call us -- a tune that changes key with the frame rate.
* Refused, and filed upstream as akgl_audio_sweep. TODO.md section 7.
* step. `dir` is 0 to hold, 1 to sweep up, 2 to sweep down and 3 to
* oscillate; `min` is the far end of the sweep as a register value, and
* `step` is how far the pitch moves per tick.
*
* This was refused outright until libakgl 0.3.0 grew akgl_audio_sweep --
* faking it by re-issuing tones from akbasic_runtime_step() would have tied
* audible pitch to how often the host happens to call us, a tune that
* changes key with the frame rate. It is still refused when the host's
* backend has no sweep, which is what an older one looks like.
*/
FAIL_NONZERO_RETURN(errctx, (count >= 4 && args[3] != 0.0), AKBASIC_ERR_DEVICE,
"SOUND's frequency sweep needs a device capability that does not exist yet");
if ( count >= 4 && args[3] != 0.0 ) {
FAIL_ZERO_RETURN(errctx, (args[3] >= 0.0 && args[3] <= 3.0), AKBASIC_ERR_BOUNDS,
"SOUND direction %d out of range (0 to 3)", (int)args[3]);
FAIL_ZERO_RETURN(errctx, (obj->audio->sweep != NULL), AKBASIC_ERR_DEVICE,
"SOUND's frequency sweep needs an audio device that can sweep");
FAIL_ZERO_RETURN(errctx, (count >= 6), AKBASIC_ERR_SYNTAX,
"SOUND's frequency sweep expected a direction, a minimum and a step");
PASS(errctx, sweep_note(obj, voice, hz, args, ms));
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
if ( count >= 7 ) {
/* The seventh argument selects a waveform, 0 through 3. */