Audio Composer — SFX + Music
Two-tier audio system: real AI-generated MP3s when ELEVENLABS_API_KEY is available, deterministic chiptune oscillator synthesis as a zero-cost fallback. The same AudioManager.js handles both transparently — no code change needed in Game.js.
When to use
Run in parallel with the art generation stages (sprite-artist, tile-artist, bg-artist), after GDD is finalized. Also run on an existing game when the user says "add sound", "add music", or "upgrade to ElevenLabs audio."
Two modes
Mode A — ElevenLabs (when ELEVENLABS_API_KEY is set)
Calls POST https://api.elevenlabs.io/v1/sound-generation for each SFX action. Uses the eleven_text_to_sound_v2 model with a genre-flavoured text prompt (e.g. "2D action platformer game, retro 8-bit jump sound, quick upward pitch sweep"). Saves MP3s to public/assets/sfx/<name>.mp3. Adds mp3Path to each entry in sfx.json. AudioManager.js preloads all MP3s as AudioBuffers via decodeAudioData and plays them on demand.
Rate limit: requests are made sequentially with automatic retry on HTTP 429.
Mode B — Web Audio oscillator (no API key, or --no-elevenlabs)
Writes oscillator parameters (freq, dur, type, vol) to sfx.json. AudioManager.js generates sound at runtime using OscillatorNode — no audio files, no network calls, works offline.
Output contract
public/assets/sfx.json — SFX descriptors (always written)
public/assets/music.json — music loop: BPM + pentatonic note sequence
public/assets/sfx/<name>.mp3 — real audio files (Mode A only)
src/audio/AudioManager.js — fixed runtime template (never regenerated)
game-state.json gains state.audio:
{
"audio": {
"sfxPath": "assets/sfx.json",
"musicPath": "assets/music.json",
"managerPath": "src/audio/AudioManager.js",
"elevenlabs": true, // true when Mode A ran
"mp3Count": 6 // number of MP3s successfully generated
}
}
sfx.json shape
Mode A (with MP3):
{
"jump": { "freq": 380, "dur": 0.14, "type": "square", "vol": 0.22, "mp3Path": "assets/sfx/jump.mp3" }
}
Mode B (oscillator only):
{
"jump": { "freq": 380, "dur": 0.14, "type": "square", "vol": 0.22 }
}
AudioManager.play('jump') checks for mp3Path first; if missing or load fails, falls back to the oscillator params. Per-sound graceful degradation — a failed ElevenLabs response for one SFX doesn't break the rest.
Genre presets
| Genre | BPM | SFX keys |
|---|---|---|
| action-platformer / platformer | 130 | jump, land, hit, pickup, death, win |
| top-down-adventure / dungeon-crawler | 100 | hit, pickup, death, win |
| top-down-rpg | 88 | pickup, hit, win |
| shoot-em-up | 160 | shoot, explosion, hit, death, win |
| beat-em-up | 140 | hit, death, win |
AudioManager usage (codesmith adds these)
import AudioManager from '../audio/AudioManager.js';
// In create():
AudioManager.init(this); // registers Safari AudioContext unlock
// In event handlers / overlaps:
AudioManager.play('jump'); // plays MP3 if available, oscillator if not
AudioManager.play('pickup');
AudioManager.play('hit');
AudioManager.play('death');
AudioManager.play('win');
AudioManager.stopMusic(); // call on game-over
// Optional:
AudioManager.mute();
AudioManager.unmute();
Safari iOS compatibility
AudioManager.init(scene) registers one-shot pointerdown and keydown listeners that create and unlock the AudioContext on first interaction. MP3 buffers are decoded at that point. This satisfies Safari iOS's user-gesture requirement without any extra plumbing in Game.js.
Process
- Read
game-state.json→gdd.genre. - Run
scripts/compose_audio.mjs <project-dir>.- If
ELEVENLABS_API_KEYis set: generate MP3s, fallback per-sound on error. - Otherwise: oscillator-only mode.
- If
- Patch
game-state.jsonwithstate.audio. - Codesmith reads
state.audioand addsimport AudioManager+AudioManager.init(this)toGame.js.
Scripts
scripts/compose_audio.mjs <project-dir> [--bpm N] [--no-elevenlabs]--no-elevenlabs— force oscillator mode even if the key is set--bpm N— override music tempo