Files
on_the_level/SOUND_DESIGN_HANDOFF.md
T
thejayman77 36c0cfa5ab Audio: Surface proximity ticking + Voice settled-change; stage Edge staircase
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>
2026-07-17 09:43:18 -04:00

137 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 515680 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.52.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.