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