UNPKG

dualsense-ts

Version:

The natural interface for your DualSense and DualSense Access controllers, with Typescript

254 lines 10.4 kB
"use strict"; /** * Shake detector with frequency-band energy analysis. * * Maintains circular buffers of per-axis acceleration samples. Shake * intensity is computed via RMS of the acceleration magnitude deviation * from its mean (high-pass to remove gravity). The dominant shake * frequency is identified using the Goertzel algorithm — a single-bin * DFT that's far cheaper than a full FFT — applied to the first- * difference of individual axis signals: * * - Per-axis preserves the fundamental frequency (magnitude would * full-wave-rectify the signal, doubling the apparent frequency). * - First-difference acts as a high-pass filter, removing DC (gravity) * and low-frequency drift from arm movement. Its gain is proportional * to frequency, so the Goertzel output naturally favors oscillatory * shake signals over slow drift — no ad-hoc weighting needed. * * A 1/freq compensation is applied so the net effective weighting is * proportional to freq (diff-filter gain² ∝ freq², divided by freq), * giving moderate preference to actual shake frequencies without * completely suppressing sub-1 Hz rocking. * * `.frequency` reports the **reversal rate** (2× fundamental) — how many * direction changes per second. This matches the intuitive "how fast am * I shaking it?" mental model. `.fundamental` reports the true oscillation * frequency (one full back-and-forth cycle). * * The active flag uses a sustain counter to require several consecutive * frames above threshold before triggering, preventing transient jolts * from registering as shakes. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ShakeDetector = void 0; class ShakeDetector { /** Estimated sample rate in Hz (derived from dt). Useful for diagnostics. */ get inputRate() { return this._sampleRate; } /** Number of samples in the analysis window. Can be changed at runtime (resets state). */ get windowSize() { return this.bufX.length; } set windowSize(n) { if (n === this.bufX.length) return; this.bufX = new Float64Array(n); this.bufY = new Float64Array(n); this.bufZ = new Float64Array(n); this.head = 0; this.filled = 0; this.smoothedFreq = 0; this.sustainCounter = 0; this.active = false; this.intensity = 0; this.frequency = 0; this.fundamental = 0; this._bins = []; } /** * Current frequency spectrum — weighted power at each probed bin. * Updated every frame when intensity is above half the threshold. * Useful for visualization/diagnostics. */ get spectrum() { return this._bins; } constructor(params) { /** Whether the controller is currently being shaken (sustained). */ this.active = false; /** Shake intensity from 0 (still) to 1 (violent shake). */ this.intensity = 0; /** * Dominant shake frequency in Hz as a **reversal rate** — how many * direction changes per second (2× the fundamental oscillation * frequency). 0 when not shaking. */ this.frequency = 0; /** * Fundamental oscillation frequency in Hz — one complete back-and-forth * cycle. Equal to `frequency / 2`. 0 when not shaking. */ this.fundamental = 0; this.head = 0; this.filled = 0; this._sampleRate = 250; // estimated, updated from dt /** Sustain counter for debounce — counts up when above threshold, down when below. */ this.sustainCounter = 0; /** EMA-smoothed frequency for stable readout. */ this.smoothedFreq = 0; /** Latest computed bin powers. */ this._bins = []; this.threshold = params?.threshold ?? 0.15; this.sustainRequired = params?.sustain ?? 15; const windowSize = params?.windowSize ?? 256; this.bufX = new Float64Array(windowSize); this.bufY = new Float64Array(windowSize); this.bufZ = new Float64Array(windowSize); this.freqStep = params?.freqStep ?? 0.25; const freqMin = params?.freqMin ?? 0.25; const freqMax = params?.freqMax ?? 15; this.freqBins = []; for (let f = freqMin; f <= freqMax + 1e-9; f += this.freqStep) { this.freqBins.push(Math.round(f * 1000) / 1000); // avoid float drift } } /** Reset all state (call on disconnect). */ reset() { this.bufX.fill(0); this.bufY.fill(0); this.bufZ.fill(0); this.head = 0; this.filled = 0; this.intensity = 0; this.frequency = 0; this.fundamental = 0; this.active = false; this._sampleRate = 250; this.sustainCounter = 0; this.smoothedFreq = 0; this._bins = []; } /** * Feed one accelerometer sample. * * @param ax Calibrated accel X ([-1, 1]) * @param ay Calibrated accel Y ([-1, 1]) * @param az Calibrated accel Z ([-1, 1]) * @param dt Time delta in seconds since last sample */ update(ax, ay, az, dt) { // Update sample rate estimate (exponential moving average) if (dt > 0 && dt < 0.1) { const instantRate = 1 / dt; this._sampleRate += 0.1 * (instantRate - this._sampleRate); } // Store per-axis acceleration in circular buffers const len = this.bufX.length; this.bufX[this.head] = ax; this.bufY[this.head] = ay; this.bufZ[this.head] = az; this.head = (this.head + 1) % len; if (this.filled < len) this.filled++; // Need at least half a window to produce meaningful results if (this.filled < len / 2) return; // ---- Intensity: RMS of magnitude deviation from mean ---- const n = this.filled; const start = n < len ? 0 : this.head; // Compute acceleration magnitudes and their mean let magSum = 0; for (let i = 0; i < n; i++) { const idx = (start + i) % len; const x = this.bufX[idx], y = this.bufY[idx], z = this.bufZ[idx]; magSum += Math.sqrt(x * x + y * y + z * z); } const magMean = magSum / n; let sumSq = 0; for (let i = 0; i < n; i++) { const idx = (start + i) % len; const x = this.bufX[idx], y = this.bufY[idx], z = this.bufZ[idx]; const d = Math.sqrt(x * x + y * y + z * z) - magMean; sumSq += d * d; } const rms = Math.sqrt(sumSq / n); this.intensity = Math.min(1, rms * ShakeDetector.INTENSITY_SCALE); // ---- Frequency: per-axis Goertzel on first-difference signal ---- if (this.intensity > this.threshold * 0.5) { let peakPower = 0; let peakFreq = 0; this._bins = new Array(this.freqBins.length); for (let b = 0; b < this.freqBins.length; b++) { const freq = this.freqBins[b]; const px = this.goertzel(freq, this.bufX); const py = this.goertzel(freq, this.bufY); const pz = this.goertzel(freq, this.bufZ); // The first-difference filter has gain ∝ freq², so raw Goertzel // power is ∝ freq² × signal_power. Divide by freq to get net // weighting ∝ freq — moderate preference for shake frequencies // without killing sub-1 Hz rocking. const power = Math.max(px, py, pz) / freq; this._bins[b] = { freq, power }; if (power > peakPower) { peakPower = power; peakFreq = freq; } } // EMA-smooth the frequency reading for stability. // α = 0.2 gives ~5-frame smoothing at 250 Hz. const alpha = 0.2; if (this.smoothedFreq === 0) { this.smoothedFreq = peakFreq; } else { this.smoothedFreq += alpha * (peakFreq - this.smoothedFreq); } // Snap to nearest bin step for clean readout const snapped = Math.round(this.smoothedFreq / this.freqStep) * this.freqStep; this.fundamental = snapped; this.frequency = snapped * 2; // reversal rate } else { this.smoothedFreq = 0; this.frequency = 0; this.fundamental = 0; this._bins = []; } // ---- Sustain: require consecutive frames to activate/deactivate ---- if (this.intensity > this.threshold) { this.sustainCounter = Math.min(this.sustainCounter + 1, this.sustainRequired * 2); } else { this.sustainCounter = Math.max(this.sustainCounter - 1, 0); } this.active = this.sustainCounter >= this.sustainRequired; } /** * Goertzel algorithm: compute power at a single target frequency. * * Operates on the first-difference of the filled portion of a circular * buffer. The first-difference (x[n] - x[n-1]) acts as a high-pass * filter that removes DC (gravity) and low-frequency arm drift, with * gain proportional to frequency. */ goertzel(targetFreq, buf) { const N = this.filled; const k = (targetFreq * N) / this._sampleRate; const w = (2 * Math.PI * k) / N; const coeff = 2 * Math.cos(w); let s1 = 0; let s2 = 0; const start = this.filled < buf.length ? 0 : this.head; let prevSample = buf[start % buf.length]; for (let i = 0; i < N; i++) { const idx = (start + i) % buf.length; const raw = buf[idx]; const x = raw - prevSample; // first-difference high-pass prevSample = raw; const s0 = x + coeff * s1 - s2; s2 = s1; s1 = s0; } return s1 * s1 + s2 * s2 - coeff * s1 * s2; } } exports.ShakeDetector = ShakeDetector; /** * Scale factor: maps RMS of our [-1, 1] calibrated accel deviation * to a 0–1 intensity. A vigorous shake produces magnitude-RMS of * ~0.3–0.5 in our units; this maps that to ~0.6–1.0 intensity. */ ShakeDetector.INTENSITY_SCALE = 2; //# sourceMappingURL=shake.js.map