feat: add OneEuro (1€ filter) smoothing utility - #120
Open
edumeneses wants to merge 4 commits into
Open
Conversation
Adds puara_gestures::utils::OneEuro, the 1€ filter (Casiez et al. 2012): a speed-adaptive low-pass that removes jitter when the signal is still and eases off when it moves fast, trading jitter against lag far better than a fixed low-pass or moving average. It is the de-facto standard smoothing stage for interactive sensors and controllers. Configurable via mincutoff/beta/dcutoff. Timing comes from the library monotonic clock (filter(value)) or an explicit step (filter(value, dt)), so it behaves the same in an Arduino loop(), a thread, or an ossia score process. Header-only, doubles only, no STL/Boost/allocation. Includes an Arduino example, README entry, and Catch2 tests covering the first-sample passthrough, constant-signal stability, step-response lag, and the jitter-rejection vs fast-tracking tradeoff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163cH7Znu2oYvnaP7fTEQMN
jcelerier
reviewed
Jul 22, 2026
| } | ||
|
|
||
| private: | ||
| static constexpr double pi = 3.14159265358979323846; |
Member
Author
There was a problem hiding this comment.
Hi @jcelerier — I'm an AI assistant (Claude Code) working on this branch with @emeneses; flagging that up front.
I looked into using std::numbers::pi here and decided against it, for portability reasons:
std::numbers::pirequires C++20<numbers>. It compiles fine in the desktop test build (it forces-std=c++20), but on the embedded side it is a regression: olderarduino-esp32cores (2.x) and various Arduino IDE cores default tognu++11, where<numbers>is unavailable, so the header would fail to compile there.- The library already standardizes on
M_PIwith a Boost fallback for exactly this reason — seeutils.handwrap.h:Boost is already a hard dependency (shimmed on Arduino via#ifndef M_PI #define M_PI boost::math::constants::pi<double>() #endif
boost-embedded-190).
So instead of the hardcoded literal I switched to M_PI — same value, matches the surrounding code, and stays portable across cores. oneeuro.h does not include puara/utils.h, so it carries the same guarded M_PI define locally, like wrap.h does. I applied the same change to the two other new headers that had a hardcoded pi (heading.h, multitouch.h).
Happy to move everything to std::numbers::pi if you would rather set a C++20 floor for the library as a whole — just let me know and I will switch them over.
Address review comment (#120): replace the hardcoded pi literal with std::numbers::pi now that the header targets C++20. Behaviour unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163cH7Znu2oYvnaP7fTEQMN
Keep std::numbers::pi where the standard library provides it (__cpp_lib_math_constants), with a literal fallback otherwise, and only include <numbers> when present. Ensures the header still compiles under Arduino/embedded toolchains whose C++ standard may predate the <numbers> header, so the ESP32/PlatformIO and Arduino CI stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163cH7Znu2oYvnaP7fTEQMN
Revert the std::numbers::pi change. Match the library convention of M_PI with a Boost fallback (as in wrap.h) so pi stays portable across Arduino cores that predate the C++20 <numbers> header. oneeuro.h does not include puara/utils.h, so it defines the same guarded M_PI locally. Behaviour unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163cH7Znu2oYvnaP7fTEQMN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
puara_gestures::utils::OneEuro, an implementation of the 1€ filter (Casiez, Roussel & Vogel, CHI 2012). It is a speed-adaptive low-pass: it filters hard when the signal is still (removes jitter) and eases off when the signal moves fast (keeps responsiveness), trading jitter against lag far better than a fixed low-pass or moving average.This is the de-facto standard smoothing stage for interactive sensors and controllers, and slots in front of any Puara descriptor.
API
puara_gestures::utils::OneEuro smoother{1.0, 0.007}; // mincutoff, beta double clean = smoother.filter(analogRead(A0)); // clock-timed double clean = smoother.filter(value, dt); // explicit step (seconds)mincutoff— cutoff (Hz) at rest; lower = smoother but laggierbeta— how much the cutoff opens with speed; higher = less lag on fast movesdcutoff— derivative cutoff (Hz), rarely changedcurrent_value— last output;reset()clears stateConventions kept
#pragma once,puara_gestures::utilsnamespacecurrent_valuelikeLeakyIntegratorgetCurrentTimeMicroseconds) → Arduinoloop(), thread, or ossia processpiconstant — no STL/Boost/allocationFiles
include/puara/utils/oneeuro.hinclude/puara/utils.hexamples/arduino/utils/oneeuro/oneeuro.inotests/test_utils.cpp([utils][oneeuro])Testing
🤖 Generated with Claude Code
https://claude.ai/code/session_0163cH7Znu2oYvnaP7fTEQMN