3ebb36f265
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>
79 lines
5.2 KiB
Markdown
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.
|