@@ -70,16 +70,57 @@ function detectFSKSymbol(fftData: Uint8Array, sampleRate: number): number {
7070 return bestIndex ;
7171}
7272
73- /** Checks whether a handshake tone (A or B) is dominant in the FFT data. */
73+ /**
74+ * Checks whether a handshake tone is the **dominant peak** at the target
75+ * frequency, relative to the local noise floor.
76+ *
77+ * A simple threshold (mag > 60) fails in real rooms because broadband noise
78+ * (voices, fans, music) raises all FFT bins together — the ratio stays the
79+ * same, but the absolute level exceeds 60 everywhere.
80+ *
81+ * Instead we:
82+ * 1. Measure the target bin's magnitude.
83+ * 2. Compute a local "noise floor" from bins ±30 around the target
84+ * (excluding a ±4-bin guard zone so the tone itself isn't averaged in).
85+ * 3. Require the target to be ≥ SNR_RATIO × noise floor AND above a
86+ * minimum absolute floor (so silence never falsely qualifies).
87+ */
88+ const HANDSHAKE_SNR_RATIO = 2.8 ; // Target must be 2.8× louder than local noise
89+ const HANDSHAKE_NOISE_WINDOW = 30 ; // Bins on each side sampled for noise floor
90+ const HANDSHAKE_GUARD_BINS = 4 ; // Bins around target excluded from noise floor
91+
7492function detectHandshakeTone (
7593 fftData : Uint8Array ,
7694 sampleRate : number ,
7795 handshakeFreq : number
7896) : boolean {
7997 const binSize = sampleRate / FFT_SIZE ;
8098 const bin = Math . round ( handshakeFreq / binSize ) ;
81- const mag = ( fftData [ bin - 1 ] ?? 0 ) + ( fftData [ bin ] ?? 0 ) + ( fftData [ bin + 1 ] ?? 0 ) ;
82- return mag / 3 > RX_DETECTION_THRESHOLD ;
99+
100+ // Target magnitude (average ±1 bin for pitch drift tolerance).
101+ const targetMag = (
102+ ( fftData [ bin - 1 ] ?? 0 ) +
103+ ( fftData [ bin ] ?? 0 ) +
104+ ( fftData [ bin + 1 ] ?? 0 )
105+ ) / 3 ;
106+
107+ // Absolute floor: don't trigger if the room is nearly silent (avoids
108+ // random 0-magnitude bins dividing to huge SNR).
109+ if ( targetMag < RX_DETECTION_THRESHOLD ) return false ;
110+
111+ // Local noise floor: sample surrounding bins, skip the guard zone.
112+ let noiseSum = 0 ;
113+ let noiseSamples = 0 ;
114+ for ( let i = bin - HANDSHAKE_NOISE_WINDOW ; i <= bin + HANDSHAKE_NOISE_WINDOW ; i ++ ) {
115+ if ( i < 0 || i >= fftData . length ) continue ;
116+ if ( Math . abs ( i - bin ) <= HANDSHAKE_GUARD_BINS ) continue ; // guard zone
117+ noiseSum += fftData [ i ] ;
118+ noiseSamples ++ ;
119+ }
120+ const noiseFloor = noiseSamples > 0 ? noiseSum / noiseSamples : 1 ;
121+
122+ // Only accept if the tone is clearly above the surrounding noise floor.
123+ return targetMag > noiseFloor * HANDSHAKE_SNR_RATIO ;
83124}
84125
85126/**
0 commit comments