Files
on_the_level/SOUND_DESIGN_HANDOFF.md
thejayman77 f8aa27e6cb Doc cleanup: handoff + load-listener comment match shipped bullseye audio
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>
2026-07-17 11:47:23 -04:00

7.5 KiB
Raw Permalink Blame History

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