Software
Features
Detectors

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.

FieldTypeDescription
absoluteBandPowersAbsolute power per band (µV²/Hz) — Delta, Theta, Alpha, Beta, Gamma
relativeBandPowersNormalized to sum = 1
totalPowernumberSum across all bands
dominantFrequencynumberPeak 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):

  1. Average p2p across the frontal pair (one noisy channel cannot dominate).
  2. Rolling median + MAD baseline (~3 s). Blinks are outliers, so they do not raise the gate.
  3. Threshold = max(25 µV, baseline + max(5σ, 8 µV)).
  4. 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() }

FieldTypeDescription
blinkedbooleantrue for exactly one poll cycle when a blink is confirmed
countnumberCumulative blink count
amplitudenumberCurrent peak-to-peak µV
lastBlinkTimenumberEpoch ms of last confirmed blink
baselinenumberRolling robust baseline (µV)
thresholdnumberCurrent detection threshold (µV)
warmedbooleanTrue 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).

FieldTypeDescription
focusnumberSmoothed (Beta + Gamma) / (Alpha + Theta + Delta) ratio normalized to 0–1
rawnumberUnsmoothed, uncalibrated band-power ratio
calibratedbooleanWhether 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.

FieldTypeDescription
relaxationnumberSmoothed composite alpha / theta-beta index normalized to 0–1
alphaRelativenumberRelative alpha power, Alpha / totalPower (0–1)
thetaBetaRationumberRaw Theta / Beta power ratio
calibratedbooleanWhether 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);
}, []);