Skip to content

Commit 1c0269a

Browse files
committed
fix: replace threshold-only handshake detection with SNR-based detector to prevent false triggers from ambient noise
1 parent 8cf9525 commit 1c0269a

1 file changed

Lines changed: 44 additions & 3 deletions

File tree

src/services/fskDecoder.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
7492
function 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

Comments
 (0)