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

79 lines
5.2 KiB
Markdown

# 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.