Spaces:
Running
Running
File size: 2,869 Bytes
843a4b2 5eb6ab5 843a4b2 5eb6ab5 843a4b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | /**
* Timing configuration for the impulse wire code.
*
* Everything is expressed as a multiple of one base `unitMs`, the single
* speed knob. The gap hierarchy must stay well-separated so the decoder can
* bin each inter-onset interval unambiguously:
*
* dahGapMs < elemGapMs < letterGapMs < wordGapMs
*
* Defaults are deliberately *slow and robust* (large gaps) — through-air
* impulse detection with room reverb needs generous spacing. Calibration on
* real hardware can tighten these; nothing in the codec hard-codes a value.
*
* Note: the detector enforces a minimum onset separation (~150-200 ms) to
* reject reverb tails, so `dahGapMs` (the *smallest* meaningful gap, between
* the two taps of a dash) must stay comfortably above that floor.
*/
/**
* Build a full timing profile from a base unit.
*
* @param {number} unitMs base time unit in milliseconds
* @returns {{unitMs:number, dahGapMs:number, elemGapMs:number,
* letterGapMs:number, wordGapMs:number, clickMs:number,
* decisions:{elem:number, letter:number, word:number}}}
*/
export function makeTiming(unitMs = 120) {
const u = Math.max(40, Math.round(unitMs));
const dahGapMs = 2 * u; // between the two taps of a dash
const elemGapMs = 4 * u; // between elements of one letter
const letterGapMs = 8 * u; // between letters
const wordGapMs = 14 * u; // between words
return Object.freeze({
unitMs: u,
dahGapMs,
elemGapMs,
letterGapMs,
wordGapMs,
// Nominal audible click length on the speaker (synth only). The
// detector keys off onsets, so this does not affect decoding.
clickMs: Math.min(60, Math.round(u * 0.5)),
// Decision boundaries (geometric midpoints) used by the decoder to
// classify an observed inter-onset interval into a gap class.
decisions: Object.freeze({
elem: Math.round(Math.sqrt(dahGapMs * elemGapMs)), // dah|elem split
letter: Math.round(Math.sqrt(elemGapMs * letterGapMs)),
word: Math.round(Math.sqrt(letterGapMs * wordGapMs)),
}),
});
}
/**
* Named speed presets (unit in ms). Smaller = faster.
*
* Floor calibrated through-air on a MacBook (tools/calibrate_audio.py,
* 2026-06-01): unit=120 decoded 18/18, unit=90 failed because a dash's two
* taps (2×unit = 180 ms) dropped below the detector's ~200 ms reverb-rejection
* window and merged into one. So `dahGap = 2×unit` must stay comfortably above
* ~200 ms → keep unit ≳ 120. `brisk` is pinned to that proven floor; `normal`
* and `relaxed` add margin since the robot's antenna clicks reverberate more
* than a speaker blip.
*/
export const SPEED_PRESETS = Object.freeze({
relaxed: 190,
normal: 150,
brisk: 120,
});
export const DEFAULT_TIMING = makeTiming(SPEED_PRESETS.normal);
|