audio-design
Skill1 starsChanged 49 days agoMITView on GitHub
Designs, renders, checks and plays game sound with the engine's audio module - parametric sound effects (SfxParams, layers, sfxPresets, mutateSfx), music written as text notation (defineSong with tracks, chord symbols, drums, sections, arrangement), the pnpm audio CLI that renders MP3s plus a manifest and prints a loudness/clipping/key analysis table with preview PNGs, and the runtime AudioManager (playSfx with rate/volume/cooldown, playMusic with cross-fades, master/music/sfx mute and volume persisted, sfx preload, streamed music with delayed unload, web autoplay unlock, dev-only synth fallback, release manifest check) within the package size budget. Use when adding or changing sound effects, background music, jingles, UI clicks, mute/volume settings, or when audio is too loud, too quiet, clipping, late, off-key, repetitive or missing on a device ("sound", "sfx", "audio", "music", "bgm", "jingle", "volume", "mute", "音效", "音乐", "背景音乐", "声音", "静音", "配乐").
5 other projects use a version of this.
Adopted by
Most recently taken up by
- gamedev-skills/awesome-gamedev-agent-skillstheir own version, first seen Nov 11, 2026
- rodrigo-guerraa/roblox-hubtheir own version, first seen Oct 17, 2026
- ukanwat/aaabenchtheir own version, first seen Aug 15, 2026
- ukanwat/overtimetheir own version, first seen Aug 15, 2026
- ukanwat/selfstartertheir own version, first seen Aug 15, 2026
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 5
- gamedev-skills/awesome-gamedev-agent-skills★ 9ksame name
- ukanwat/aaabench★ 399same name
- ukanwat/overtime★ 399same name
- ukanwat/selfstarter★ 399same name
- rodrigo-guerraa/roblox-hub★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- code-artSkill
- engine-extendSkill
- game-feelSkill
- input-and-i18nSkill
- make-a-gameSkill
- performanceSkill
- physicsSkill
- release-platformsSkill
- testing-and-shotsSkill
- ui-screensSkill
The file
We split this skill into the rules it gives an agent
5 rules. 0 are common, 0 are distinctive, and 5 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 12
All names are exported from '@engine' (source engine/audio/).
- Very specific: line 18
Write the sound contract: before designing: names (file-safe, unique across sfx and music), when each plays, max length, and how it varies …
- Very specific: line 136
Call setupAudio(game) from the app's boot().
- Very specific: line 160
Keep <app>/audio/index.ts exporting only sfx and music (named or as default): with other exports the build keeps the whole library in the …
- Very specific: line 178
Don't call createAudioManager per scene: one per game (a second call replaces the first).
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Audio design
Sounds are data in <app>/audio/index.ts: sfx (SfxParams) and music (SongDef). pnpm audio --app <app>
renders them to <app>/assets/audio/*.mp3 + manifest.json and prints an analysis you can judge without
listening. At runtime the AudioManager plays the files (dev builds also synthesize missing ones from the same
library; release builds only play files).
All names are exported from '@engine' (source engine/audio/). Worked example: game/audio/index.ts
(10 sfx tuned to F major + a 44 s five-section loop) and its contract test game/audio/audio.test.ts.
Timing sounds to hits, squash and shake is covered by the game-feel skill.
Workflow
- Write the sound contract before designing: names (file-safe, unique across sfx and music), when each
plays, max length, and how it varies (e.g.
mergerate falls with fruit size). Put it in a comment at the top of<app>/audio/index.tsand in a test (step 5). - Pick one key for the music and tune pitched sfx to its scale (use
noteNameToMidi+midiToFreq), so effects and music sound like one family. - Design each sfx from a preset (
sfxPresets.coin(seed)) or from scratch with layered voices; write music as notation (grammar inreferences/sfx-and-music.md). - Render and read the analysis:
pnpm audio --app game(all),pnpm audio --app game --only "merge,bgm" --preview(quote comma lists in PowerShell). Fix every verdict that is notok, then Read.shots/audio/<name>.png(waveform, spectrogram, piano roll for songs). - Test lengths, verdicts, key and loop seam with
renderSfx/renderSong+analyzeAudio(pattern ofgame/audio/audio.test.ts), andt.played()in gameplay tests. - Play through the AudioManager; commit the rendered MP3s and manifest so builds ship them (release builds fail when the manifest misses a library sound).
Define sounds
import { defineSong, mutateSfx, sfxPresets, type SfxParams, type SongDef } from '@engine';
/** Rising square blip; layers inherit every voice param of the root. */
const jump: SfxParams = { wave: 'square', freq: 330, duty: 0.25, sustain: 0.04, decay: 0.16, slide: 24, lowpass: 3000, volume: 0.45, seed: 2 };
/** C major arpeggio: root C5 plus two later layers (E5, G5). */
const levelUp: SfxParams = {
wave: 'triangle',
freq: 523.25,
sustain: 0.05,
decay: 0.2,
volume: 0.6,
layers: [{ at: 0.08, freq: 659.26 }, { at: 0.16, freq: 783.99, decay: 0.4 }],
};
export const sfx: Record<string, SfxParams | (() => SfxParams)> = {
coin: { ...sfxPresets.coin(3), volume: 0.5 },
jump,
'level-up': levelUp,
hit: () => mutateSfx(sfxPresets.hit(2), 0.1, 5),
click: () => sfxPresets.click(1),
};
export const music: Record<string, SongDef> = {
menu: defineSong({
bpm: 100,
key: 'C major',
master: { loudness: -22 },
tracks: {
lead: { instrument: 'marimba', volume: 0.6, effects: { reverb: 0.2 } },
chords: { instrument: 'pad', volume: 0.3 },
bass: { instrument: 'bass', volume: 0.5 },
kit: { instrument: 'drums', volume: 0.35 },
},
sections: {
A: {
lead: 'E5 . G5 . C6 - B5 A5 | G5 - E5 - D5 - . . | F5 . A5 . C6 - A5 F5 | G5 - - - . . . . |',
chords: 'C | Am | F | G |',
bass: 'C3 . C3 . | A2 . A2 . | F2 . F2 . | G2 . G2 . |',
kit: 'k h s h k k s h |',
},
B: {
lead: 'A5 - G5 - E5 - C5 - | F5 - E5 - D5 - . . | E5 - C5 - A4 - C5 - | D5 - - - G4 - - - |',
chords: 'Am | F | C | G |',
bass: 'A2 . A2 . | F2 . F2 . | C3 . C3 . | G2 . G2 . |',
kit: 'k h h h s h h h |',
},
},
arrangement: ['A', 'B', 'A'],
}),
};Sfx rules of thumb: start instantly (no attack on hits), keep UI sounds under 0.1 s and frequent sounds soft
(low volume, few highs: lowpass), use slide/arp for "up = good, down = bad", layer a noise transient
(wave: 'white', short decay, bandpass) for impact, set seed so renders are stable. volume is the peak
after normalisation (default 0.8): balance sounds against each other with it.
Music rules: 30-60 s loops, 2+ sections, loop: true (default) for seamless tails, master.loudness around
-22..-24 so music sits under sfx, chord symbols never with an octave digit (G7 is a note; write Gdom7).
defineSong throws with the track and section on any notation error.
Read the analysis
pnpm audio prints one row per sound: peak, loud (loudest 400 ms), rms, crest, clip, lead/tail
silence, bright (spectral centroid), zcr, on (onsets), KB, verdict, plus key check: lines for songs
(notes outside key). Unchanged sounds show cached (--force re-renders). The command does not fail on bad
verdicts: read them.
| Verdict | Fix |
clipping / no headroom | lower volume (sfx) or master.ceiling / master.loudness (music) |
too quiet / quiet | raise volume, shorten a long quiet tail, add a brighter layer |
dense/loud (sfx) / very loud (music) | lower volume/loudness; music must not dominate sfx |
late start | remove the root voice's attack; put a short transient layer at at: 0 |
long silent tail | shorter decay, less echo/reverb |
harsh/bright | lowpass, fewer square/saw/noise highs, lower freq |
muddy | highpass, raise the pitch, less sub bass |
over-compressed (music) | lower master.loudness, fewer simultaneous loud tracks |
long for an sfx | sfx over 3 s belongs in music (loop: false for jingles) |
loop seam jump | keep loop: true; end sections so tails ring into the start |
| key check notes out of key | fix the notes, or correct key |
Play at runtime
import { createAudioManager, playSound, uiEvents, type Game } from '@engine';
import { music, sfx } from './audio/index';
export function setupAudio(game: Game): void {
createAudioManager(game, { library: { sfx, music } }); // starts loading every sfx
uiEvents.on('tap', () => playSound('click'));
}
export function onMerge(level: number, combo: number): void {
playSound('merge', { rate: 1.25 - level * 0.05 });
if (combo > 1) playSound('combo', { rate: Math.min(1.5, 1 + 0.08 * (combo - 2)), volume: 0.8 });
}- Call
setupAudio(game)from the app'sboot(). Passlibraryso dev builds can synthesize sounds missing from the manifest (release builds cannot: see below). Game code plays through the shortcutsplaySound/playSong/stopSong/isAudioMuted/setAudioMuted(no-ops when no manager exists); usegetAudioManager()only for the rest of the manager API (volumes, preload, unload). playSfx(name, { volume, rate, pitchJitter, cooldownMs, maxVoices })returnsnullwhen skipped (muted, hidden, within the 40 ms cooldown, or not loaded: the first call only starts loading). The manager preloads every sfx once the manifest is read (preloadSfx: falseopts out; then callpreload([...])yourself). Max 4 voices per sfx by default (oldest stops).playMusic(name, { fadeMs, volume, restart })cross-fades (same track again is a no-op);stopMusic(fadeMs). Music is loaded on first play with the backend hint{ stream: true }(mini-games: a plain streamed InnerAudioContext; web: the MP3 is kept encoded and only the playing track is decoded). A track that stopped or faded out is unloadedmusicUnloadMslater (default 4000;Infinitykeeps it) unless it plays again first, so switching tracks does not keep every decoded loop in memory.unload(name)frees any sound now.- Mini-games: sfx get
useWebAudioImplementcontexts (fully decoded, low latency) on wx; a looping play never reuses one of those.innerAudioOptioninapp.json(for example{ "obeyMuteSwitch": false }) is passed tosetInnerAudioOptionat startup (device behaviour unverified). - Settings:
isMuted/setMuted/toggleMute('master' | 'music' | 'sfx'),getVolume/setVolume, persisted underaudio.settings;on('settings', ...)fires on changes. Wire toggles as ingame/scenes/play.tsopenPause. - Web: the AudioContext starts suspended and unlocks on the first tap/click/key, so music requested on the title screen becomes audible at the first gesture. Hide/show (app background) pauses and resumes automatically.
- The synth fallback renders at runtime on the main thread (a song takes noticeable time on phones): it is a dev
convenience only (dev server, tests, shots). Release builds compile it out, replace the library imported by
<app>/main.tswith an empty one, and fail whenassets/audio/manifest.jsonmisses a library sound, has the wrong kind or a missing file. Runpnpm audio --app <app>after every sound change and commit the output. - Keep
<app>/audio/index.tsexporting onlysfxandmusic(named or as default): with other exports the build keeps the whole library in the release bundle (it warns).
Budget
MP3 sizes: sfx mono 64 kbps (about 1-16 KB each), music stereo 128 kbps by default (about 16 KB per second) or
kbps on the song (the Suika bgm uses 80 kbps: 44 s = 435 KB). WeChat/Douyin main packages are capped at
4 MB including code and all assets (see the release-platforms skill): keep total audio under about 1 MB, one or
two music loops, and prefer shorter loops or lower kbps over cutting sfx. pnpm audio prints the total.
Pitfalls
- A name used in
playSfxbut missing from the library/manifest fails silently: keep a contract test. - Renaming or deleting a sound: run
pnpm audiowithout--onlyso stale files are removed from the manifest. ratechanges pitch and length together; keep sounds short and pure (sines/triangles) if they are played at many rates. Douyin needs base library 2.33.0+ for playback rate.- Mini-game backends loop the whole MP3 file; the CLI fades loop edges by 3 ms, so a tiny dip at the seam is
expected there. Keep
seamJumplow in tests (< 0.05). - Don't call
createAudioManagerper scene: one per game (a second call replaces the first).
Reference (every SfxParams field, notation grammar, instruments, song options, analyzer thresholds):
references/sfx-and-music.md.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

