- name
- dev-audio-lesson
- description
- Add an audio lesson — sound playback, mixing, spatial audio, DSP effects, with SDL GPU scenes and forge UI
- argument-hint
- [number] [topic-name] [description]
Every audio lesson produces two things: **library code** and a **demo
program**. The library — `common/audio/forge_audio.h` — is the primary
deliverable. The lessons teach concepts; the library is what remains when the
learning is done. It must be correct, efficient, tested, and safe. The demo
program visualizes and controls the audio in action, rendered with SDL GPU
and controlled through forge UI panels.
**When to use this skill:**
- You need to teach audio playback, mixing, spatial sound, or DSP effects
- A learner wants to understand PCM audio, streaming, attenuation, or filters
- The concept benefits from a visual 3D scene showing audio source positions
- New functionality needs to be added to `common/audio/forge_audio.h`
**Smart behavior:**
- Before creating a lesson, check if an existing audio lesson already covers it
- **Library first, demo second.** Design, implement, document, and test the
library code before writing a single line of the demo program. The demo
exercises the library — it does not replace it.
- Audio lessons are interactive — every concept must be audible in the running
program and controllable via forge UI
- Focus on *why* the technique works, not just the code — connect DSP math to
what the listener hears
- Use simple geometric shapes for sound source visualization — the audio is
the focus
- All scene geometry comes from `common/shapes/forge_shapes.h` — never write
inline geometry generation functions
- Cross-reference math lessons (vectors, dot products) and GPU lessons where
relevant
## Arguments
The user (or you) can provide:
- **Number**: two-digit lesson number (e.g. 01, 02)
- **Topic name**: kebab-case (e.g. audio-basics, sound-effects)
- **Description**: what this teaches (e.g. "PCM fundamentals, WAV loading,
SDL audio streams")
If any are missing, infer from context or ask.
## Steps
### 1. Analyze what's needed
- **Check existing audio lessons**: Is there already a lesson for this topic?
- **Check `common/audio/`**: Does relevant library code already exist?
- **Check `common/ui/`**: Does the UI library provide the widgets needed for
audio controls? If a required widget is missing, note it.
- **Identify the scope**: What specific audio concepts does this lesson cover?
- **Find cross-references**: Which math/GPU/UI lessons relate?
- **Check PLAN.md**: Where does this lesson fit in the audio track?
### 2. Create the lesson directory
`lessons/audio/NN-topic-name/`
With subdirectories:
```text
lessons/audio/NN-topic-name/
main.c
CMakeLists.txt
README.md
audio.conf (gitignored — local path to audio files)
assets/
screenshot.png (lesson screenshot — checked in)
```
Baseline rendering shaders (shadow, scene, grid, sky, UI) are provided by
`forge_scene.h` — no per-lesson copies needed. If the lesson introduces a
topic-specific shader (e.g. a waveform visualization pass), add a `shaders/`
subdirectory for those files only.
### 3. Design and implement the audio library code
Every audio lesson adds to `common/audio/forge_audio.h`. This is not
optional — it is the primary deliverable. The demo program exists to exercise
and visualize the library; the library is what ships.
For the first audio lesson, create the file. For subsequent lessons, extend
it. Design the API before writing the demo. The demo calls the library —
never the reverse.
#### Library standards
**Correctness:**
- Every function must implement a named, well-understood technique. Cite the
source in the doc comment — a textbook, paper, or DSP reference.
- Audio processing must not introduce clicks, pops, or DC offset. Transitions
(volume changes, crossfades) must be smooth.
- Sample rate conversion and channel mapping must be handled correctly via
SDL audio stream properties.
- Buffer sizes and sample counts must be computed correctly — off-by-one
errors in audio produce audible artifacts.
**Thread safety:**
- SDL audio streams deliver data on a separate audio thread. Any shared state
between the main thread and audio processing must use SDL atomics or
SDL mutexes.
- Volume, panning, and effect parameters should be set atomically so partial
updates don't produce glitches.
- Resource lifetimes must be clear — never free audio data while a stream
might still reference it.
**Performance:**
- Audio processing runs in real time — dropped samples cause audible glitches.
Keep per-sample operations simple (multiply, add, table lookup).
- Precompute coefficients (filter taps, attenuation curves, panning tables)
at init time or when parameters change, not per-sample.
- Use `float` for all audio processing. Convert to/from integer formats only
at the SDL boundary.
**Numerical safety:**
- Clamp output samples to `[-1.0f, 1.0f]` before delivery to prevent
clipping distortion beyond what the user intends.
- Guard against division by zero in attenuation calculations (zero distance).
- Filter coefficients must be validated to prevent instability (poles inside
the unit circle for IIR filters).
**Header-only implementation:**
- `static inline` for all functions — no separate `.c` compilation unit
- Guard with `#ifndef FORGE_AUDIO_H` / `#define` / `#endif`
- Include only SDL3, `"math/forge_math.h"`, and standard C headers
- No heap allocation in per-sample paths — allocate buffers at init time
- Deterministic: identical input samples and parameters produce identical output
**Documentation (every function, no exceptions):**
```c
/* Load a WAV file into an audio buffer.
*
* Reads the WAV file at the given path using SDL_LoadWAV and converts
* it to the specified format. The returned buffer owns its sample data
* and must be freed with forge_audio_buffer_free().
*
* Parameters:
* path — file path to the WAV file (must not be NULL)
* spec — desired output format (sample rate, channels, format)
*
* Returns:
* ForgeAudioBuffer with sample data, or a zero-initialized struct
* on failure (check .data != NULL).
*
* Usage:
* ForgeAudioBuffer buf = forge_audio_load_wav("/path/to/audio/click.wav", spec);
* if (!buf.data) { SDL_Log("Failed to load WAV"); return; }
*
* See: Audio Lesson 01 — Audio Basics
* Ref: SDL3 documentation, SDL_LoadWAV
*/
```
Every doc comment must include:
- Summary (one sentence — what it does)
- Algorithm or technique being implemented
- Parameters with types, valid ranges, and nullability
- Return value (if any) with format/units
- Usage example
- Cross-reference to the lesson that introduces it
- Reference to the source material
**Naming:**
- Functions: `forge_audio_verb_noun()` — e.g. `forge_audio_init()`,
`forge_audio_load_wav()`, `forge_audio_play_oneshot()`
- Types: `ForgeAudioNoun` — e.g. `ForgeAudioBuffer`, `ForgeAudioSource`,
`ForgeAudioMixer`, `ForgeAudioListener`
- Constants: `FORGE_AUDIO_UPPER` — e.g. `FORGE_AUDIO_MAX_SOURCES`
**Core types to establish in Lesson 01:**
```c
#ifndef FORGE_AUDIO_H
#define FORGE_AUDIO_H
#include <SDL3/SDL.h>
#include "math/forge_math.h"
/* --- Audio buffer (decoded sample data) --------------------------------- */
typedef struct ForgeAudioBuffer {
float *data; /* interleaved float samples */
int sample_count; /* total samples (frames * channels) */
int channels; /* 1 = mono, 2 = stereo */
int sample_rate; /* Hz — typically 44100 or 48000 */
} ForgeAudioBuffer;
/* --- Audio source (a playing instance) ---------------------------------- */
typedef struct ForgeAudioSource {
const ForgeAudioBuffer *buffer; /* sample data (not owned) */
int cursor; /* current read position in samples */
float volume; /* linear gain [0..1] */
float pan; /* stereo pan [-1..1], 0 = center */
bool looping; /* true = restart at end of buffer */
bool playing; /* true = actively producing samples */
} ForgeAudioSource;
/* ... functions grow lesson by lesson ... */
#endif /* FORGE_AUDIO_H */
```
#### Testing the library (MANDATORY)
The audio library is tested independently of the demo program. Tests validate
correctness, edge cases, and determinism. Every function added to
`forge_audio.h` must have corresponding tests in `tests/audio/test_audio.c`.
**Test categories (every function must have all applicable categories):**
1. **Basic correctness** — Known inputs produce expected outputs. For audio
processing functions, verify output samples against hand-computed values.
2. **Edge cases** — Empty buffers, zero-length audio, single-sample buffers,
zero volume, maximum volume, out-of-range panning values, mismatched
sample rates.
3. **No artifacts** — Volume transitions produce no clicks (verify samples
are continuous). Looping produces seamless playback (verify loop-point
samples match). Mixing does not overflow `[-1, 1]` without explicit
clipping.
4. **Determinism** — Two identical sources with identical parameters produce
bit-identical output.
5. **Thread safety** — Parameter changes during playback do not crash. Verify
with single-threaded simulation (no need to test actual SDL threads in
unit tests).
**Test infrastructure:**
- Test file: `tests/audio/test_audio.c`
- Register in root `CMakeLists.txt` as `test_audio`
- Use the same `ASSERT_NEAR` / test macros as `tests/math/test_math.c`
- Run: `cmake --build build --target test_audio && ctest --test-dir build -R audio`
- Tests must pass before the demo program is written
### 4. Create the demo program (`main.c`)
A focused C program that demonstrates audio concepts with both audible output
and visual feedback using SDL GPU and forge UI.
**MANDATORY: Use `forge_scene.h` for the rendering baseline.** The scene
library provides Blinn-Phong lighting, shadow mapping, grid floor, sky
gradient, FPS camera, and UI in a single `forge_scene_init()` call. This
eliminates hundreds of lines of rendering boilerplate and lets the lesson
focus on audio. See the `forge-scene-renderer` skill for the full API.
**Every audio lesson MUST include these features:**
1. **SDL GPU rendered scene via `forge_scene.h`** — A 3D scene providing
visual context for the audio. For spatial audio lessons, this means visible
sound source positions in the scene. For fundamentals lessons, a simple
scene with visual feedback (waveform visualization, VU meters as colored
geometry) is sufficient. The rendering baseline (Blinn-Phong, grid, shadow
map, depth buffer, sRGB swapchain, sky) is provided by `forge_scene.h`.
2. **First-person camera** — Provided by `forge_scene.h`. The camera position
(accessible via `forge_scene_cam_pos()`) doubles as the audio listener
position for spatial audio lessons.
3. **Forge UI panel** — Provided by `forge_scene.h` via
`forge_scene_begin_ui()` / `forge_scene_end_ui()`. Every lesson must have
at least:
- Master volume slider
- Play/pause button (or status indicator)
- Lesson-specific controls (per-source volume, panning, effect parameters)
4. **Capture support** — `forge_capture.h` integration (when `FORGE_CAPTURE`
defined) via `scripts/capture_lesson.py`.
**Audio requirements:**
- **SDL audio stream** — All audio output goes through `SDL_AudioStream`.
Create the stream at init, feed samples in the iterate callback or via
SDL's audio device callback.
- **User-supplied audio files** — Audio files are **not** checked into the
repository. Each lesson has an `audio.conf` file (gitignored) that points
to a local directory containing the user's audio files. See step 7 for the
full config workflow.
- **Real-time parameter control** — UI sliders and keys should change audio
parameters (volume, pan, effect intensity) in real time with audible
results.
**Simulation controls:**
| Key | Action |
|---|---|
| R | Reset / replay audio from start |
| P | Pause / resume audio playback |
| 1–9 | Trigger sound effects (where applicable) |
| +/- | Adjust master volume |
**Scene geometry via `forge_shapes.h` (MANDATORY):**
All scene geometry **must** come from `common/shapes/forge_shapes.h`. Use
spheres or icospheres for sound source positions, cones for directional
sources. Never write inline geometry generation functions.
**Template structure:**
```c
/*
* Audio Lesson NN — Topic Name
*
* Demonstrates: [what this shows]
*
* Controls:
* WASD / Arrow keys — move camera (listener position)
* Mouse — look around
* R — reset / replay audio
* P — pause / resume
* 1–9 — trigger sound effects
* +/- — adjust master volume
* Escape — release mouse / quit
*
* SPDX-License-Identifier: Zlib
*/
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stddef.h>
#include "math/forge_math.h"
#include "audio/forge_audio.h"
#include "shapes/forge_shapes.h"
#define FORGE_SCENE_IMPLEMENTATION
#include "scene/forge_scene.h"
/* ── Constants ────────────────────────────────────────────────────── */
#define AUDIO_SAMPLE_RATE 48000
/* ── Types ────────────────────────────────────────────────────────── */
typedef struct app_state {
ForgeScene scene; /* rendering, camera, shadow map, grid, sky, UI */
/* GPU resources — app-owned geometry */
SDL_GPUBuffer *sphere_vb, *sphere_ib;
int sphere_index_count;
/* Audio state — lesson-specific */
SDL_AudioStream *audio_stream; /* SDL audio output stream */
/* ForgeAudioBuffer buffers[N]; */
/* ForgeAudioSource sources[N]; */
float master_volume; /* linear gain [0..1] */
bool audio_paused; /* true = audio playback frozen */
} app_state;
```
**Init pattern:**
```c
SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv)
{
app_state *state = SDL_calloc(1, sizeof(*state));
if (!state) return SDL_APP_FAILURE;
*appstate = state;
ForgeSceneConfig cfg = forge_scene_default_config("Audio NN — Topic");
cfg.cam_start_pos = vec3_create(0.0f, 4.0f, 12.0f);
cfg.font_path = "assets/fonts/liberation_mono/LiberationMono-Regular.ttf";
if (!forge_scene_init(&state->scene, &cfg, argc, argv))
return SDL_APP_FAILURE;
state->master_volume = 1.0f;
/* Read audio directory from audio.conf */
char audio_dir[512];
if (!read_audio_conf("audio.conf", audio_dir, sizeof(audio_dir))) {
SDL_Log("audio.conf not found or not configured.");
SDL_Log("See the lesson README for setup instructions.");
return SDL_APP_FAILURE;
}
/* Load audio files from user's directory, set up SDL audio stream,
* upload shapes ... */
return SDL_APP_CONTINUE;
}
```
**Iterate pattern (camera position as listener):**
```c
SDL_AppResult SDL_AppIterate(void *appstate)
{
app_state *state = appstate;
ForgeScene *s = &state->scene;
if (!forge_scene_begin_frame(s)) return SDL_APP_CONTINUE;
/* Use camera position as audio listener */
vec3 listener_pos = forge_scene_cam_pos(s);
/* Shadow pass */
forge_scene_begin_shadow_pass(s);
Auf GitHub ansehen