Skip to content

Commit 86ccba8

Browse files
committed
feat: Add bandpass filter (150-800 Hz) for breathing/snoring isolation
Based on respiratory sound research: - Normal breathing: 100-1500 Hz (peak 400-800 Hz) - Snoring: 20-800 Hz (fundamental 40-100 Hz, peaks at 300-520 Hz) - Heart sounds to filter: <100 Hz Filter settings (research-backed 'goldilocks' range): - Low cutoff: 150 Hz (filters heart sounds) - High cutoff: 800 Hz (captures breathing + snoring harmonics) - Q factor: 0.7 (wide enough for natural variation) - Center frequency: ~346 Hz (geometric mean) Sources: PMC2990233, PMC8448177, Stevens Institute sleep monitoring
1 parent 87c09cf commit 86ccba8

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

services/sleep.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,15 @@ const MOVEMENT_SPIKE_THRESHOLD = 3.0;
8484
const HR_MOVEMENT_DELTA_THRESHOLD = 8;
8585
const HR_MOVEMENT_WINDOW_SEC = 30;
8686

87+
const BANDPASS_LOW_FREQ = 150;
88+
const BANDPASS_HIGH_FREQ = 800;
89+
const BANDPASS_Q = 0.7;
90+
8791
let currentSession: SleepSession | null = null;
8892
let recentHRSamples: { hr: number; timestamp: number }[] = [];
8993
let hrMovementScore = 0;
9094
let audioContext: AudioContext | null = null;
95+
let bandpassFilter: BiquadFilterNode | null = null;
9196
let analyzer: MeydaAnalyzerInstance | null = null;
9297
let mediaStream: MediaStream | null = null;
9398
let isAudioRunning = false;
@@ -378,9 +383,17 @@ async function startWebAudioDetection(): Promise<boolean> {
378383
audioContext = new AudioContext();
379384
const source = audioContext.createMediaStreamSource(mediaStream);
380385

386+
const centerFreq = Math.sqrt(BANDPASS_LOW_FREQ * BANDPASS_HIGH_FREQ);
387+
bandpassFilter = audioContext.createBiquadFilter();
388+
bandpassFilter.type = 'bandpass';
389+
bandpassFilter.frequency.value = centerFreq;
390+
bandpassFilter.Q.value = BANDPASS_Q;
391+
392+
source.connect(bandpassFilter);
393+
381394
analyzer = Meyda.createMeydaAnalyzer({
382395
audioContext,
383-
source,
396+
source: bandpassFilter,
384397
bufferSize: 2048,
385398
featureExtractors: ['rms', 'zcr', 'spectralCentroid', 'spectralFlatness'],
386399
callback: processAudioFeatures,
@@ -408,6 +421,11 @@ function stopAudioDetection(): void {
408421
analyzer = null;
409422
}
410423

424+
if (bandpassFilter) {
425+
bandpassFilter.disconnect();
426+
bandpassFilter = null;
427+
}
428+
411429
if (mediaStream) {
412430
mediaStream.getTracks().forEach((track) => track.stop());
413431
mediaStream = null;
@@ -992,6 +1010,23 @@ export function getDetectionMode(): 'audio' | 'vitals' | 'fused' | 'none' {
9921010
return 'none';
9931011
}
9941012

1013+
export function getBandpassFilterStatus(): {
1014+
enabled: boolean;
1015+
lowFreq: number;
1016+
highFreq: number;
1017+
centerFreq: number;
1018+
q: number;
1019+
} {
1020+
const centerFreq = Math.sqrt(BANDPASS_LOW_FREQ * BANDPASS_HIGH_FREQ);
1021+
return {
1022+
enabled: bandpassFilter !== null,
1023+
lowFreq: BANDPASS_LOW_FREQ,
1024+
highFreq: BANDPASS_HIGH_FREQ,
1025+
centerFreq: Math.round(centerFreq),
1026+
q: BANDPASS_Q,
1027+
};
1028+
}
1029+
9951030
export const sleepService = {
9961031
startSession: startSleepSession,
9971032
endSession: endSleepSession,

0 commit comments

Comments
 (0)