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>
This commit is contained in:
@@ -60,6 +60,26 @@ static akerr_ErrorContext *snd_tone(akbasic_AudioBackend *self, int voice, doubl
|
||||
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);
|
||||
@@ -128,6 +148,7 @@ akerr_ErrorContext *akbasic_audio_init_akgl(akbasic_AudioBackend *obj)
|
||||
|
||||
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;
|
||||
|
||||
@@ -27,13 +27,6 @@
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* akgl/actor.h before akgl/controller.h: controller.h declares two handler
|
||||
* function pointers taking an akgl_Actor * and includes nothing that declares
|
||||
* the type, so on its own it does not compile. Filed upstream against libakgl;
|
||||
* see the same note in src/input_akgl.c, which is where it turned up first.
|
||||
*/
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
/*
|
||||
@@ -49,28 +42,6 @@
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/frontend.h>
|
||||
|
||||
/** @brief Fill in the 2D backend's six vtable entries against an existing renderer.
|
||||
*
|
||||
* akgl_render_init2d() installs exactly these, but it also creates its own
|
||||
* window from the game properties and writes to the `camera` global, which makes
|
||||
* it part of the akgl_game_init() path -- and a host that already has a renderer
|
||||
* is not on that path. Without them akgl_text_rendertextat() dereferences a NULL
|
||||
* draw_texture and the first PRINT segfaults.
|
||||
*
|
||||
* Filed upstream: what is wanted is the vtable half of init2d on its own.
|
||||
* tests/akgl_backends.c carries the identical six lines for the identical
|
||||
* reason; delete both when it lands.
|
||||
*/
|
||||
static void install_2d_vtable(akgl_RenderBackend *backend)
|
||||
{
|
||||
backend->shutdown = &akgl_render_2d_shutdown;
|
||||
backend->frame_start = &akgl_render_2d_frame_start;
|
||||
backend->frame_end = &akgl_render_2d_frame_end;
|
||||
backend->draw_texture = &akgl_render_2d_draw_texture;
|
||||
backend->draw_mesh = &akgl_render_2d_draw_mesh;
|
||||
backend->draw_world = &akgl_render_2d_draw_world;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const char *title, int w, int h, const char *fontpath, int fontsize, FILE *mirror, FILE *in)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -106,7 +77,15 @@ akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const
|
||||
&obj->window, &obj->renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL, "Couldn't create the window: %s", SDL_GetError());
|
||||
window = obj->window;
|
||||
install_2d_vtable(obj->renderer);
|
||||
/*
|
||||
* Bind the 2D methods onto the renderer we just made. akgl_render_init2d()
|
||||
* would do this too, but it creates its own window from the game properties
|
||||
* first, which is the akgl_game_init() path a host with its own window is
|
||||
* not on. libakgl 0.3.0 split the vtable half out for exactly this caller --
|
||||
* it was API-gap item 7, and until it landed these six pointers were
|
||||
* assigned by hand here and in tests/akgl_backends.c.
|
||||
*/
|
||||
PASS(errctx, akgl_render_bind2d(obj->renderer));
|
||||
|
||||
obj->font = TTF_OpenFont(fontpath, (float)fontsize);
|
||||
FAIL_ZERO_RETURN(errctx, (obj->font != NULL), AKGL_ERR_SDL,
|
||||
|
||||
@@ -16,15 +16,6 @@
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* akgl/actor.h before akgl/controller.h, and it is not optional: controller.h
|
||||
* declares two handler function pointers taking an akgl_Actor * and includes
|
||||
* nothing that declares the type, so on its own it does not compile. Not a
|
||||
* workaround anybody should copy -- filed upstream against libakgl, whose own
|
||||
* style rules call for self-contained headers. Delete this note and the include
|
||||
* when controller.h includes what it uses.
|
||||
*/
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
102
src/sink_akgl.c
102
src/sink_akgl.c
@@ -13,19 +13,11 @@
|
||||
* equivalent until then.
|
||||
*/
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* akgl/actor.h before akgl/controller.h, and it is not optional: controller.h
|
||||
* declares two handler function pointers taking an akgl_Actor * and includes
|
||||
* nothing that declares the type. Filed upstream; see the same note in
|
||||
* src/input_akgl.c, which is where it was found first.
|
||||
*/
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/text.h>
|
||||
@@ -152,47 +144,83 @@ static void echo_line(akbasic_AkglSink *state)
|
||||
state->echolen = state->editlen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fold one keycode into the line being typed.
|
||||
*
|
||||
* The keycodes are SDL's, and an unshifted printable key carries its own ASCII
|
||||
* value, which is why the range test below is all the translation there is.
|
||||
* Shifted characters are unreachable: the ring carries no modifier state. See
|
||||
* akbasic_sink_akgl_set_pump() in akgl.h.
|
||||
*/
|
||||
static void edit_key(akbasic_AkglSink *state, int keycode, bool *submitted)
|
||||
/** @brief Append one byte to the line being typed, if there is room for it. */
|
||||
static void edit_append(akbasic_AkglSink *state, char c)
|
||||
{
|
||||
if ( keycode == '\r' || keycode == '\n' ) {
|
||||
if ( state->editlen >= (int)sizeof(state->editline) - 1 ) {
|
||||
/* Full. Dropped silently, exactly as a C128's 80-character limit does. */
|
||||
return;
|
||||
}
|
||||
state->editline[state->editlen] = c;
|
||||
state->editlen += 1;
|
||||
state->editline[state->editlen] = '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fold one keystroke into the line being typed.
|
||||
*
|
||||
* **The composed text is preferred over the keycode wherever there is any**, and
|
||||
* that is the whole reason libakgl 0.3.0's akgl_Keystroke exists. A keycode
|
||||
* cannot express a shifted character, a keyboard layout, a compose key or a dead
|
||||
* key; SDL has already worked all of that out by the time the ring sees it, and
|
||||
* `text` is the answer. Before 0.3.0 this took a bare keycode, folded letters to
|
||||
* upper case, and could not type a double quote -- which meant a BASIC string
|
||||
* literal could not be typed at the window at all.
|
||||
*
|
||||
* The keycode is still what identifies the editing keys, because Return,
|
||||
* Backspace and Escape are keys rather than characters and several of them
|
||||
* compose to text SDL would otherwise hand straight through.
|
||||
*/
|
||||
static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted)
|
||||
{
|
||||
size_t i = 0;
|
||||
|
||||
if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER ||
|
||||
key->key == '\r' || key->key == '\n' ) {
|
||||
*submitted = true;
|
||||
return;
|
||||
}
|
||||
if ( keycode == '\b' || keycode == 0x7f ) {
|
||||
if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) {
|
||||
if ( state->editlen > 0 ) {
|
||||
/*
|
||||
* One *byte* at a time, which is wrong for a multi-byte character and
|
||||
* is left that way deliberately: every character a BASIC program can
|
||||
* hold is one byte (section 1.2's inline string), so a multi-byte
|
||||
* character cannot survive being submitted anyway. Erasing what was
|
||||
* accepted is consistent with that.
|
||||
*/
|
||||
state->editlen -= 1;
|
||||
state->editline[state->editlen] = '\0';
|
||||
echo_line(state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ( keycode == 0x1b ) {
|
||||
if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) {
|
||||
state->editlen = 0;
|
||||
state->editline[0] = '\0';
|
||||
echo_line(state);
|
||||
return;
|
||||
}
|
||||
if ( keycode < 0x20 || keycode > 0x7e ) {
|
||||
/* A cursor or function key. Not an editing command here; a script's own
|
||||
* GET loop is what wants those. */
|
||||
|
||||
if ( key->text[0] != '\0' ) {
|
||||
for ( i = 0; key->text[i] != '\0' && i < sizeof(key->text); i++ ) {
|
||||
/*
|
||||
* Printable ASCII only. The grid is a byte per cell and a value's
|
||||
* string is a fixed 256 bytes, so a multi-byte character has nowhere
|
||||
* to go -- dropping it is honest where storing half of it is not.
|
||||
*/
|
||||
if ( (unsigned char)key->text[i] >= 0x20 && (unsigned char)key->text[i] < 0x7f ) {
|
||||
edit_append(state, key->text[i]);
|
||||
}
|
||||
}
|
||||
echo_line(state);
|
||||
return;
|
||||
}
|
||||
if ( state->editlen >= (int)sizeof(state->editline) - 1 ) {
|
||||
/* Full. Dropped silently, exactly as a C128's 80-character limit does. */
|
||||
return;
|
||||
}
|
||||
state->editline[state->editlen] = (char)toupper(keycode);
|
||||
state->editlen += 1;
|
||||
state->editline[state->editlen] = '\0';
|
||||
echo_line(state);
|
||||
|
||||
/*
|
||||
* No composed text: a cursor key, a function key or a bare modifier. Not an
|
||||
* editing command here -- a script's own GET loop is what wants those.
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,15 +234,21 @@ static void edit_key(akbasic_AkglSink *state, int keycode, bool *submitted)
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *edit_loop(akbasic_AkglSink *state, bool *eof)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Keystroke key;
|
||||
bool submitted = false;
|
||||
bool available = false;
|
||||
bool running = true;
|
||||
int keycode = 0;
|
||||
|
||||
while ( !submitted ) {
|
||||
PASS(errctx, akgl_controller_poll_key(&keycode, &available));
|
||||
/*
|
||||
* poll_keystroke rather than poll_key: the same ring, with the modifier
|
||||
* state and the composed text still attached. poll_key is the reduced
|
||||
* view, and it is what GET and GETKEY still use -- a script asking "was
|
||||
* the up arrow pressed" wants a keycode, not the empty string.
|
||||
*/
|
||||
PASS(errctx, akgl_controller_poll_keystroke(&key, &available));
|
||||
if ( available ) {
|
||||
edit_key(state, keycode, &submitted);
|
||||
edit_key(state, &key, &submitted);
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user