@shiihaa/breath-detection 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Felix Zeller / shii · haa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @shiihaa/breath-detection
2
+
3
+ Real-time breath cycle detection using microphone energy analysis, spectral centroid classification, and optional BLE heart rate data. Works in browsers and Capacitor apps.
4
+
5
+ **By [shii · haa](https://shiihaa.app)** — a breathwork and biofeedback app built by a physician.
6
+
7
+ ## Features
8
+
9
+ - **Microphone-based breath detection** — detects inhale/exhale cycles from audio energy
10
+ - **Spectral centroid classification** — distinguishes inhale (higher frequency, turbulent airflow) from exhale (lower frequency, laminar airflow)
11
+ - **Dual-path detection** — threshold-based state machine for paused breathing + peak-based fallback for continuous breathing
12
+ - **Auto-recalibration** — adapts to changing ambient noise every 10 seconds
13
+ - **Refractory period** — prevents double-counting with configurable minimum cycle gap
14
+ - **Optional BLE heart rate input** — accepts external HR data for enhanced analysis
15
+ - **TypeScript types included**
16
+ - **Zero dependencies**
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @shiihaa/breath-detection
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```javascript
27
+ import { BreathDetector } from '@shiihaa/breath-detection';
28
+
29
+ const detector = new BreathDetector();
30
+
31
+ // Listen for completed breath cycles
32
+ detector.onCycle((cycle) => {
33
+ console.log(`Breath: ${cycle.inhaleMs}ms in, ${cycle.exhaleMs}ms out`);
34
+ console.log(`Rate: ${(60000 / cycle.cycleMs).toFixed(1)} breaths/min`);
35
+ console.log(`Method: ${cycle.method}`); // 'threshold' or 'peak'
36
+ });
37
+
38
+ // Listen for real-time phase changes
39
+ detector.onPhase((event) => {
40
+ console.log(`Phase: ${event.phase}, Energy: ${event.energy.toFixed(2)}`);
41
+ });
42
+
43
+ // Start microphone
44
+ const ok = await detector.start();
45
+ if (!ok) { console.error('Mic access denied'); return; }
46
+
47
+ // Calibrate (user breathes for 6 seconds)
48
+ const cal = await detector.calibrate();
49
+ console.log(`Noise: ${cal.noiseFloor.toFixed(4)}, Breath: ${cal.breathMax.toFixed(4)}`);
50
+
51
+ // Start detection loop
52
+ detector.startDetection();
53
+
54
+ // Later: stop
55
+ detector.stop();
56
+ ```
57
+
58
+ ## How It Works
59
+
60
+ ### Detection Pipeline
61
+
62
+ ```
63
+ Microphone → FFT → Energy + Centroid → State Machine → Breath Cycles
64
+
65
+ Peak Counter (fallback)
66
+ ```
67
+
68
+ 1. **Audio Capture**: Microphone via Web Audio API (`getUserMedia`)
69
+ 2. **Spectral Analysis**: FFT computes energy in 150–2500 Hz band + spectral centroid
70
+ 3. **Energy Smoothing**: EMA filter removes noise spikes
71
+ 4. **State Machine**: Tracks phases (active → silent → active → silent = 1 cycle)
72
+ 5. **Peak Fallback**: If energy never drops to silence (continuous breathing), counts energy peaks instead — two peaks = one breath cycle
73
+ 6. **Centroid Classification**: Higher centroid → inhale (turbulent "shii..."), lower centroid → exhale (laminar "...haa")
74
+ 7. **Auto-Recalibration**: Noise floor and breath max adapt every 10s
75
+
76
+ ### Inhale vs. Exhale Classification
77
+
78
+ The spectral centroid (frequency center of mass) differs between inhale and exhale:
79
+
80
+ | Phase | Airflow | Centroid |
81
+ |-------|---------|----------|
82
+ | Inhale | Turbulent, through nasal passages | ~800–2500 Hz |
83
+ | Exhale | Laminar, relaxed | ~200–800 Hz |
84
+
85
+ This is a novel approach — to our knowledge, no other breathwork app uses spectral analysis for breath phase classification.
86
+
87
+ ## API Reference
88
+
89
+ ### `new BreathDetector(options?)`
90
+
91
+ | Option | Type | Default | Description |
92
+ |--------|------|---------|-------------|
93
+ | `fftSize` | number | 4096 | FFT window size |
94
+ | `smoothingAlpha` | number | 0.25 | Energy EMA smoothing (0–1) |
95
+ | `minCycleGapSeconds` | number | 2.5 | Minimum seconds between cycles |
96
+ | `minPhaseSeconds` | number | 1.5 | Minimum seconds per breath phase |
97
+ | `thresholdFactor` | number | 0.35 | Detection sensitivity (0=sensitive, 1=strict) |
98
+ | `freqLow` | number | 150 | Low frequency bound (Hz) |
99
+ | `freqHigh` | number | 2500 | High frequency bound (Hz) |
100
+ | `enableCentroid` | boolean | true | Enable spectral centroid classification |
101
+ | `centroidThreshold` | number | 40 | Hz difference for confident in/out labeling |
102
+
103
+ ### Methods
104
+
105
+ | Method | Returns | Description |
106
+ |--------|---------|-------------|
107
+ | `start()` | `Promise<boolean>` | Start microphone capture |
108
+ | `stop()` | `void` | Stop capture, release resources |
109
+ | `calibrate()` | `Promise<CalibrationResult>` | 6-second calibration (2s noise + 4s breath) |
110
+ | `startDetection()` | `void` | Start the detection loop |
111
+ | `stopDetection()` | `void` | Stop detection (keeps mic open) |
112
+ | `setHeartRate(bpm)` | `void` | Feed external HR for enhanced analysis |
113
+ | `getState()` | `object` | Current state (phase, energy, centroid, etc.) |
114
+
115
+ ### Events
116
+
117
+ | Event | Callback | Description |
118
+ |-------|----------|-------------|
119
+ | `onCycle(cb)` | `(cycle: BreathCycle) => void` | Full breath cycle detected |
120
+ | `onPhase(cb)` | `(event: BreathPhaseEvent) => void` | Phase change (every tick) |
121
+ | `onCalibration(cb)` | `(result: CalibrationResult) => void` | Calibration complete |
122
+ | `onEnergy(cb)` | `(energy, centroid) => void` | Raw energy + centroid per tick |
123
+
124
+ ### BreathCycle
125
+
126
+ ```typescript
127
+ {
128
+ inhaleMs: number; // Inhale duration
129
+ exhaleMs: number; // Exhale duration
130
+ holdInMs: number; // Hold after inhale (0 if none)
131
+ holdOutMs: number; // Hold after exhale (0 if none)
132
+ cycleMs: number; // Total cycle duration
133
+ peakEnergy: number; // Peak energy (0–1)
134
+ confidence: number; // Detection confidence (0–100)
135
+ labelSwapped: boolean; // Whether in/out was corrected by centroid
136
+ centroidA1: number; // Spectral centroid phase 1 (Hz)
137
+ centroidA2: number; // Spectral centroid phase 2 (Hz)
138
+ method: string; // 'threshold' or 'peak'
139
+ timestamp: number; // Completion time
140
+ }
141
+ ```
142
+
143
+ ## Use with Capacitor / iOS
144
+
145
+ On iOS, `WKWebView`'s Web Audio API `AnalyserNode` returns garbage data with `getUserMedia`. Use our companion plugin [`@shiihaa/capacitor-audio-analysis`](https://github.com/shiihaa-app/capacitor-audio-analysis) for native `AVAudioEngine` audio capture, then feed the energy values to `BreathDetector`.
146
+
147
+ ## Background
148
+
149
+ This library was extracted from [shii · haa](https://shiihaa.app), a breathwork and biofeedback app. The breath detection algorithm was developed to solve a specific challenge: real-time breath phase detection using only a smartphone microphone, without any wearable sensor.
150
+
151
+ The spectral centroid approach for inhale/exhale classification emerged from the observation that inhaled air creates turbulent flow (higher frequencies) while exhaled air creates laminar flow (lower frequencies) — a well-known principle in respiratory physiology that hadn't been applied to mobile breath detection before.
152
+
153
+ ## Related
154
+
155
+ - [`@shiihaa/capacitor-audio-analysis`](https://github.com/shiihaa-app/capacitor-audio-analysis) — Native iOS audio capture plugin (AVAudioEngine) for Capacitor
156
+ - [shii · haa](https://shiihaa.app) — The breathwork app that uses this library
157
+ - [Blog: How We Solved iOS Audio Analysis](https://shiihaa.app/#blog-audio) — Technical deep-dive
158
+
159
+ ## License
160
+
161
+ MIT © [Felix Zeller](https://linkedin.com/in/felixzeller) / [shii · haa](https://shiihaa.app)
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @shiihaa/breath-detection
3
+ * Real-time breath cycle detection using microphone energy analysis,
4
+ * spectral centroid classification, and optional BLE heart rate data.
5
+ *
6
+ * © 2026 shii · haa — Felix Zeller
7
+ * MIT License
8
+ */
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Types
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ export interface BreathDetectorOptions {
15
+ /** FFT size for audio analysis (default: 4096) */
16
+ fftSize?: number;
17
+ /** EMA smoothing alpha for energy (0-1, default: 0.25) */
18
+ smoothingAlpha?: number;
19
+ /** Minimum seconds between detected cycles (default: 2.5) */
20
+ minCycleGapSeconds?: number;
21
+ /** Minimum seconds per breath phase (default: 1.5) */
22
+ minPhaseSeconds?: number;
23
+ /** Threshold factor: 0 = very sensitive, 1 = very strict (default: 0.35) */
24
+ thresholdFactor?: number;
25
+ /** Low frequency bound in Hz (default: 150) */
26
+ freqLow?: number;
27
+ /** High frequency bound in Hz (default: 2500) */
28
+ freqHigh?: number;
29
+ /** Enable spectral centroid for inhale/exhale classification (default: true) */
30
+ enableCentroid?: boolean;
31
+ /** Centroid Hz difference needed for confident classification (default: 40) */
32
+ centroidThreshold?: number;
33
+ }
34
+
35
+ export interface BreathCycle {
36
+ /** Inhale duration in ms */
37
+ inhaleMs: number;
38
+ /** Exhale duration in ms */
39
+ exhaleMs: number;
40
+ /** Hold after inhale in ms (0 if no hold) */
41
+ holdInMs: number;
42
+ /** Hold after exhale in ms (0 if no hold) */
43
+ holdOutMs: number;
44
+ /** Total cycle duration in ms */
45
+ cycleMs: number;
46
+ /** Peak energy during this cycle (0-1 normalized) */
47
+ peakEnergy: number;
48
+ /** Detection confidence (0-100) */
49
+ confidence: number;
50
+ /** Whether inhale/exhale labels were swapped based on centroid or BLE */
51
+ labelSwapped: boolean;
52
+ /** Spectral centroid during phase 1 (Hz) */
53
+ centroidA1: number;
54
+ /** Spectral centroid during phase 2 (Hz) */
55
+ centroidA2: number;
56
+ /** Detection method: 'threshold' (normal) or 'peak' (pauseless fallback) */
57
+ method: 'threshold' | 'peak';
58
+ /** Timestamp of cycle completion */
59
+ timestamp: number;
60
+ }
61
+
62
+ export interface BreathPhaseEvent {
63
+ /** Current phase: 'idle', 'inhale', 'transition', 'exhale', 'pause' */
64
+ phase: 'idle' | 'inhale' | 'transition' | 'exhale' | 'pause';
65
+ /** Normalized energy (0-1) */
66
+ energy: number;
67
+ /** Spectral centroid in Hz */
68
+ centroid: number;
69
+ /** Time in current phase (ms) */
70
+ phaseTime: number;
71
+ }
72
+
73
+ export interface CalibrationResult {
74
+ /** Noise floor energy level */
75
+ noiseFloor: number;
76
+ /** Maximum breath energy level */
77
+ breathMax: number;
78
+ /** Whether calibration succeeded */
79
+ success: boolean;
80
+ }
81
+
82
+ export type BreathEventCallback = (cycle: BreathCycle) => void;
83
+ export type PhaseEventCallback = (event: BreathPhaseEvent) => void;
84
+ export type CalibrationCallback = (result: CalibrationResult) => void;
85
+ export type EnergyCallback = (energy: number, centroid: number) => void;
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // BreathDetector
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ export declare class BreathDetector {
92
+ constructor(options?: BreathDetectorOptions);
93
+ start(): Promise<boolean>;
94
+ stop(): void;
95
+ calibrate(): Promise<CalibrationResult>;
96
+ startDetection(): void;
97
+ stopDetection(): void;
98
+ setHeartRate(bpm: number): void;
99
+ getState(): { running: boolean; calibrated: boolean; phase: string; energy: number; centroid: number; noiseFloor: number; breathMax: number };
100
+ onCycle(callback: BreathEventCallback): void;
101
+ onPhase(callback: PhaseEventCallback): void;
102
+ onCalibration(callback: CalibrationCallback): void;
103
+ onEnergy(callback: EnergyCallback): void;
104
+ }
105
+
106
+ export default BreathDetector;
107
+
108
+ // Default export
109
+ export default BreathDetector;