libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
OneEuroFilter.h
1/****************************************************************************
2 * Platform Abstraction Layer
3 * Daryl Borth 2026
4 * OneEuroFilter.h
5 *
6 * Adaptive low-pass filter for noisy pointer/position signals, per Casiez,
7 * Roussel & Vogel, "1€ Filter: A Simple Speed-based Low-pass Filter for
8 * Noisy Input in Interactive Systems" (CHI 2012).
9 *
10 * A fixed-alpha exponential moving average forces a single tradeoff between
11 * jitter (too little smoothing) and lag (too much smoothing) for every speed
12 * of movement. This filter instead smooths the *signal* heavily when it's
13 * nearly still (killing jitter) and smooths it far less when it's moving
14 * fast (killing lag), by feeding a filtered estimate of speed back into the
15 * cutoff frequency used to filter the signal itself. It needs no platform
16 * headers and only a handful of multiplies per sample, so it's cheap enough
17 * to run per-axis, per-channel, per-frame on devkitPPC.
18 *
19 * Two knobs:
20 * minCutoff - cutoff frequency (Hz) when speed is ~0. Lower = smoother at
21 * rest, but more lag when motion starts. This is the main
22 * "jitter vs lag" knob.
23 * beta - how much cutoff increases with speed. Higher = snappier
24 * during fast motion, but the filter tolerates more residual
25 * jitter as speed rises to get there.
26 *
27 * dCutoff (derivative cutoff) rarely needs tuning away from 1.0.
28 ***************************************************************************/
29#pragma once
30
31#include <cmath>
32
35class LowPassFilter {
36public:
37 LowPassFilter() : initialized(false), storedValue(0.0f) {}
38
43 float filter(float value, float alpha) {
44 if (!initialized) {
45 storedValue = value;
46 initialized = true;
47 } else {
48 storedValue = alpha * value + (1.0f - alpha) * storedValue;
49 }
50 return storedValue;
51 }
52
54 float lastValue() const { return storedValue; }
56 void reset() { initialized = false; storedValue = 0.0f; }
57
58private:
59 bool initialized;
60 float storedValue;
61};
62
66public:
70 OneEuroFilter(float minCutoff = 1.0f, float beta = 0.0f, float dCutoff = 1.0f)
71 : minCutoff(minCutoff), beta(beta), dCutoff(dCutoff), firstSample(true) {}
72
76 float filter(float value, float dt) {
77 if (dt <= 0.0f) dt = 1.0f / 60.0f;
78
79 if (firstSample) {
80 firstSample = false;
81 xFilter.filter(value, 1.0f);
82 dxFilter.filter(0.0f, 1.0f);
83 return value;
84 }
85
86 // Estimate (filtered) speed of the signal.
87 float dx = (value - xFilter.lastValue()) / dt;
88 float edx = dxFilter.filter(dx, computeAlpha(dCutoff, dt));
89
90 // Cutoff rises with speed: fast movement gets less smoothing (less
91 // lag), near-zero movement gets the full minCutoff smoothing (less
92 // jitter).
93 float cutoff = minCutoff + beta * std::fabs(edx);
94
95 return xFilter.filter(value, computeAlpha(cutoff, dt));
96 }
97
99 void reset() {
100 firstSample = true;
101 xFilter.reset();
102 dxFilter.reset();
103 }
104
106 void setParams(float newMinCutoff, float newBeta, float newDCutoff = 1.0f) {
107 minCutoff = newMinCutoff;
108 beta = newBeta;
109 dCutoff = newDCutoff;
110 }
111
112private:
113 static float computeAlpha(float cutoff, float dt) {
114 float tau = 1.0f / (2.0f * (float)M_PI * cutoff);
115 return 1.0f / (1.0f + tau / dt);
116 }
117
118 float minCutoff;
119 float beta;
120 float dCutoff;
121 bool firstSample;
122 LowPassFilter xFilter;
123 LowPassFilter dxFilter;
124};
float lastValue() const
void reset()
Forgets all history; the next filter() call returns its input unchanged.
float filter(float value, float alpha)
Feeds one sample through the filter.
float filter(float value, float dt)
void setParams(float newMinCutoff, float newBeta, float newDCutoff=1.0f)
Changes the filter parameters without resetting its state; see the constructor for their meaning.
OneEuroFilter(float minCutoff=1.0f, float beta=0.0f, float dCutoff=1.0f)
void reset()
Forgets all history; the next filter() call returns its input unchanged.