| 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:
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):
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:
#ifndef FORGE_AUDIO_H
#define FORGE_AUDIO_H
#include <SDL3/SDL.h>
#include "math/forge_math.h"
typedef struct ForgeAudioBuffer {
float *data;
int sample_count;
int channels;
int sample_rate;
} ForgeAudioBuffer;
typedef struct ForgeAudioSource {
const ForgeAudioBuffer *buffer;
int cursor;
float volume;
float pan;
bool looping;
bool playing;
} ForgeAudioSource;
#endif
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):
-
Basic correctness — Known inputs produce expected outputs. For audio
processing functions, verify output samples against hand-computed values.
-
Edge cases — Empty buffers, zero-length audio, single-sample buffers,
zero volume, maximum volume, out-of-range panning values, mismatched
sample rates.
-
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.
-
Determinism — Two identical sources with identical parameters produce
bit-identical output.
-
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:
-
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.
-
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.
-
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)
-
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:
#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"
#define AUDIO_SAMPLE_RATE 48000
typedef struct app_state {
ForgeScene scene;
SDL_GPUBuffer *sphere_vb, *sphere_ib;
int sphere_index_count;
SDL_AudioStream *audio_stream;
float master_volume;
bool audio_paused;
} app_state;
Init pattern:
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;
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;
}
return SDL_APP_CONTINUE;
}
Iterate pattern (camera position as listener):
SDL_AppResult SDL_AppIterate(void *appstate)
{
app_state *state = appstate;
ForgeScene *s = &state->scene;
if (!forge_scene_begin_frame(s)) return SDL_APP_CONTINUE;
vec3 listener_pos = forge_scene_cam_pos(s);
forge_scene_begin_shadow_pass(s);