Files
on_the_level/AUDIO_DESIGN_LOG.md
T
thejayman77 3ebb36f265 Address Codex audit of ticking (36c0cfa): dynamic + init-race fixes
P1 - Tick cadence now reacts immediately to changing tilt. The due time is
recomputed every update from the last-tick anchor plus the CURRENT interval,
so dropping from 5deg to 0.4deg accelerates to the fast cadence at once
instead of waiting out the stale slow interval. Anchor advances by whole
intervals (no drift) and resyncs to now if a full interval behind (no burst).
Added far->near and near->far transition tests.

P1 - One-shot cues no longer lost to async loading. SonarSoundPool queues a
Level asked for before its sample loads and plays it on load-complete (and
cancels the queued Level on unlock/gate). VoiceSpeaker queues the latest
phrase requested before TTS finishes init and speaks it on ready. The policy
fires these once, so they can't rely on retries.

P2 - Unlock stops the level.wav tail BEFORE playing the resumed tick (order
was reversed, allowing a brief overlap).

P2 - Audio-mode preference write moved from the screen's rememberCoroutineScope
to container.applicationScope, so a quick navigation can't cancel it (matches
the earlier persistence hardening).

Cleanup: AudioAssistMode and LevelPipeline comments now describe ticking, not
the retired radar pings/homing bands; AUDIO_DESIGN_LOG cadence principle
reconciled with the 125 ms continuous-tick near rate.

78 tests passing; assembleDebug clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:01:52 -04:00

5.2 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 = continuous proximity ticking. A tick (tick.wav, 60 ms) repeats faster as you near level — axis-agnostic "warmer/colder", the RATE is the whole message (a bubble level asks nothing of you). No settle gate — it ticks at every point in the adjustment. Rate is exponential in the stable tilt error, clamped 1.5/s (≥~5°) → 8/s (≤~0.4°), hard-capped at 8/s. Constant loudness (acceleration is the signal, not volume). On lock (reusing LockDetector.isLocked — dwell + hysteresis, no second audio threshold): ticking stops, level.wav plays once, silence while it holds; unlock resumes ticking. Scheduling is deadline-based off a monotonic clock — advances a next-tick deadline (no drift, no catch-up bursts, ≤1 tick per sensor update); re-entry (enable TONES / return FACE_UP / unlock) ticks immediately. level.wav's 1.39 s stream is retained and stopped on unlock/disable so a fresh tick can't overlap its tail.

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.