Files
on_the_level/AUDIO_DESIGN_LOG.md
T
thejayman77 0cb315fdcc Audio: instant bullseye cue on alignment, decoupled from the held lock
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>
2026-07-17 11:18:04 -04:00

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.wav immediately — 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)

  1. Two fixed proximity bands + lock ping (original). Worked but coarse; the near/close jump was audible ("a lower ding when close").
  2. Continuous cadence — beep interval shrinks smoothly toward center (parking-sensor). → "gets a bit much." Monotonous over a long adjustment.
  3. Rising-pitch discrete beeps. Better, but still same-note-ish and busy.
  4. 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.
  5. 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.
  6. 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.
  7. 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.