Sound design vocabulary
Canonical 8-cue vocabulary for UI audio in Koder products (focus / hover / press / success / error / notify / voice-wake / voice-end), with timbre family, token format, and mute contract. Pairs with voice/wake-word.kmd (handles voice-channel sound) and errors/user-facing-messages.kmd (handles error-channel pairing). Owner curates final timbre + .wav samples; Web Audio API synthesized cues ship as slice 1.
Spec — Sound design vocabulary
*tatus* v0.1.0 Draft (2026
0522). Owner sign-off required for timbre and final samples; Web Audio synth ships as slice 1.
R1 — The 8-cue catalog
The vocabulary is *losed*— only these 8 cues are canonical. A product needing a cue NOT in this list must amend the spec, not ship a one-off.
| Cue ID | Trigger | Default state | Duration target | Channel pairing |
|---|---|---|---|---|
focus |
Focus enters an interactive element via keyboard | OFF | ≤ 80 ms | none required |
hover |
Pointer enters a primary interactive element | OFF | ≤ 80 ms | none required |
press |
Pointer / key activates a button or link | OFF | ≤ 100 ms | none required |
success |
Operation completed successfully | ON | ≤ 200 ms | visual toast (errors/user |
error |
Operation failed | ON | ≤ 200 ms | visual toast (mandatory) |
notify |
Background event surfaces to the user | OFF | ≤ 250 ms | visual badge/toast |
voice-wake |
Wake-word detected, Voice mode entered | ON | ≤ 150 ms | visual mic indicator (voice/wake-word.kmd § Visual feedback) |
voice-end |
Voice mode exited (timeout / dismissal) | ON | ≤ 150 ms | mic indicator hides |
*efaults* success, error, voice-wake, voice-end are ON by default; focus, hover, press, notify are OFF. Per R4 the user can flip any of the 8 via Settings.
R2 — Timbre family
The Koder sound vocabulary is a *ingle-octave synth palette*— all cues share the same timbre, only the pitch / envelope changes. Owner curates the timbre; v0.1 advisory default is "soft sine with a short attack and ~50 ms tail." No samples ship until owner picks.
Slice 1 (this spec's first publication) ships *eb Audio API synthesized cues*using deterministic parameters (see § R3 token format). When owner ratifies the timbre, real .wav recordings replace the synth path.
R3 — Token format
Each cue is addressed by a CSS-like custom property and a SDK key:
--kds-sound-<cue>: url(/sound/<cue>.wav)
--kds-sound-<cue>-duration: <ms>
--kds-sound-<cue>-gain: <0..1>Slice 1 synth equivalents (Web Audio API):
const cues = {
focus: { freq: 660, dur: 60, type: 'sine' },
hover: { freq: 520, dur: 60, type: 'sine' },
press: { freq: 440, dur: 80, type: 'sine' },
success: { freq: 880, dur: 180, type: 'sine', glide: [880, 1320] },
error: { freq: 220, dur: 180, type: 'triangle', glide: [330, 220] },
notify: { freq: 660, dur: 220, type: 'sine', glide: [660, 880] },
'voice-wake': { freq: 880, dur: 140, type: 'sine', glide: [880, 1320] },
'voice-end': { freq: 880, dur: 140, type: 'sine', glide: [1320, 880] },
};(Numbers above are the *anonical synth slice 1*values. Real .wav ratification swaps the implementation but the public API stays.)
SDK-side:
// koder_kit
await KoderSound.play(KoderSoundCue.success);// koder_web_kit
import { KoderSound } from '@koder/web-kit';
KoderSound.play('success');R4 — Mute contract
The user controls sound playback via Settings → "Sounds":
| Setting | Default | Range |
|---|---|---|
| Master mute | OFF | ON / OFF (master overrides everything) |
| Per-cue override | (per R1 defaults) | ON / OFF per cue |
| Gain | 1.0 | 0.0 – 1.0 (global) |
Implementation rules:
- Master mute MUST be honored — even synthesized cues stop.
- Per-cue overrides MUST persist across sessions (i18n parity —
user picks once on any device, applies on every device per the sync model in
koder-app/behaviors.kmd § Settings sync). - The OS-level mute (silent switch on iOS, "do not disturb" on
Android) MUST take precedence over Koder's master mute (cannot override the OS).
- Voice cues (
voice-wake,voice-end) MUST also respect thevoice/wake-word.kmd
voice.enabledtoggle — disabling voice silences these even when the per-cue setting is ON.
R5 — A11y contract
Sound is *lways*a secondary channel. Every cue listed above MUST have a visual twin (per the persona work in accessibility/personas.kmd § P7 + P8):
| Cue | Visual twin (mandatory) |
|---|---|
focus |
Visible focus ring (existing — policies/focus-management.kmd) |
hover |
Hover state (existing) |
press |
Press state (existing) |
success |
Toast / banner — errors/user-facing-messages.kmd § success |
error |
Toast / banner — errors/user-facing-messages.kmd § error |
notify |
Badge / toast |
voice-wake |
Mic indicator — voice/wake-word.kmd § Visual feedback |
voice-end |
Mic indicator hides |
Audit fails if a cue plays without its visual twin.
R6 — Implementation surfaces
| Surface | Slice 1 (synth) | Slice 2 (samples) |
|---|---|---|
| Web (koderwebkit) | Web Audio API per § R3 | <audio src="/sound/<cue>.wav"> via CSP media-src 'self' |
| Flutter (koder_kit) | audio_session + tone generator |
assets/sound/<cue>.wav shipped in the SDK |
| Android native | SoundPool with synthesized PCM |
res/raw/<cue>.wav |
| iOS native | AVAudioEngine tone |
bundle <cue>.caf |
Cross-platform consumers SHOULD load the canonical Settings tile (KodeVoiceSettingsTile extended with the Sound group) instead of hand-rolling toggles.
R7 — Tests of the contract
| ID | Test |
|---|---|
| T1 | Calling KoderSound.play('success') with master-mute ON emits no audio. |
| T2 | Disabling voice setting silences voice-wake + voice-end even when their per-cue toggles are ON. |
| T3 | OS-level mute (silent switch / DND) suppresses every cue. |
| T4 | Every cue has its visual twin asserted (snapshot or DOM check). |
| T5 | Slice 1 synth respects the canonical frequencies / durations from § R3. |
| T6 | Settings persistence — mute survives app restart + sync across devices. |
| T7 | A consumer attempting to play a NON |
R8 — Cross-references
voice/wake-word.kmd— voice-channel sound handled by Talk Modepipeline; the two
voice-*cues here are the boundary markers.errors/user-facing-messages.kmd—errorcue MUST pair with atoast whose copy follows that spec.
accessibility/personas.kmd § P7 + P8— Deaf + situationalhearing personas — visual twin is the load-bearing channel.
koder-app/behaviors.kmd § Settings— Settings drawer "Sounds"group integration.
policies/security.kmd § media-src— selfhostedonly audiofiles; no third-party CDN.
services/ai/voice/— Voice consumer of thevoice-wake/voice-endcues.
R9 — Open questions
- Should
notifydefault ON when the OS notification permissionwas granted? (Microsoft Fluent default is ON for system
toastaccompanied sound; KDS default is OFF until user opts in.) - Should the SDK expose a
play(cue, { gain: 0.3 })runtime gainoverride, or is the user-level Settings gain the only control?
- Wave 2 — does sound vocabulary extend to *mbient music*(login
screen background loop) or stay strictly transactional? Out of scope here; tracked as potential
specs/sound/ambient.kmdfollow-up.
Sign-off
| Role | Owner |
|---|---|
| Author | @rpm (2026 |
| Timbre direction | pending owner_ |
| .wav sample production | pending_ (slice 2) |
| SDK binding | pending koderkit + koderwebkit follow-up tickets |
| Ratification | pending_ |