AKGL_AUDIO_WAVE_SINE = 4 /**< No SOUND equivalent. A pure tone. */
} akgl_AudioWaveform;
```
| BASIC 7.0 | libakgl | Notes |
|---|---|---|
| `SOUND v, f, d` | `akgl_audio_tone(voice, hz, ms)` | One pitch, held for a gate length |
| `SOUND v, f, d, dir, min, step` | `akgl_audio_sweep(voice, from_hz, to_hz, step_hz, ms)` | Direction comes from the two frequencies, not from a sign |
| `SOUND v, f, d, , , , wf` | `akgl_audio_waveform(voice, waveform)` | Same five shapes, same first four numbers |
| `ENVELOPE n, a, d, s, r` | `akgl_audio_envelope(voice, attack, decay, sustain, release)` | Set per voice rather than per numbered preset |
your own audio pipeline -> akgl_audio_mix(your buffer, frames)
| no device is ever opened here
```
**Do not use both.** `akgl_audio_mix` mutates the voice table — it advances each voice's
frame counter and clears `active` on voices that have finished — and **it does not take the
stream lock.** Every other entry point does, when there is a stream to lock: the internal
`lock_voices` is a no-op with no device open, which is exactly right for path B and exactly
wrong if you mix the two. Calling `akgl_audio_mix` yourself while a device opened by
`akgl_audio_init` is running is two threads advancing the same voices.
Path B is what a host with its own audio pipeline wants, and it is how `tests/audio.c`
exercises the synthesis without depending on a sound card's timing:
```c
#include <akerror.h>
#include <akgl/audio.h>
#define HOST_FRAMES 512
/* A host that owns its own audio pipeline pulls samples itself and never
calls akgl_audio_init. Do not do both: this path does not take the
stream lock. */
akerr_ErrorContext *host_fill(float32_t *dest)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_audio_mix(dest, HOST_FRAMES));
SUCCEED_RETURN(errctx);
}
```
Samples are single-precision, one channel, in the range -1.0 to 1.0. `dest` is **overwritten,
not accumulated into**, and the frame count is trusted rather than checked against anything —
`akgl_audio_mix` raises `AKERR_NULLPOINTER` for a `NULL` destination and `AKERR_OUTOFBOUNDS`
for a negative count, and that is the extent of the validation. A count of 0 is a no-op.
Three voices at full level can sum past full scale. The mix is **clamped** rather than scaled
by the voice count, which keeps a single voice at the level it was asked for instead of a
third of it.
### Sample rate, mix size, and the numbers
```c excerpt=include/akgl/audio.h
/** @brief Sample rate of the generated stream, in frames per second. */
#define AKGL_AUDIO_SAMPLE_RATE 44100
/** @brief Frames the device callback generates per pass through the mixer. */
#define AKGL_AUDIO_MIX_FRAMES 512
```
Durations are milliseconds everywhere in the API and frames everywhere inside it. The
conversion truncates, so a duration under about 0.023 ms rounds to nothing. There is **no
upper bound on frequency**: a frequency above half the sample rate aliases rather than being
refused.
`akgl_audio_shutdown` closes the device and resets the voice table to its defaults —
including the master volume, back to 1.0 — so a later `akgl_audio_init` starts from a known
state. It is safe with no device open and safe to call twice. `akgl_audio_init` is a no-op
when a device is already open, and resets the voice table **only on the first call**, so it
does not silence a voice that is already sounding.
### What raises what
| Status | When |
|---|---|
| `AKERR_OUTOFBOUNDS` | A voice index outside 0–2; a non-positive frequency or sweep step; a zero `ms`; a `sustain` or `volume` outside 0.0–1.0; a waveform outside the five; a negative frame count |
| `AKERR_NULLPOINTER` | `NULL` passed to `akgl_audio_voice_active`'s `active` or `akgl_audio_mix`'s `dest` |
| `AKGL_ERR_SDL` | `akgl_audio_init` cannot open or resume a playback device. The message carries `SDL_GetError()` |
A zero-length tone is **refused** rather than treated as "stop", because `akgl_audio_stop`
already means that.
## Part 2 — the asset path
One function:
```c
#include <akerror.h>
#include <akgl/assets.h>
#include <akgl/game.h>
/* akgl_game_init creates akgl_mixer; this needs it. Nothing else in the
library calls akgl_load_start_bgm, and there is no music registry entry. */
akerr_ErrorContext *start_music(char *path)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_load_start_bgm(path));
SUCCEED_RETURN(errctx);
}
```
It loads `fname` through `akgl_mixer`, creates a `MIX_Track` for it, stores that track in
`akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM]`, publishes the audio as `akgl_bgm`, and starts
playback.
**Which formats work is SDL3_mixer's question, not libakgl's** — see the
[SDL3_mixer documentation](https://wiki.libsdl.org/SDL3_mixer/). libakgl passes the path
through verbatim; it is not resolved against `SDL_GetBasePath()`, so a relative path is
relative to the process working directory. Loading is not deferred: `MIX_LoadAudio(..., true)`
predecodes, so a large file costs its decode time here rather than at first play.
`akgl_game.h` reserves slot 1 of a 64-slot track table for the music and leaves the rest to
you:
```c excerpt=include/akgl/game.h
/** @brief Slot in ::akgl_tracks reserved for background music. Note that slot 0 is unused. */
#define AKGL_GAME_AUDIO_TRACK_BGM 1
/** @brief Size of the ::akgl_tracks table. Every simultaneous sound needs its own slot. */
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
```
### Four honest statements about this half
**It requires `akgl_game_init`, not `akgl_audio_init`.** `akgl_mixer` is created by
`akgl_game_init` at `src/game.c:250`, and it is the only place in the tree that creates it.
`assets.h:17-18` says "`akgl_game_init` (or a bare `akgl_audio_init`) has to have run first";
that is wrong. `akgl_audio_init` opens the *synthesizer's*`SDL_AudioStream` and never
touches `akgl_mixer`, so calling only that leaves `akgl_load_start_bgm` handing `NULL` to
`MIX_LoadAudio`.
**The music does not loop.** `src/assets.c:20` initialises `bgmprops` to 0,
`src/assets.c:43` sets `MIX_PROP_PLAY_LOOPS_NUMBER` on it, and `src/assets.c:45` plays the
track with it. **0 is SDL's "no property set" sentinel**, not a set this function owns, so the
write is rejected — unchecked — and the play call is given no options. The music plays once
and stops, and `akgl_load_start_bgm` reports success either way. `TODO.md` item 19; the fix
is `SDL_CreateProperties()` into `bgmprops`, checked, and destroyed in `CLEANUP`.
**`AKGL_REGISTRY_MUSIC` exists and nothing populates it.** `akgl_registry_init_music()`
creates the property set and `akgl_game_init` calls it, but no function in the library ever
writes an entry. If you want music by name, that registry is yours to fill and yours to read
— through `SDL_SetPointerProperty` and `SDL_GetPointerProperty` directly, as
[Chapter 6](06-the-registry.md) describes. There is no `akgl_music_*` API.
**There are no audio fixtures anywhere in this repository.** Not one `.wav`, `.ogg`, `.mp3`,
`.flac` or tracker module, in `tests/assets/` or `util/assets/` or anywhere else. Nothing in
the tree calls `akgl_load_start_bgm` either. **This function has no test coverage at all**,
which is worth knowing before you build on it: the loop defect above was found by reading
the code, and a test would have caught it.
### The overwrite and the leak on the way out
`akgl_load_start_bgm` overwrites `akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM]` without releasing
whatever was there, so calling it twice leaks a `MIX_Track`. On a failure *after* the audio
loads, the audio is destroyed again before the error is returned — but the track, if one was
created, is not. Both are startup-helper behaviour and both are visible in the twelve lines
of `src/assets.c`; treat this as a "call it once at startup" function, which is what its
`@brief` says it is.
## Which one do I want?
**Sound effects for a game you are writing from scratch: the synthesizer.** No files, no
formats, no decode cost, no licensing question about the samples, and a laser is four lines.
**Music, or anything recorded: SDL3_mixer.** Either through `akgl_load_start_bgm` for the one
background track, or by using `akgl_mixer` directly with SDL3_mixer's own API — `akgl_mixer`
and `akgl_tracks` are public globals for exactly that reason, and there is nothing libakgl