f8aa27e6cb
Codex re-audit of 0cb315f was functional-green (81 tests, clean build,
device-green) with two non-runtime doc findings:
- SOUND_DESIGN_HANDOFF.md still described the retired "Level on lock,
once, then silence" behavior. Now documents immediate bullseye
alignment (enter <=0.2 / exit >=0.35 spatial hysteresis, no dwell) and
the looping level.wav, with the velocity lock kept separate for
lime/label/haptic.
- The SoundPool load listener's comment still said "one-shot Level";
updated to describe the pending-loop-start path.
No code behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
7.5 KiB
Markdown
148 lines
7.5 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; the moment you're on center, the ticks give way to
|
||
the looping `level.wav` bullseye sound. One vocabulary: faster ticks = closer,
|
||
bullseye sound = centered right now.
|
||
|
||
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 center zone: stop ticking and **immediately** start the
|
||
looping `level.wav` — no dwell, no velocity gate, even on a fast pass. A
|
||
momentary alignment is real locating information; that little zone needs the
|
||
most help. `level.wav` loops while you rest on center and stops the instant
|
||
you leave (ticks resume).
|
||
4. **Spatial hysteresis** on the zone (enter ≤0.2°, rearm only after leaving
|
||
≥0.35°) — *not* a time debounce, which would hide a legitimate quick
|
||
re-crossing. Alignment is measured from the same hypot(pitch, roll) that
|
||
drives the bubble, so the sound can't lag the visual.
|
||
5. Ticking runs continuously outside the zone, no settle gate — the realtime
|
||
rate IS the feedback. (The settle-then-sound rule applies to edge mode.)
|
||
6. The stronger **"held level" confirmation — persistent lime, the on-screen
|
||
label, and the haptic — is decided separately** by the velocity-aware lock
|
||
(≤0.2° and moving slowly for ~175 ms). The locating *sound* never waits on
|
||
it: a fast fly-through gets the bullseye cue but not the "you nailed it."
|
||
|
||
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.
|