36c0cfa5ab
Replaces the hands-free audio assist after extensive on-device iteration (full journey + rejected approaches in AUDIO_DESIGN_LOG.md; current design in SOUND_DESIGN_HANDOFF.md, from Jay's sound-design sessions). Surface (TONES) = continuous proximity ticking: tick.wav repeats faster as you near level (axis-agnostic warmer/colder, the rate is the message). - Exponential rate in stable tilt error, clamped 1.5/s (>=~5 deg) to 8/s (<=~0.4 deg), hard-capped; constant loudness (acceleration is the signal). - Deadline scheduler off a monotonic clock (SystemClock.elapsedRealtime): advances a next-tick deadline, so no drift and no catch-up bursts; <=1 tick per sensor update; immediate tick on re-entry (enable/return FACE_UP/unlock). - Lock reuses LockDetector.isLocked (dwell + hysteresis, no second audio threshold): stop ticking, play level.wav once, silence while held. The 1.39s level stream is retained and stopped on unlock/disable so a fresh tick can't overlap its tail. - Pure SurfaceTickPolicy (unit-tested: anchors/clamps, monotonic rates, gating, immediate re-entry, no catch-up burst, level-once, resume on unlock). Voice (VOICE) = settled-change announcer: one correction on the dominant axis, silent while moving, speaks again on settle only if direction changed, crossed coarse->fine, or reached lock. No periodic repeat. (VoiceGuidancePolicy, unit-tested.) Assets: tick.wav + level.wav downsampled to mono 44.1kHz in res/raw (from Jay's SoundQ-derived 96k masters). AudioAssistMode adds OFF/TONES/VOICE. Retired the settle-gated two-ding packet scheduler (AudioAssistStateMachine + its ding assets) - superseded by ticking for Surface; logged as tested/rejected. Edge staircase (1-D, future): 5 pitch-contour masters staged in sonar-staircase-v3/ for when Edge mode is built. 76 tests passing; assembleDebug clean; blessed on-device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
6.7 KiB
Markdown
137 lines
6.7 KiB
Markdown
# On The Level — Audio Feedback Handoff
|
||
|
||
Handoff from the sound-design session (2026-07-16) to the app implementation.
|
||
The design is final and approved by Jay; assets are ready to ship.
|
||
|
||
Two modes, two mechanisms, **one sound identity** — every sound derives from
|
||
the same 500 Hz blip:
|
||
|
||
- **Edge mode (1-D)**: sonar staircase, one discrete report per settle event.
|
||
- **Surface mode (2-D)**: continuous proximity ticking — tick rate rises as
|
||
you approach the bullseye; on lock, `level.wav` plays as the arrival sound.
|
||
|
||
The staircase does NOT ship in surface mode. All earlier discussion of 2-D
|
||
dominant-axis mapping is dead — surface is axis-agnostic warmer/colder by
|
||
design, because leveling a surface shouldn't require decoding anything a
|
||
bubble level wouldn't ask of you.
|
||
|
||
## Surface mode: proximity ticking
|
||
|
||
Assets: `tick.wav` (in AudioGenerator's `outputs/surface-proximity/`) — the
|
||
same 500 Hz blip shortened to 60 ms with a soft release. The app plays this
|
||
single asset and schedules repetitions; rate is computed, not baked into
|
||
loops. Arrival = the shared `level.wav` (already in this repo under
|
||
`sonar-staircase-v3/`).
|
||
|
||
Rules — these are the difference between this and the old rejected
|
||
"annoying mode":
|
||
|
||
1. Map total error → tick rate **exponentially**, ~1.5 ticks/s (far) to
|
||
~8 ticks/s (near). Hard cap at 8/s; beyond ~10/s it fuses into a buzz.
|
||
2. **Loudness stays constant** as rate rises. The acceleration is the signal;
|
||
volume ramping is what turns it into a panic siren.
|
||
3. On entering the lock zone: stop ticking, play `level.wav` once, then stay
|
||
silent while level holds.
|
||
4. **Hysteresis on the lock boundary** (enter slightly inside, exit slightly
|
||
outside) so the arrival sound can't retrigger repeatedly at the threshold.
|
||
5. Ticking runs continuously, no settle gate — in this mode the realtime rate
|
||
IS the feedback. The settle-then-sound rule below applies to edge mode.
|
||
|
||
Rejected candidates kept for reference in AudioGenerator: the sustained
|
||
"energy field" loops (`bullseye-field-*.wav`) and the alternate ticks
|
||
(`outputs/tick-candidates/`). Do not ship them.
|
||
|
||
## Edge mode: the "sonar staircase"
|
||
|
||
Leveling feedback is a short train of four sonar blips whose **pitch contour is
|
||
the message**:
|
||
|
||
| Reading | Sound | Pitch steps (semitones per blip) |
|
||
|---|---|---|
|
||
| Level | flat train — all four blips at the same pitch | 0, 0, 0, 0 |
|
||
| Slightly low (raise it a little) | gentle rising staircase | 0, +1, +2, +3 |
|
||
| Very low (raise it a lot) | steep rising staircase | 0, +3, +6, +9 |
|
||
| Slightly high | gentle falling staircase | 0, −1, −2, −3 |
|
||
| Very high | steep falling staircase | 0, −3, −6, −9 |
|
||
|
||
Why this works:
|
||
- **Direction** = contour direction (rising = go higher; falling = go lower).
|
||
Never invert this mapping.
|
||
- **Magnitude** = contour steepness. Humans are poor at absolute pitch but
|
||
excellent at relative pitch, so a staircase is decodable without a reference.
|
||
- **Level literally sounds "flat."** The metaphor is self-explaining, and the
|
||
flat train doubles as the reward/confirmation state.
|
||
- **Constant rhythm at every zone.** All five sounds share the same 175 ms blip
|
||
cadence — a far-off reading is exactly as fast as a near one. Direction is
|
||
audible by blip 2 (~200 ms), full reading by ~525 ms; the rest is decay.
|
||
|
||
## When to play it (edge mode)
|
||
|
||
Staircase feedback fires **only after movement followed by settling** (same
|
||
trigger as the app's voice feedback). One staircase per settle event.
|
||
|
||
## The assets
|
||
|
||
`outputs/sonar-staircase-v3/` (copy into the app bundle):
|
||
|
||
| File | Duration |
|
||
|---|---|
|
||
| `level.wav` | 1.39 s |
|
||
| `slightly-low.wav` | 1.36 s |
|
||
| `very-low.wav` | 1.32 s |
|
||
| `slightly-high.wav` | 1.42 s |
|
||
| `very-high.wav` | 1.50 s |
|
||
|
||
All are 96 kHz stereo PCM-16 WAV, peak-normalized to 0.85. Downsampling to
|
||
44.1/48 kHz for the bundle is fine. The 500 Hz fundamental sits comfortably in
|
||
phone-speaker range, including the −9 st variant (~297 Hz).
|
||
|
||
## Anatomy of the sound (for regeneration or runtime synthesis)
|
||
|
||
- **Blip**: a single pure 500 Hz sine blip (100% tonal purity), ~165 ms
|
||
including its natural decay, extracted from Jay's licensed SoundQ file
|
||
(`~/Downloads/SoundQ Audio/UIBeep_Beep Sonar_PSE_BW-BD3HL_OmEo5.wav`,
|
||
segment 515–680 ms). 4 ms attack fade, 30 ms release fade.
|
||
- **Train**: 4 blips, onsets every 175 ms, pitched per the table above.
|
||
Pitch shifts are plain resampling (varispeed — pitch and length change
|
||
together). On Android, `SoundPool.setRate()` reproduces this exactly, and
|
||
its 0.5–2.0 range covers the full ±12 semitones used here.
|
||
- **Tail**: NOT reverb. The decay is *rhythmic*: the final blip repeats 4 more
|
||
times at the same 175 ms cadence, each repeat −8 dB, holding the last blip's
|
||
pitch (the tail "holds up the answer"). Jay specifically rejected a smooth
|
||
reverb tail — the pulse must survive into silence. This matches the source
|
||
file's own decay behavior (measured: repeats every ~180 ms, ~6 dB/repeat).
|
||
|
||
Generator script: `build_staircase.py` in this folder (AudioGenerator).
|
||
Every parameter above is a named constant at the top. Run with
|
||
`.venv/bin/python build_staircase.py`; outputs to `outputs/sonar-staircase-v3/`.
|
||
|
||
## Implementation notes
|
||
|
||
1. **Hysteresis on tier boundaries (important).** With discrete tiers, a
|
||
reading sitting at the slightly/very threshold will flip-flop between
|
||
sounds on consecutive settles and feel indecisive. Require ~15% past a
|
||
boundary before switching tiers. Same for the level/slightly boundary.
|
||
2. **Three tiers is deliberate — don't add a fourth.** Absolute-judgment
|
||
limits make >3 discrete steepness levels hard to tell apart in the field.
|
||
If finer feedback is ever wanted, synthesize the blips at runtime instead
|
||
(enveloped sine oscillators via AudioTrack or Oboe) and make steepness
|
||
*continuous* — proportional to the error angle. Continuous mapping
|
||
sidesteps the categorization limit entirely; users only compare successive
|
||
readings ("shallower than last time = converging").
|
||
3. **Interruption**: if a new settle event lands while a staircase is still
|
||
playing its tail, cut it and play the new reading — the tail is decoration,
|
||
the fresh reading is information.
|
||
4. **Haptic pairing (optional)**: a light haptic tap synced to the level.wav
|
||
blips makes the "arrived" state feel physical; the tool is usually pressed
|
||
against a surface, so it lands well.
|
||
|
||
## Licensing
|
||
|
||
The blip derives from Jay's SoundQ "Lifetime License" pack — perpetual
|
||
royalty-free use, which for SFX marketplaces normally covers embedding in
|
||
apps; standalone redistribution of the sounds is what's prohibited. If any
|
||
doubt ever arises: the blip is a mathematically pure 500 Hz sine with simple
|
||
envelopes, so a from-scratch synthesized replacement (zero licensing) is
|
||
trivial and indistinguishable — the design carries the value, not the sample.
|