feat(bin): add sfx to render notification sounds offline from TOML recipes

sfx is a single-file uv script that synthesises sounds from sine partials and seeded noise, so its output contains no samples, SoundFonts or model output, renders without a network once uv has installed numpy and mido, and comes out byte-identical on every run. Each sound in a recipe is normalised to a target loudness (ITU-R BS.1770, default -26 LUFS, the level of the macOS Glass alert) and written as WAV, with optional MP3 through ffmpeg and a MIDI file of the same notes. Unknown keys and bad note names stop the render with an error naming the table.

The new sound-design skill documents the recipe format and how to design notification sounds; link-ai-configs.sh links it for omo and Claude Code.
This commit is contained in:
David Ibia
2026-10-18 12:55:57 +01:00
parent 161d205dfd
commit 5805f45358
5 changed files with 396 additions and 4 deletions
+1
View File
@@ -17,6 +17,7 @@ OpenCode-era evaluation notes and rollback steps are in git history at
| `web-design-guidelines` | [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) (`skills/web-design-guidelines/SKILL.md`) | `063bee94c3f4df8453406c830b0a7df0f2860278` | per upstream |
| ten marketing and copy skills | [coreyhaines31/marketingskills](https://github.com/coreyhaines31/marketingskills) (`skills/`) | `b9ba399dd88b082b926e261e8ccfb843d20aa066` | MIT, copied into each skill |
| `frontend-workflow` | local | - | - |
| `sound-design` | local | - | - |
The ten marketing skills are `product-marketing`, `copywriting`, `copy-editing`,
`emails`, `cold-email`, `content-strategy`, `seo-audit`, `ai-seo`, `launch` and
+61
View File
@@ -0,0 +1,61 @@
---
name: sound-design
description: Creates original notification sounds, UI sounds, short sound effects and MIDI offline with the sfx recipe renderer, so the audio contains no third-party material and works on macOS and Linux. Use when asked to make, tweak or wire up a ding, chime, alert, notification or effect sound, including omo's own notification sounds.
---
# Sound design with sfx
`sfx` (`~/.local/bin/sfx`, a uv script in the dotfiles) renders sounds from a TOML recipe. Everything is synthesised from sine partials and seeded noise, and the same recipe renders to byte-identical files on every run and machine. `sfx --help` lists the options.
## Rules
- Synthesise only. Never use samples, SoundFonts, sound libraries, online generators or AI audio models: the point is audio with no third-party material that renders without a network.
- You cannot hear. Read the numbers `sfx` prints, then ask the user to audition the file before calling a sound done, and never change a sound they approved without them listening again.
- Keep the recipe next to the rendered files and commit both. The recipe is the source.
## Workflow
1. Write or edit a recipe file (format below).
2. Run `sfx recipes.toml [OUT_DIR] [--only NAME ...]`. Files go next to the recipe unless OUT_DIR is given. The first run lets uv install numpy and mido once.
3. Check each report line: length, loudness and peak, e.g. `omo-done: 2.22s, -26.0 LUFS, peak -15.8 dBFS`. A note saying the peak ceiling held it back means the sound is quieter than the target; lower `bright` or spread the notes out.
4. Ask the user to listen with `omo-sound path/to/NAME.wav` (afplay on macOS; pw-play, paplay, aplay or ffplay on Linux), then iterate on the notes and tone.
## Recipe format
```toml
loudness = -26.0 # target LUFS for every sound (default -26, the level of the macOS Glass alert)
extras = ["mp3", "mid"] # optional: MP3 needs ffmpeg; MIDI exports the notes only
program = 11 # General MIDI program for the .mid files (default 11, vibraphone)
[sound.example-chime]
about = "what it is for"
notes = [[0.00, "G5", 0.75, -0.15], [0.11, "E6", 1.00, 0.15]] # [start s, note, velocity 0..1, pan -1..1]
tone = { bright = 0.6, decay = 1.3 }
room = { rt60 = 0.7, wet = 0.22 }
whoosh = { start = 0.0, duration = 0.38, from_hz = 300, to_hz = 5000, gain = 0.6, pan = 0.0 }
loudness = -24.0 # optional per-sound override
```
- **notes**: a note is a name (`E6`, `C#5`, `Bb4`; C4 is middle C, 60) or a MIDI number. Each one is a struck-bar ding.
- **tone**: `bright` sets how much of the metallic strike comes through (0.35 is muted, 1.0 neutral, 1.25 sparkly). `decay` scales the ring time; at 1.0 a note rings out in about two seconds.
- **room**: a small synthetic room. `rt60` is its decay time (0.05 to 5 s) and `wet` its level; set `wet = 0` for a dry sound.
- **whoosh**: band-passed noise sweeping from `from_hz` to `to_hz` (20 to 8000), for air, swishes and sent effects. A sound needs notes, a whoosh, or both.
Unknown keys and bad notes stop the render with an error naming the table and key.
## Designing notification sounds
- Keep the audible part under a second, with two or three notes. A rising contour reads as positive or as a call for attention; a falling one reads as a problem. Brighter tones feel more urgent.
- Tell events apart by contour and timbre, not just pitch: for example a rising arpeggio for done, a brighter rising pair for a question, and a muted falling pair for an error.
- Keep notes between about C5 and C7. Higher gets shrill and lower gets muddy on laptop speakers.
- Give a set of sounds one loudness and let their shape carry the meaning.
## Formats
- **WAV** (always written, 48 kHz 16-bit stereo): desktop playback and native notifications. macOS plays a custom notification sound only from inside the app bundle.
- **MP3** (`extras = ["mp3"]`): browsers. Browsers refuse to play audio until the user has interacted with the page, and web push notifications cannot carry a custom sound at all.
- **MIDI** (`extras = ["mid"]`): the same notes for a DAW. A whoosh has no MIDI form.
## omo's own sounds
`~/dotfiles/.config/omo/sounds/recipes.toml` renders `omo-done`, `omo-input` and `omo-error`. `omo-notify` (local hooks) and `omo-remote` (events from flowstate-remote) play them through `omo-sound NAME`. To change one, edit the recipe, run `sfx recipes.toml` in that directory, and the next event uses the new sound. Keep the three names, because the scripts map events to them.