Detectors
Browser-side EEG processing hooks — React refs, zero re-renders. Read .current in your requestAnimationFrame loop.
import { useBandPowers, useBlink, useFocus, useRelax } from "../../hooks/detectors";Honest metrics — spectral band-power ratios vs. mental states: useBandPowers and useBlink measure physical signal properties (FFT spectral power in µV²/Hz and frontal corneal-retinal EOG deflections). The useFocus and useRelax hooks are shorthand heuristic names for composite spectral band-power ratios ((Beta + Gamma) / (Alpha + Theta + Delta) and Alpha / total + Theta / Beta). High-frequency power (Beta/Gamma) is strongly influenced by scalp/facial muscle activity (sEMG) and electrode placement, and Alpha power changes primarily with eyes-closed vs. eyes-open visual cortex rhythms — these ratios are useful for interactive game controls and neurofeedback exploration, not direct measures of cognitive attention or emotional relaxation.
useBandPowers(eegData, config?)
Foundation layer. Single FFT instance, averaged spectral band powers.
| Field | Type | Description |
|---|---|---|
absolute | BandPowers | Absolute power per band (µV²/Hz) — Delta, Theta, Alpha, Beta, Gamma |
relative | BandPowers | Normalized to sum = 1 |
totalPower | number | Sum across all bands |
dominantFrequency | number | Peak PSD bin (Hz) |
Config: { updateHz?, channels?, smoothing? }
useBlink(eegData, config?)
Trainless frontal EOG detector. Face-interface electrodes on Fp1/Fp2 (channels 0/1). A blink is a large common-mode corneal-retinal deflection (typically 100-500 µV p2p, 60-600 ms) against resting EEG (~20-80 µV). Peak-to-peak over 100 ms is DC-insensitive.
No prompted calibration. The detector warms up silently (~0.8 s) while a rolling robust baseline accumulates.
Per tick (~40 Hz):
- Average p2p across the frontal pair (one noisy channel cannot dominate).
- Rolling median + MAD baseline (~3 s). Blinks are outliers, so they do not raise the gate.
- Threshold =
max(25 µV, baseline + max(5σ, 8 µV)). - Onset above threshold, offset below a hysteresis level (0.6 of the margin), duration gate (60-600 ms), then 300 ms refractory.
Jaw / forehead EMG / head motion also produce large frontal p2p. This is not a morphology classifier.
Returns: { state, reset() }
| Field | Type | Description |
|---|---|---|
blinked | boolean | true for exactly one poll cycle when a blink is confirmed |
count | number | Cumulative blink count |
amplitude | number | Current peak-to-peak µV |
lastBlinkTime | number | Epoch ms of last confirmed blink |
baseline | number | Rolling robust baseline (µV) |
threshold | number | Current detection threshold (µV) |
warmed | boolean | True once the rolling baseline has enough history |
Prefer state.current.count over the one-shot blinked flag: miss one 25 ms poll and the flag is gone.
BioPose Recorder uses that count while Record is on: each increment writes { t, text: "blink" } onto the take. Same detector, same default channels.
Config: { channels?, threshold?, windowMs?, minDurationMs?, maxDurationMs?, refractoryMs?, pollHz? }
threshold is a fixed µV override. Omit it to keep the adaptive gate. Default channels are [0, 1] (Fp1/Fp2).
useFocus(eegData, config?)
High-to-low frequency band-power ratio — (Beta + Gamma) / (Alpha + Theta + Delta). Named focus as an interactive control shorthand; reflects relative fast-wave power (which can rise with both cortical beta activity and cranial/facial sEMG).
| Field | Type | Description |
|---|---|---|
focus | number | Smoothed (Beta + Gamma) / (Alpha + Theta + Delta) ratio normalized to 0–1 |
raw | number | Unsmoothed, uncalibrated band-power ratio |
calibrated | boolean | Whether a within-session baseline has been captured |
Config: { channels?, updateHz?, smoothing?, scaleDivisor? }
Returns: { state, calibrate(), resetCalibration(), calibrating }
useRelax(eegData, config?)
Alpha-dominance and theta-to-beta ratio composite — combines relative Alpha power (Alpha / totalPower) with the Theta / Beta ratio. Named relax as a shorthand; primarily tracks posterior/global alpha rhythm elevation (for example, closing the eyes) relative to beta power.
| Field | Type | Description |
|---|---|---|
relaxation | number | Smoothed composite alpha / theta-beta index normalized to 0–1 |
alphaRelative | number | Relative alpha power, Alpha / totalPower (0–1) |
thetaBetaRatio | number | Raw Theta / Beta power ratio |
calibrated | boolean | Whether a within-session baseline has been captured |
Config: { channels?, updateHz?, smoothing?, alphaWeight?, tbrCeiling? }
Returns: { state, calibrate(), resetCalibration(), calibrating }
Usage Pattern
All detectors use the same ref-based pattern for zero-rerender reads:
const bands = useBandPowers(eegData);
useEffect(() => {
let raf: number;
function loop() {
const alphaRel = bands.current.relative.Alpha; // 0–1 relative alpha power
const betaRel = bands.current.relative.Beta; // 0–1 relative beta power
// use band powers to drive animation, game logic, etc.
raf = requestAnimationFrame(loop);
}
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);