Audio-parity TDDs (sound capture / presence + sync + acoustic similarity + loudness diff / cross-impl reference-target)
The audio sibling of specs/develop/animation-parity-tdds.kmd (visual motion) and visual-regression-tdds.kmd (static). Every Koder surface that PLAYS sound during an interaction or flow — UI feedback sounds, notification/alert chimes, error beeps, Talk Mode / voice prompts, media playback, device-event sounds (volume change, plug/unplug) — MUST ship audio-parity TDDs. Defines: (1) headless capture of a surface's audio OUTPUT during an interaction (virtual sink + monitor record), (2) the four audio comparison categories (presence/absence, AV sync, acoustic similarity, loudness), (3) a "reference-target" mode comparing against a captured reference impl (e.g. GNOME 48 event sounds) alongside self-golden, and (4) the generation contract + the audio lane of the s.khost1 AV parity engine. Together with animation-parity this makes the parity engine truly **audio-visual**. Capture/compare runs HEADLESS on s.khost1, never on the owner's laptop.
Scope
Every Koder surface that emits audio during an interaction or flow MUST ship audio-parity TDDs under <module>/tests/regression/audio/. Covered sound classes:
- UI feedback sounds — toggles, presses, drag-drop, error/decline.
- Event sounds — volume change, device plug/unplug, battery low,
screenshot shutter, notification/alert chimes.
- Voice / Talk Mode — wake-word ack, TTS prompts (
specs/voice/). - Media playback — players, video, ringtones.
Out of scope: surfaces that never emit sound; the content of user-supplied media (we test that the app plays it, not the media itself).
Two comparison modes (mirrors animation-parity § R3).
self-golden(vs this surface's own committed audio golden — catches regressions) and, under a parity program,reference-target(vs a captured reference impl, e.g. GNOME 48 — proves "sounds like / behaves like GNOME"). Per interaction, the parity manifest decides whether the same asset, the same acoustic character, or merely the same presence/timing is required (R3).
R1 — Categories (the four audio TDDs)
Category A1 — Presence / absence parity
At each interaction point, assert a sound plays iff it should — same trigger points as the reference, and silence when it must be silent:
∀ interaction i:
played = (captured RMS over i's window > noise_floor)
assert played == expected(i) # self-golden or reference
# event-sounds disabled → MUST be silent
with event_sounds = off: assert not playedCatches both regressions (a sound that vanished) and over-eager noise (a sound GNOME does NOT play at that point). Respects the desktop "event sounds" / mute setting — silence under mute is a required assertion, not an exemption.
Category A2 — AV sync parity
The sound must fire at the right moment relative to its visual event (the bridge to animation-parity M-categories):
onset_audio = first sample where RMS crosses noise_floor
onset_visual = the M-category trigger marker (animation-parity § R2.1 hook)
assert |onset_audio − onset_visual| ≤ 60ms # perceptual sync floor60 ms is the perceptual audio-video sync window. The trigger marker is the same debug-hook marker the animation prong uses, so audio and motion share one timeline.
Category A3 — Acoustic similarity
Compare the captured audio to the golden/reference by robust acoustic features, NOT raw waveform (different assets, encoders, sample rates):
feat = log-mel / MFCC / chroma fingerprint of the captured clip
assert feature_distance(feat, golden_feat) ≤ tol(mode, interaction)- self-golden — tight tol (the surface's own sound shouldn't drift).
- reference-target — the manifest (R3) picks the bar per interaction:
same-asset(tight — we ship literally GNOME's freedesktop sound),same-character(loose — a distinct Koder sound in the same role:shortpercussiverising, etc. — feature-distance within a family band),
presence-only(A3 skipped; A1+A2 govern).
- Self-hosted-first: prefer kodec for decode + feature extraction
where it has parity; chromaprint/ffmpeg are the documented fallback until kodec flips (
policies/self-hosted-first.kmd+registries/self-hosted-pairs.md).
Category A4 — Loudness / level parity
Normalized perceived loudness within range, so no UI sound is jarring:
assert |LUFS(captured) − LUFS(golden_or_target)| ≤ 3 LU
assert true_peak(captured) ≤ −1 dBTP # no clippingDefault target band from the surface's sound theme; reference-target uses the reference clip's measured LUFS. Prevents the "one sound 12 dB louder than the rest" regression.
R2 — Capture pipeline (headless, on s.khost1)
Runs on s.khost1, never the owner's laptop (
heavy-work-isolation.kmd). It is the audio lane of the same AV parity engine as animation-parity § R6 — one run captures video (wf-recorder) + audio together, frame- and sample-aligned by the shared trigger marker.
| Step | Tool | Output |
|---|---|---|
| Route surface audio | a PipeWire/PulseAudio virtual sink the harness owns | isolates the surface's output (no real speakers) |
| Record | the sink's monitor source → wav/flac (or wf-recorder's audio track) | the captured clip per interaction |
| Trigger | the animation-parity § R2.1 debug-hook marker (or a synthetic input, synthetic-input-and-virtual-devices.kmd) |
start/stop + onset timeline |
| Features | kodec (self-hosted-first) / chromaprint fallback | A3 fingerprint, A4 LUFS |
The surface is launched with PULSE_SINK / a PipeWire target pointing at the harness's virtual sink, so its audio is captured deterministically and never reaches hardware.
R3 — Reference-target mode (vs GNOME etc.)
Mirrors animation-parity § R3. A reference clip per interaction is captured once from the reference impl and committed under <module>/tests/regression/audio/__reference__/<impl>/, with a sidecar <interaction>.meta.json (impl+version, sample rate, measured LUFS, fingerprint). The surface's tests/regression/audio/parity.toml lists, per interaction: reference impl, the bar (same-asset | same-character | presence-only), category tolerances, and the M4-style phase (report-only → parity-achieved). Re-capture only on a deliberate reference-version bump (recorded in the manifest + commit).
R4 — Generation contract (extends the test-gen)
/k-test MUST emit the audio suite when a module declares it emits sound:
koder.toml[ui]withplays_audio = trueor asound_theme = "...".- A bundled sound theme /
.oga.wavassets under a `sounds` dir. - A dependency on the Koder voice SDK (
specs/voice/) or a media playersurface.
Generated tests live under <module>/tests/regression/audio/, named <interaction>-<category>.{spec.ts,dart,kt,sh}. Golden + reference clips ARE committed (golden, not artifact). For surfaces with a deterministic, synthesizable sound (a synth/cue defined in code) the generator also emits a pure test of the cue parameters (the audio analogue of the animation cairo prong) so A1/A4 can run seat-free.
R5 — Run cadence
| Trigger | Categories | Where | On failure |
|---|---|---|---|
Pre-commit (/k-commit) |
A1 + A4 pure-cue (seat-free, where available) | laptop/CI | Block commit |
| Nightly CI | A1 + A2 + A3 + A4 (self-golden) | s.khost1 | Open ticket auto |
Pre-release (/k-ship) |
all + A3/A2 reference-target | s.khost1 | Block on parity-achieved; report-only otherwise |
| Parity sweep | A1–A4 reference-target, report-only | s.khost1 | Gap report → tickets |
R6 — Reduced/accessibility & determinism
- Mute / event-sounds-off is a first-class state (A1) — every audio
TDD includes the silenced case and asserts silence.
- Determinism — capture is gated on the trigger marker, never a fixed
sleep; feature distance uses fixed-window analysis, not wall-clock. - No PII / no user media in goldens — golden/reference clips are UI
sounds only; never capture a flow that would record user audio content.
R7 — Forbidden / anti-patterns
- ❌ Comparing raw waveforms / byte-diffing audio (encoder + asset
differences) — use acoustic features (A3).
- ❌ Capturing to real hardware output — virtual sink only (R2).
- ❌ Asserting a sound "eventually" via sleep — gate on the marker (R6).
- ❌ Marking
parity-achievedwithout a committed reference clip + passingA1–A4 at the manifest's bar.
- ❌ Shipping a sound fix without an audio TDD that would have caught it
(
policies/regression-tests.kmd).
Tests of the test contract
| ID | Test |
|---|---|
| T1 | A module with [ui] plays_audio = true gets tests/regression/audio/ populated by /k-test --gen-only. |
| T2 | A1 fails when a UI sound stops playing at an interaction point (regression) AND when a sound plays where the reference is silent (over-eager). |
| T3 | A1 silenced-state: with event-sounds off, the capture is silent (RMS < floor) — fails if a sound leaks. |
| T4 | A2 fails when the chime onset drifts > 60 ms from the visual trigger marker. |
| T5 | A3 same-character: a distinct Koder sound in the same role passes; a wrong-character sound (long pad where a short tick is expected) fails the family band. |
| T6 | A4 fails on a sound mastered 6 dB louder than the theme target / clipping above −1 dBTP. |
| T7 | Reference-target A3 produces a per-interaction gap report vs __reference__/gnome-48/ (feature distance + LUFS Δ + onset Δ). |
| T8 | Self-hosted: A3A4 run via kodec when present; chromaprintffmpeg fallback only when kodec lacks parity. |
Relationship to existing specs
- Audio sibling of
animation-parity-tdds.kmd(visual motion) andvisual-regression-tdds.kmd(static) — A2 binds audio to the visual trigger timeline; together they are the AV parity engine. - Consumes
synthetic-input-and-virtual-devices.kmd— the virtualmic / device-event drivers trigger the flows that produce sound (e.g. plug a virtual BT headset → device-connect sound + audio re-route).
- Cross-references
specs/voice/wake-word.kmd— Talk Mode prompts arean audio surface covered here.
- Bound by
policies/self-hosted-first.kmd(kodec for analysis) andpolicies/heavy-work-isolation.kmd(s.khost1).
R8 — Existing live gaps (snapshot at ratification)
At ratification (2026-06-07) no Koder surface ships an audio suite. First consumers: Kolide event sounds (volumeplugnotification — GNOME parity), the voice/Talk Mode surfaces, and any media player. Track closure in meta/docs/stack/registries/audio-parity-coverage.md (new — created with this spec).
Open follow-ups
- Stand up the audio lane of the s.khost1 AV engine (virtual sink +
monitor record + kodec features) — KSTACK-179.
- Capture the first GNOME-48 event-sound reference clips (volume change,
device plug, notification).
- Extend
/k-testR4 detection + audio template emitters.