Synthesise tones on three voices
There was no audio API at all: SDL3_mixer was vendored and AKGL_REGISTRY_MUSIC was declared, but nothing opened a mixer, and a mixer plays recordings anyway. BASIC 7.0's SOUND, PLAY, ENVELOPE and VOL describe notes, so this adds a tone generator -- three voices, five waveforms, an ADSR envelope each, mixed to one float stream and fed to an SDL_AudioStream. The voice table works whether or not a device is open. akgl_audio_init connects it to one; a host that owns its own audio pipeline can call akgl_audio_mix instead. That is also what makes the tests deterministic: a device pulls samples on SDL's audio thread whenever it likes, so tests/audio.c mixes by hand and only opens a device in its last test. Phase is computed from the frame counter rather than accumulated, because a float increment of hz/44100 added 44100 times a second walks a held note off pitch. A voice that has never been configured defaults to a square wave at full level, since a zeroed voice would have a sustain of 0.0 and make no sound with no error to explain why. Voices summing past full scale are clamped rather than scaled, so one voice plays at the level it was asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
TODO.md
29
TODO.md
@@ -725,6 +725,35 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
of the four and the one most worth designing before writing. Tests can run under the dummy
|
||||
audio driver and assert state transitions rather than sound.
|
||||
|
||||
**Resolved.** `include/akgl/audio.h` and `src/audio.c` add a three-voice tone
|
||||
generator over `SDL_AudioStream`: `akgl_audio_init`, `_shutdown`, `_tone`,
|
||||
`_stop`, `_waveform`, `_envelope`, `_volume`, `_voice_active` and `_mix`.
|
||||
It is deliberately separate from the SDL3_mixer side of the library, which
|
||||
plays audio *assets*; nothing in `audio.c` reads a file. Decisions worth
|
||||
knowing:
|
||||
|
||||
- **The voice table works with no device open.** `akgl_audio_init` connects
|
||||
it to one; without that a host can still pull samples itself through
|
||||
`akgl_audio_mix`. That is not only for embedding — it is what makes the
|
||||
suite deterministic. A device pulls samples on SDL's audio thread whenever
|
||||
it likes, so a test that opened one and then asserted on voice state would
|
||||
be racing the callback. `tests/audio.c` mixes by hand and opens a device
|
||||
only in its last test.
|
||||
- **Phase is derived from the frame counter, not accumulated.** A float
|
||||
increment of `hz / 44100` is not exact, and adding it 44100 times a second
|
||||
walks a held note off pitch.
|
||||
- **A voice that was never configured is audible.** A zeroed voice has a
|
||||
sustain of 0.0, which is silence with no error to explain it, so the table
|
||||
defaults to a square wave held at full level.
|
||||
- **Three voices summing past full scale are clamped, not scaled**, so one
|
||||
voice plays at the level it was asked for rather than a third of it.
|
||||
- Everything that touches the table takes the stream lock when a device is
|
||||
open, since the callback reads it on another thread.
|
||||
|
||||
Still missing for a complete BASIC sound vocabulary: `FILTER` (SDL3 has no
|
||||
filter primitive; this would need writing), `TEMPO` and the `PLAY` note-string
|
||||
parser, both of which belong in the interpreter rather than here.
|
||||
|
||||
4. **No non-blocking keystroke read.** `include/akgl/controller.h` is built around SDL event
|
||||
handlers the host pumps (`akgl_controller_handle_event` and friends), which suits a game
|
||||
loop and does not suit `GET` and `GETKEY` -- those ask "is there a keystroke waiting, yes or
|
||||
|
||||
Reference in New Issue
Block a user