Three-way consensus (Jay/Codex/Claude): one sound, split timing not identity. The bullseye sound now means 'centered right now' and fires the instant you're aligned - even a fast pass over center - so the zone that needs the most help locating always announces itself. It no longer waits on the velocity+dwell lock, which was the delay near center. - SurfaceTickPolicy: outside the center zone, proximity ticks (unchanged anchor scheduler). Inside, emit ALIGNED every frame -> caller loops level.wav. Entry is immediate (no dwell, no velocity gate). Alignment uses SPATIAL hysteresis (enter <=0.2, rearm only after leaving >=0.35) - not a time debounce, so a legitimate quick re-crossing still announces. Ticks resume immediately on exit. - Alignment error = hypot(stable pitch, stable roll) - the SAME axes that drive the bubble, so the sound can't lag the visual (Codex's implementation note). - Player: level.wav loops while aligned (ensureLevelLooping, idempotent), stopped before every restart and on exit; pending-load queue retained. - The held-level confirmation (persistent lime, label, haptic) stays on the velocity-aware LockDetector; the locating sound is fully decoupled from it. Tests: immediate cue on fast crossing, one start / ticks-suppressed while inside, stop+resume-ticks on exit, spatial-hysteresis rearm, tick reactivity. 81 tests passing; assembleDebug clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.9 KiB
Audio Assist — design log
Hands-free audio to level something without watching the screen. Setting cycles
OFF / TONES / VOICE from the Level header. Plays on USAGE_MEDIA (so the volume slider
controls it) and never takes audio focus.
Source of truth for the current design is [
SOUND_DESIGN_HANDOFF.md], from Jay's dedicated sound-design sessions. This log records the journey — especially rejected approaches — so they don't get rediscovered. Where the two ever disagree, the handoff wins.
Chosen direction (current)
Two modes / two mechanisms, one sound identity (all derived from a 500 Hz blip):
SURFACE (2-D) — TONES. One vocabulary: faster ticks = approaching, the bullseye sound = centered right now. Timing is split, not sound identity:
- Outside the center zone:
tick.wav(60 ms) repeats faster as you near level — axis-agnostic "warmer/colder", rate is the message. No settle gate. Rate is exponential in the error, clamped 1.5/s (≥~5°) → 8/s (≤~0.4°), hard-capped. Constant loudness (acceleration is the signal). Scheduling is anchored to the last tick off a monotonic clock, due-time recomputed each frame from the current interval → immediate reaction to changing tilt, no drift, no catch-up burst, ≤1 tick per update. - Inside the center zone: the ticks give way to the looping
level.wavimmediately — no dwell, no velocity gate — even on a fast pass, because a momentary alignment is real, locating information (that little zone needs the most help). It stops the instant you leave. Alignment uses spatial hysteresis (enter ≤0.2°, rearm only after leaving ≥0.35°) — not a time debounce, which would hide a legitimate quick re-crossing — and is measured from the same hypot(stable pitch, stable roll) that drives the bubble, so the sound can't lag the visual.level.wav's stream id is retained and stopped before every restart / on exit. - The stronger "held level" confirmation — persistent lime, the "Flat within 0.35°" label,
and the haptic — is decided separately by the velocity-aware
LockDetector(≤0.2° + ≤0.3°/s → 175 ms confirm). The locating sound never waits on it; a fast fly-through gets the bullseye cue but not the "you nailed it" confirmation.
EDGE (1-D, future) — the "sonar staircase." Direction + magnitude as a 4-blip pitch
contour (rising = raise it, falling = lower it, steepness = how far; level sounds flat), one
report per move→settle. Five pre-rendered assets staged in sonar-staircase-v3/; wired when
Edge mode is built.
VOICE — deliberately separate, a SETTLED-CHANGE announcer (not a timer). Speaks one correction, then stays silent while moving; when it settles again it speaks only if the dominant direction changed, it crossed coarse→fine, or it reached lock ("that's level"). A settle still needing the same coarse nudge gets silence. No periodic repeat, no bed under it.
Implementation: pure SurfaceTickPolicy (monotonic deadline scheduler, exponential rate,
unit-tested) + VoiceGuidancePolicy; the SoundPool layer stays dumb (play a tick / play &
stop level). Sounds are Jay's SoundQ-derived blips (tick.wav, level.wav) downsampled to
mono 44.1 kHz in res/raw.
Rejected approaches (and why)
- Two fixed proximity bands + lock ping (original). Worked but coarse; the near/close jump was audible ("a lower ding when close").
- Continuous cadence — beep interval shrinks smoothly toward center (parking-sensor). → "gets a bit much." Monotonous over a long adjustment.
- Rising-pitch discrete beeps. Better, but still same-note-ish and busy.
- Continuous glide tone — one warm tone, pitch low→treble by closeness, resolving into the aura. → "the constant sound works but it's too much; not clicking." A continuous tone gives the ear no silence to rest on, so it fatigues; it also bypassed the tested scheduler.
- Radar doublet on a per-band cadence — two sonar pings ~110 ms apart, repeating on a 1.3/0.9/0.6 s cadence while off-level. The literal two-ping "pew-pew" read as UI/radar beeps, not sonar, and the continued cadence was still a clock. Kept the bands/pitch, dropped the cadence in favor of one packet per settle.
- Continuous forcefield aura bed (looping CC0 ambience while locked). Seamless on a PC media player but clicked at the loop point through Android SoundPool. Pulled; a gapless bed needs AudioTrack/ExoPlayer.
- Settle-gated two-ding "status packet" (banded pitch + gap, one packet per move→settle, fuller arrival ding at lock). Built, unit-tested, on-device tested. Coherent, but on the bench the settle-then-report model felt sparse/laggy for the fiddly 2-D surface adjustment — you want live "warmer/colder" while you're actually moving it, not a report after you stop. Superseded by continuous ticking for Surface. (The settle-then-report shape lives on for VOICE and for future EDGE, where a discrete per-settle report fits the 1-D task.)
Guiding principles (learned)
- Cue character decides tolerance more than raw rate. As discrete report cues, ~180 ms spacing felt alarm-like and ~600 ms felt calm — but as a continuous proximity tick an 8/s (125 ms) near rate reads as pleasant acceleration, not an alarm (it's a Geiger/parking sensor, and the ear expects it to quicken as you close in). Judge by feel on the phone.
- Volume is a poor information channel (phone volume + room noise make loudness unreliable) — let rate/pitch carry the message and keep loudness constant.
- Keep the SoundPool layer dumb; put decisions in tested pure logic.
- The centered sound needs source loudness/spectrum presence, not just a bigger volume number.
- No reverb/comb tails on phone speakers — they smear and lengthen fatigue.
- Reuse the real lock state for the centered handoff; never invent a second audio threshold.