From 90860741d9faea3a11d305a2691ecd1ff6a45ac9 Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:20:01 -0500 Subject: [PATCH 1/7] initial commit --- CMakeLists.txt | 11 +++++ Puara/PPAmplitude.cpp | 104 +++++++++++++++++++++++++++++++++++++++++ Puara/PPAmplitude.hpp | 105 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 Puara/PPAmplitude.cpp create mode 100644 Puara/PPAmplitude.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6af25f2..32b5fdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,6 +126,17 @@ avnd_score_plugin_add( NAMESPACE puara_gestures::objects ) + +avnd_score_plugin_add( + BASE_TARGET score_addon_puara + SOURCES + Puara/PPAmplitude.hpp + Puara/PPAmplitude.cpp + TARGET peak_amplitude + MAIN_CLASS PeakAmplitude + NAMESPACE puara_gestures::objects +) + avnd_score_plugin_add( BASE_TARGET score_addon_puara SOURCES diff --git a/Puara/PPAmplitude.cpp b/Puara/PPAmplitude.cpp new file mode 100644 index 0000000..2e2b7a1 --- /dev/null +++ b/Puara/PPAmplitude.cpp @@ -0,0 +1,104 @@ +#include "PPAmplitude.hpp" + +namespace puara_gestures::objects +{ + +void PeakAmplitude::prepare(halp::setup info) +{ + setup = info; + + last_min_ = 0.0f; + last_max_ = 0.0f; + last_amplitude_ = 0.0f; + + have_min_ = false; + have_max_ = false; + + prev_min_gate_ = false; + prev_max_gate_ = false; + + // Prime mode watcher so the first tick doesn't treat it as a spurious change. + mode_watcher.changed(inputs.mode.value); +} + +void PeakAmplitude::operator()(halp::tick) +{ + const float raw = inputs.raw_signal; + const bool min_gate = inputs.peak_min_gate; + const bool max_gate = inputs.peak_max_gate; + + Mode mode_value = inputs.mode.value; + + // Handle mode change: if mode flips (OnMax <-> OnMin), reset the cycle state + // so we don't mix a "last min" from the previous mode with a "new max", etc. + if (mode_watcher.changed(mode_value)) + { + have_min_ = false; + have_max_ = false; + last_min_ = raw; + last_max_ = raw; + last_amplitude_ = 0.0f; + prev_min_gate_ = min_gate; + prev_max_gate_ = max_gate; + } + + // Rising-edge detection on the gates. + const bool min_rise = (min_gate && !prev_min_gate_); + const bool max_rise = (max_gate && !prev_max_gate_); + + prev_min_gate_ = min_gate; + prev_max_gate_ = max_gate; + + // Latch raw values at the exact peak events. + if (min_rise) + { + last_min_ = raw; + have_min_ = true; + } + + if (max_rise) + { + last_max_ = raw; + have_max_ = true; + } + + float amp = last_amplitude_; // hold previous amplitude by default + + const bool strict = inputs.strict_cycles; + + // Emit amplitude depending on the selected mode and available extrema. + switch (mode_value) + { + case Mode::OnMax: + // When a max event occurs and we have already seen a min for this cycle: + // amplitude = max_raw - last_min_raw + if (max_rise && have_min_) + { + amp = last_max_ - last_min_; + last_amplitude_ = amp; + + // Strict min→max→min cycles: force a new min before next amplitude. + if (strict) + have_min_ = false; + } + break; + + case Mode::OnMin: + // When a min event occurs and we have already seen a max for this cycle: + // amplitude = last_max_raw - min_raw + if (min_rise && have_max_) + { + amp = last_max_ - last_min_; + last_amplitude_ = amp; + + // Strict max→min→max cycles: force a new max before next amplitude. + if (strict) + have_max_ = false; + } + break; + } + + outputs.amplitude = amp; +} + +} // namespace puara_gestures::objects diff --git a/Puara/PPAmplitude.hpp b/Puara/PPAmplitude.hpp new file mode 100644 index 0000000..6c72e73 --- /dev/null +++ b/Puara/PPAmplitude.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include "halp_utils.hpp" + +#include +#include +#include + +#include + +namespace puara_gestures::objects +{ +// amplitude +class PeakAmplitude +{ +public: + halp_meta(name, "Peak-to-peak amplitude") + halp_meta(category, "Analysis/Data") + halp_meta(c_name, "peak_amplitude") + halp_meta(author, "Luana Belinsky") + halp_meta( + description, + "Computes peak-to-peak amplitude in raw units using external peak events.\n" + "Takes a raw signal and two gates: peak-min and peak-max (from a PeakDetector).\n" + "You can choose whether to emit an amplitude at each detected peak-max or " + "peak-min.\n" + "Amplitude is computed as (raw_peak_max - raw_peak_min) and held until the next " + "cycle.\n" + "An optional 'Strict cycles' mode enforces a full min→max→min (or max→min→max) " + "sequence before emitting the next amplitude.") + halp_meta(manual_url, "https://ossia.io/score-docs/") + // Documentation TODO + halp_meta(uuid, "f2ffb195-2e4e-4a34-b044-7662107ea0a7") + + /// When to emit the peak-to-peak amplitude. + enum class Mode + { + OnMax, ///< Output amplitude when a peak max event occurs (using last min). + OnMin ///< Output amplitude when a peak min event occurs (using last max). + }; + + struct + { + halp::data_port< + "Raw signal", + "Original, non-normalized signal whose peak-to-peak amplitude will be measured.", + float> + raw_signal; + + halp::data_port< + "Peak min gate", + "Boolean gate that goes high when a minimum is detected (e.g. from PeakDetector MIN mode).", + bool> + peak_min_gate; + + halp::data_port< + "Peak max gate", + "Boolean gate that goes high when a maximum is detected (e.g. from PeakDetector MAX mode).", + bool> + peak_max_gate; + + // How to define the cycle: + // - OnMax: emit amplitude when a max occurs (cycle min→max→min). + // - OnMin: emit amplitude when a min occurs (cycle max→min→max). + halp::enum_t mode{Mode::OnMax}; + + // If enabled, require a full min→max→min or max→min→max sequence + // before emitting the next amplitude (no reusing old extrema). + halp::toggle<"Strict cycles"> strict_cycles{true}; + + } inputs; + + struct + { + halp::data_port< + "Peak-to-peak amplitude", + "Peak-to--peak amplitude in raw units, computed as (peak_max_raw - peak_min_raw).\n" + "Value is updated when a peak event occurs according to the selected mode and held otherwise.", + float> + amplitude; + } outputs; + + halp::setup setup; + void prepare(halp::setup info); + + using tick = halp::tick; + void operator()(halp::tick t); + +private: + // Internal state + float last_min_ = 0.0f; + float last_max_ = 0.0f; + float last_amplitude_ = 0.0f; + + bool have_min_ = false; + bool have_max_ = false; + + bool prev_min_gate_ = false; + bool prev_max_gate_ = false; + + // Watch mode changes so we can reset cycle state cleanly. + halp::ParameterWatcher mode_watcher; +}; + +} // namespace puara_gestures::objects From 64ebce3802f1dbc0120319a06642a7eb5b1c792c Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:18:36 -0500 Subject: [PATCH 2/7] simplified some comments --- Puara/PPAmplitude.cpp | 2 +- Puara/PPAmplitude.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Puara/PPAmplitude.cpp b/Puara/PPAmplitude.cpp index 2e2b7a1..00e01ba 100644 --- a/Puara/PPAmplitude.cpp +++ b/Puara/PPAmplitude.cpp @@ -17,7 +17,7 @@ void PeakAmplitude::prepare(halp::setup info) prev_min_gate_ = false; prev_max_gate_ = false; - // Prime mode watcher so the first tick doesn't treat it as a spurious change. + // Initialize mode watcher mode_watcher.changed(inputs.mode.value); } diff --git a/Puara/PPAmplitude.hpp b/Puara/PPAmplitude.hpp index 6c72e73..3a7dc8c 100644 --- a/Puara/PPAmplitude.hpp +++ b/Puara/PPAmplitude.hpp @@ -98,7 +98,7 @@ class PeakAmplitude bool prev_min_gate_ = false; bool prev_max_gate_ = false; - // Watch mode changes so we can reset cycle state cleanly. + // Parameter watcher halp::ParameterWatcher mode_watcher; }; From a803062a9e2932b823c69c9ec8d520684b1b8a41 Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:33:22 -0500 Subject: [PATCH 3/7] simplified a description --- Puara/PPAmplitude.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Puara/PPAmplitude.hpp b/Puara/PPAmplitude.hpp index 3a7dc8c..097e112 100644 --- a/Puara/PPAmplitude.hpp +++ b/Puara/PPAmplitude.hpp @@ -43,7 +43,7 @@ class PeakAmplitude { halp::data_port< "Raw signal", - "Original, non-normalized signal whose peak-to-peak amplitude will be measured.", + "Original signal whose peak-to-peak amplitude will be measured.", float> raw_signal; From 1204cb66ebe81d38dc339a6da2ce9f0dd70d744c Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:50:07 -0500 Subject: [PATCH 4/7] cleaning branch - separate from peakdetector --- 3rdparty/extras/PeakDetector.cpp | 176 ------------------------------- 3rdparty/extras/PeakDetector.h | 128 ---------------------- 2 files changed, 304 deletions(-) delete mode 100644 3rdparty/extras/PeakDetector.cpp delete mode 100644 3rdparty/extras/PeakDetector.h diff --git a/3rdparty/extras/PeakDetector.cpp b/3rdparty/extras/PeakDetector.cpp deleted file mode 100644 index 369f870..0000000 --- a/3rdparty/extras/PeakDetector.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/* - * PeakDetector.cpp - * - * Adapté de : - * Plaquette (c) 2022 Sofian Audry :: info(@)sofianaudry(.)com - * - * Adaptation par Luana Belinsky 2025 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "PeakDetector.h" - -#include "helpers.h" - -#include - -#include - -PeakDetector::PeakDetector(float triggerThreshold_, uint8_t mode_) - : - _triggerThreshold(triggerThreshold_), - _reloadThreshold(triggerThreshold_), - _fallbackTolerance(0.1f), - _mode(PEAK_RISING) // will be reset properly when calling mode(mode_) -{ - // Assign mode. - mode(mode_); - - // Assign triggerThreshold (flip if necessary). - triggerThreshold(triggerThreshold_); - - // Set default values. - reloadThreshold(triggerThreshold_); - fallbackTolerance(0.1f); - - // Reset detector. - _reset(); -} - -void PeakDetector::triggerThreshold(float triggerThreshold) { - triggerThreshold = modeInverted() ? -triggerThreshold : triggerThreshold; - - if (_triggerThreshold != triggerThreshold) { - _triggerThreshold = triggerThreshold; - _reset(); - } -} - -void PeakDetector::reloadThreshold(float reloadThreshold) { - if (modeInverted()) reloadThreshold = -reloadThreshold; - reloadThreshold = std::min(reloadThreshold, _triggerThreshold); - - if (_reloadThreshold != reloadThreshold) { - _reloadThreshold = reloadThreshold; - _reset(); - } -} - -void PeakDetector::fallbackTolerance(float fallbackTolerance) { - _fallbackTolerance = std::clamp(fallbackTolerance, 0.0f, 1.0f); -} - -void PeakDetector::mode(uint8_t mode) { - // Save current state. - bool wasInverted = modeInverted(); - - // Change mode. - _mode = std::clamp(mode, (uint8_t)PEAK_MAX, (uint8_t)PEAK_FALLING); - - // If mode inversion was changed, adjust triggerThresholds. - if (modeInverted() != wasInverted) { - // Flip. - _triggerThreshold = -_triggerThreshold; - _reloadThreshold = -_reloadThreshold; - } -} - -bool PeakDetector::modeInverted() const { - return (_mode == PEAK_FALLING || _mode == PEAK_MIN); -} - -bool PeakDetector::modeCrossing() const { - return (_mode == PEAK_RISING || _mode == PEAK_FALLING); -} - -bool PeakDetector::modeApex() const { - return !modeCrossing(); -} - -float PeakDetector::put(float value) { - // Flip value. - if (modeInverted()) - value = -value; - - // Check if value is above triggerThreshold ("high" flag). - bool high = (value >= _triggerThreshold); // value is high if above triggerThreshold - - // Initialize _wasLow on first run. - if (_firstRun) { - _wasLow = !high; - _firstRun = false; - } - - else { - - bool crossing = (high && _wasLow); // value is crossing if just crossed triggerThreshold - bool isMax = (value > _peakValue); // value is new max if higher than current peak value - - // At the moment of crossing, reset flags. - if (crossing) { - _wasLow = false; - _crossed = true; - } - - // Check if value is below reloadThreshold. - else if (value <= _reloadThreshold) - _wasLow = true; - - // Perform fallback detection operations. - bool fallingBack = false; - if (_crossed) { - // Set peak value. - if (isMax) { - _peakValue = value; - } - - - // Check for fallback (only if value is below peak ie. !isMax). - // Fallback detected after crossing and falling below maximum and either: - // (1) drops by % tolerance between peak and triggerThreshold OR - // (2) falls below triggerThreshold (!high) - else if ((helpers::map(value, _peakValue, _triggerThreshold,0,1) >= _fallbackTolerance && - _peakValue != _triggerThreshold) // deal with special case where mapTo01(...) would return 0.5 by default - || !high) { - - // Fallback detected. - fallingBack = true; - - // Reset. - _crossed = false; - _peakValue = -FLT_MAX; - } - } - - // Assign value depending on mode. - _onValue = (modeCrossing() ? crossing : fallingBack); - } - - return _onValue; -} - -void PeakDetector::_reset() { - // Init peak value to -inf. - _peakValue = -FLT_MAX; - - // Init all flags. - _onValue = _isHigh = _crossed = false; - _wasLow = true; - - // Set first run flag. - _firstRun = true; -} - - diff --git a/3rdparty/extras/PeakDetector.h b/3rdparty/extras/PeakDetector.h deleted file mode 100644 index e861455..0000000 --- a/3rdparty/extras/PeakDetector.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - * PeakDetector.h - * - * Adapté de : - * Plaquette (c) 2022 Sofian Audry :: info(@)sofianaudry(.)com - * - * Adaptation par Luana Belinsky 2025 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include - -/// @brief Peak modes. -enum { - PEAK_MAX, - PEAK_MIN, - PEAK_RISING, - PEAK_FALLING -}; - -/** - * Emits a signals when a signal peaks. - */ -class PeakDetector { -public: - /** - * Constructor. Possible modes are: - * - PEAK_RISING : peak detected when value becomes >= triggerThreshold, then wait until it becomes < reloadThreshold (*) - * - PEAK_FALLING : peak detected when value becomes <= triggerThreshold, then wait until it becomes > reloadThreshold (*) - * - PEAK_MAX : peak detected after value becomes >= triggerThreshold and then falls back after peaking; then waits until it becomes < reloadThreshold (*) - * - PEAK_MIN : peak detected after value becomes <= triggerThreshold and then rises back after peaking; then waits until it becomes > reloadThreshold (*) - * @param triggerThreshold value that triggers peak detection - * @param mode peak detection mode - */ - PeakDetector(float triggerThreshold, uint8_t mode=PEAK_MAX); - virtual ~PeakDetector() {} - - /// Sets triggerThreshold. - void triggerThreshold(float triggerThreshold); - - /// Returns triggerThreshold. - float triggerThreshold() const { return _triggerThreshold; } - - /** - * Sets minimal threshold that "resets" peak detection in crossing - * (rising/falling) and peak (min/max) modes. - */ - void reloadThreshold(float reloadThreshold); - - /// Returns minimal value "drop" for reset. - float reloadThreshold() const { return _reloadThreshold; } - - /** - * Sets minimal relative "drop" after peak to trigger detection in peak (min/max) - * modes, expressed as proportion (%) of peak minus triggerThreshold. - */ - void fallbackTolerance(float fallbackTolerance); - - /// Returns minimal relative "drop" after peak to trigger detection in peak modes. - float fallbackTolerance() const { return _fallbackTolerance; } - - /// Returns true if mode is PEAK_FALLING or PEAK_MIN. - bool modeInverted() const; - - /// Returns true if mode is PEAK_RISING or PEAK_FALLING. - bool modeCrossing() const; - - /// Returns true if mode is PEAK_MAX or PEAK_MIN. - bool modeApex() const; - - /// Sets mode. - void mode(uint8_t mode); - - /// Returns mode. - uint8_t mode() const { return _mode; } - - /** - * Pushes value into the unit. - * @param value the value sent to the unit - * @return the new value of the unit - */ - virtual float put(float value); - - /// Returns true if the triggerThreshold is crossed. - virtual bool isOn() { return _onValue; } - -protected: - // Resets peak detection flags. - void _reset(); - - // Threshold values. - float _triggerThreshold; - float _reloadThreshold; - float _fallbackTolerance; - float _peakValue; - - // Thresholding mode. - bool _onValue : 1; - uint8_t _mode : 2; - - // Booleans used to keep track of signal value. - bool _isHigh : 1; - bool _wasLow : 1; - bool _crossed : 1; - bool _firstRun : 1; - - // Unused extra space. - uint8_t _data : 1; - - // Previous-sample memory to synthesize between-tick events - float _prevValue = 0.f; - bool _hasPrev = false; -}; From eb2d5bab2c229aa7a2c85058edd71f1d1bda0b06 Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:42:50 -0500 Subject: [PATCH 5/7] fix rebase error (lost PeakDetector files?) --- 3rdparty/extras/PeakDetector.cpp | 176 +++++++++++++++++++++++++++++++ 3rdparty/extras/PeakDetector.h | 128 ++++++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 3rdparty/extras/PeakDetector.cpp create mode 100644 3rdparty/extras/PeakDetector.h diff --git a/3rdparty/extras/PeakDetector.cpp b/3rdparty/extras/PeakDetector.cpp new file mode 100644 index 0000000..369f870 --- /dev/null +++ b/3rdparty/extras/PeakDetector.cpp @@ -0,0 +1,176 @@ +/* + * PeakDetector.cpp + * + * Adapté de : + * Plaquette (c) 2022 Sofian Audry :: info(@)sofianaudry(.)com + * + * Adaptation par Luana Belinsky 2025 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "PeakDetector.h" + +#include "helpers.h" + +#include + +#include + +PeakDetector::PeakDetector(float triggerThreshold_, uint8_t mode_) + : + _triggerThreshold(triggerThreshold_), + _reloadThreshold(triggerThreshold_), + _fallbackTolerance(0.1f), + _mode(PEAK_RISING) // will be reset properly when calling mode(mode_) +{ + // Assign mode. + mode(mode_); + + // Assign triggerThreshold (flip if necessary). + triggerThreshold(triggerThreshold_); + + // Set default values. + reloadThreshold(triggerThreshold_); + fallbackTolerance(0.1f); + + // Reset detector. + _reset(); +} + +void PeakDetector::triggerThreshold(float triggerThreshold) { + triggerThreshold = modeInverted() ? -triggerThreshold : triggerThreshold; + + if (_triggerThreshold != triggerThreshold) { + _triggerThreshold = triggerThreshold; + _reset(); + } +} + +void PeakDetector::reloadThreshold(float reloadThreshold) { + if (modeInverted()) reloadThreshold = -reloadThreshold; + reloadThreshold = std::min(reloadThreshold, _triggerThreshold); + + if (_reloadThreshold != reloadThreshold) { + _reloadThreshold = reloadThreshold; + _reset(); + } +} + +void PeakDetector::fallbackTolerance(float fallbackTolerance) { + _fallbackTolerance = std::clamp(fallbackTolerance, 0.0f, 1.0f); +} + +void PeakDetector::mode(uint8_t mode) { + // Save current state. + bool wasInverted = modeInverted(); + + // Change mode. + _mode = std::clamp(mode, (uint8_t)PEAK_MAX, (uint8_t)PEAK_FALLING); + + // If mode inversion was changed, adjust triggerThresholds. + if (modeInverted() != wasInverted) { + // Flip. + _triggerThreshold = -_triggerThreshold; + _reloadThreshold = -_reloadThreshold; + } +} + +bool PeakDetector::modeInverted() const { + return (_mode == PEAK_FALLING || _mode == PEAK_MIN); +} + +bool PeakDetector::modeCrossing() const { + return (_mode == PEAK_RISING || _mode == PEAK_FALLING); +} + +bool PeakDetector::modeApex() const { + return !modeCrossing(); +} + +float PeakDetector::put(float value) { + // Flip value. + if (modeInverted()) + value = -value; + + // Check if value is above triggerThreshold ("high" flag). + bool high = (value >= _triggerThreshold); // value is high if above triggerThreshold + + // Initialize _wasLow on first run. + if (_firstRun) { + _wasLow = !high; + _firstRun = false; + } + + else { + + bool crossing = (high && _wasLow); // value is crossing if just crossed triggerThreshold + bool isMax = (value > _peakValue); // value is new max if higher than current peak value + + // At the moment of crossing, reset flags. + if (crossing) { + _wasLow = false; + _crossed = true; + } + + // Check if value is below reloadThreshold. + else if (value <= _reloadThreshold) + _wasLow = true; + + // Perform fallback detection operations. + bool fallingBack = false; + if (_crossed) { + // Set peak value. + if (isMax) { + _peakValue = value; + } + + + // Check for fallback (only if value is below peak ie. !isMax). + // Fallback detected after crossing and falling below maximum and either: + // (1) drops by % tolerance between peak and triggerThreshold OR + // (2) falls below triggerThreshold (!high) + else if ((helpers::map(value, _peakValue, _triggerThreshold,0,1) >= _fallbackTolerance && + _peakValue != _triggerThreshold) // deal with special case where mapTo01(...) would return 0.5 by default + || !high) { + + // Fallback detected. + fallingBack = true; + + // Reset. + _crossed = false; + _peakValue = -FLT_MAX; + } + } + + // Assign value depending on mode. + _onValue = (modeCrossing() ? crossing : fallingBack); + } + + return _onValue; +} + +void PeakDetector::_reset() { + // Init peak value to -inf. + _peakValue = -FLT_MAX; + + // Init all flags. + _onValue = _isHigh = _crossed = false; + _wasLow = true; + + // Set first run flag. + _firstRun = true; +} + + diff --git a/3rdparty/extras/PeakDetector.h b/3rdparty/extras/PeakDetector.h new file mode 100644 index 0000000..e861455 --- /dev/null +++ b/3rdparty/extras/PeakDetector.h @@ -0,0 +1,128 @@ +/* + * PeakDetector.h + * + * Adapté de : + * Plaquette (c) 2022 Sofian Audry :: info(@)sofianaudry(.)com + * + * Adaptation par Luana Belinsky 2025 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +/// @brief Peak modes. +enum { + PEAK_MAX, + PEAK_MIN, + PEAK_RISING, + PEAK_FALLING +}; + +/** + * Emits a signals when a signal peaks. + */ +class PeakDetector { +public: + /** + * Constructor. Possible modes are: + * - PEAK_RISING : peak detected when value becomes >= triggerThreshold, then wait until it becomes < reloadThreshold (*) + * - PEAK_FALLING : peak detected when value becomes <= triggerThreshold, then wait until it becomes > reloadThreshold (*) + * - PEAK_MAX : peak detected after value becomes >= triggerThreshold and then falls back after peaking; then waits until it becomes < reloadThreshold (*) + * - PEAK_MIN : peak detected after value becomes <= triggerThreshold and then rises back after peaking; then waits until it becomes > reloadThreshold (*) + * @param triggerThreshold value that triggers peak detection + * @param mode peak detection mode + */ + PeakDetector(float triggerThreshold, uint8_t mode=PEAK_MAX); + virtual ~PeakDetector() {} + + /// Sets triggerThreshold. + void triggerThreshold(float triggerThreshold); + + /// Returns triggerThreshold. + float triggerThreshold() const { return _triggerThreshold; } + + /** + * Sets minimal threshold that "resets" peak detection in crossing + * (rising/falling) and peak (min/max) modes. + */ + void reloadThreshold(float reloadThreshold); + + /// Returns minimal value "drop" for reset. + float reloadThreshold() const { return _reloadThreshold; } + + /** + * Sets minimal relative "drop" after peak to trigger detection in peak (min/max) + * modes, expressed as proportion (%) of peak minus triggerThreshold. + */ + void fallbackTolerance(float fallbackTolerance); + + /// Returns minimal relative "drop" after peak to trigger detection in peak modes. + float fallbackTolerance() const { return _fallbackTolerance; } + + /// Returns true if mode is PEAK_FALLING or PEAK_MIN. + bool modeInverted() const; + + /// Returns true if mode is PEAK_RISING or PEAK_FALLING. + bool modeCrossing() const; + + /// Returns true if mode is PEAK_MAX or PEAK_MIN. + bool modeApex() const; + + /// Sets mode. + void mode(uint8_t mode); + + /// Returns mode. + uint8_t mode() const { return _mode; } + + /** + * Pushes value into the unit. + * @param value the value sent to the unit + * @return the new value of the unit + */ + virtual float put(float value); + + /// Returns true if the triggerThreshold is crossed. + virtual bool isOn() { return _onValue; } + +protected: + // Resets peak detection flags. + void _reset(); + + // Threshold values. + float _triggerThreshold; + float _reloadThreshold; + float _fallbackTolerance; + float _peakValue; + + // Thresholding mode. + bool _onValue : 1; + uint8_t _mode : 2; + + // Booleans used to keep track of signal value. + bool _isHigh : 1; + bool _wasLow : 1; + bool _crossed : 1; + bool _firstRun : 1; + + // Unused extra space. + uint8_t _data : 1; + + // Previous-sample memory to synthesize between-tick events + float _prevValue = 0.f; + bool _hasPrev = false; +}; From a3f5e4eeb95d35a37af6afb27fe618c5d325ad33 Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:29:10 -0400 Subject: [PATCH 6/7] Amplitude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary Adds a peak-detection and amplitude-analysis utility for real-time signals. The object uses a normalized `Detection signal` to track four event types — rising trigger crossing (`PEAK_RISING`), falling trigger crossing (`PEAK_FALLING`), local maxima (`PEAK_MAX`), and local minima (`PEAK_MIN`) — based on configurable trigger, reload, and fallback parameters. Amplitude is measured from a separate `Raw signal`, with two available modes: `PeakLocked` or `CycleExtrema`. Adapted from Sofian Audry’s [Plaquette implementation](https://plaquette.org/PeakDetector.html), with additional raw-amplitude tracking. ### Functionality Inputs are: - Detection signal (`float`, ideally normalized and scaled 0 to 1) - Raw signal (`float`, original signal used for amplitude measurement) Parameters are: - Trigger threshold (`float` between 0 and 1. Level used for rising and falling trigger crossings) - Reload threshold (`float` between 0 and 1. Level used to reset / re-arm the detector) - Fallback tolerance (`float` between 0 and 1. Drop percentage between trigger and apex required to confirm maxima and minima) - Amplitude mode (`enum`) — `PeakLocked` or `CycleExtrema` - P2P update (`enum`) — updates peak-to-peak on max, min, or both Outputs are: - Peak max (`boolean`. True when a local maximum is confirmed on the detection signal) - Peak min (`boolean`. True when a local minimum is confirmed on the detection signal) - Peak rising (`boolean`. True when the detection signal crosses upward through the trigger threshold) - Peak falling (`boolean`. True when the detection signal crosses downward through the trigger threshold) - Peak-to-peak (`float`. Held raw peak-to-peak amplitude) - Max above threshold (`float`. Held raw amplitude above the rising trigger baseline) - Min below threshold (`float`. Held raw amplitude below the falling trigger baseline) Modes explained PeakLocked: - Uses raw values paired with confirmed detect peaks - Suitable when amplitude should follow the peaks confirmed by the detection signal CycleExtrema: - Uses raw extrema over two half-cycles: rising to falling for the upper excursion, and falling to next rising for the lower excursion - Suitable when amplitude should follow the full raw waveform shape between trigger crossings ### Implementation notes Uses a custom `PeakDetector` class stored in `3rdparty/extras` (to be reused in Respiration and EDA processes). Raw baselines are interpolated at trigger crossings so amplitude is measured relative to the estimated raw value at the threshold crossing, not only at the nearest sample. --- CMakeLists.txt | 10 -- Puara/PPAmplitude.cpp | 104 ------------------- Puara/PPAmplitude.hpp | 105 ------------------- Puara/PeakDetection.cpp | 225 ++++++++++++++++++++++++++++++++++------ Puara/PeakDetection.hpp | 175 ++++++++++++++++++++----------- 5 files changed, 311 insertions(+), 308 deletions(-) delete mode 100644 Puara/PPAmplitude.cpp delete mode 100644 Puara/PPAmplitude.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 32b5fdd..425c520 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,16 +127,6 @@ avnd_score_plugin_add( ) -avnd_score_plugin_add( - BASE_TARGET score_addon_puara - SOURCES - Puara/PPAmplitude.hpp - Puara/PPAmplitude.cpp - TARGET peak_amplitude - MAIN_CLASS PeakAmplitude - NAMESPACE puara_gestures::objects -) - avnd_score_plugin_add( BASE_TARGET score_addon_puara SOURCES diff --git a/Puara/PPAmplitude.cpp b/Puara/PPAmplitude.cpp deleted file mode 100644 index 00e01ba..0000000 --- a/Puara/PPAmplitude.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "PPAmplitude.hpp" - -namespace puara_gestures::objects -{ - -void PeakAmplitude::prepare(halp::setup info) -{ - setup = info; - - last_min_ = 0.0f; - last_max_ = 0.0f; - last_amplitude_ = 0.0f; - - have_min_ = false; - have_max_ = false; - - prev_min_gate_ = false; - prev_max_gate_ = false; - - // Initialize mode watcher - mode_watcher.changed(inputs.mode.value); -} - -void PeakAmplitude::operator()(halp::tick) -{ - const float raw = inputs.raw_signal; - const bool min_gate = inputs.peak_min_gate; - const bool max_gate = inputs.peak_max_gate; - - Mode mode_value = inputs.mode.value; - - // Handle mode change: if mode flips (OnMax <-> OnMin), reset the cycle state - // so we don't mix a "last min" from the previous mode with a "new max", etc. - if (mode_watcher.changed(mode_value)) - { - have_min_ = false; - have_max_ = false; - last_min_ = raw; - last_max_ = raw; - last_amplitude_ = 0.0f; - prev_min_gate_ = min_gate; - prev_max_gate_ = max_gate; - } - - // Rising-edge detection on the gates. - const bool min_rise = (min_gate && !prev_min_gate_); - const bool max_rise = (max_gate && !prev_max_gate_); - - prev_min_gate_ = min_gate; - prev_max_gate_ = max_gate; - - // Latch raw values at the exact peak events. - if (min_rise) - { - last_min_ = raw; - have_min_ = true; - } - - if (max_rise) - { - last_max_ = raw; - have_max_ = true; - } - - float amp = last_amplitude_; // hold previous amplitude by default - - const bool strict = inputs.strict_cycles; - - // Emit amplitude depending on the selected mode and available extrema. - switch (mode_value) - { - case Mode::OnMax: - // When a max event occurs and we have already seen a min for this cycle: - // amplitude = max_raw - last_min_raw - if (max_rise && have_min_) - { - amp = last_max_ - last_min_; - last_amplitude_ = amp; - - // Strict min→max→min cycles: force a new min before next amplitude. - if (strict) - have_min_ = false; - } - break; - - case Mode::OnMin: - // When a min event occurs and we have already seen a max for this cycle: - // amplitude = last_max_raw - min_raw - if (min_rise && have_max_) - { - amp = last_max_ - last_min_; - last_amplitude_ = amp; - - // Strict max→min→max cycles: force a new max before next amplitude. - if (strict) - have_max_ = false; - } - break; - } - - outputs.amplitude = amp; -} - -} // namespace puara_gestures::objects diff --git a/Puara/PPAmplitude.hpp b/Puara/PPAmplitude.hpp deleted file mode 100644 index 097e112..0000000 --- a/Puara/PPAmplitude.hpp +++ /dev/null @@ -1,105 +0,0 @@ -#pragma once - -#include "halp_utils.hpp" - -#include -#include -#include - -#include - -namespace puara_gestures::objects -{ -// amplitude -class PeakAmplitude -{ -public: - halp_meta(name, "Peak-to-peak amplitude") - halp_meta(category, "Analysis/Data") - halp_meta(c_name, "peak_amplitude") - halp_meta(author, "Luana Belinsky") - halp_meta( - description, - "Computes peak-to-peak amplitude in raw units using external peak events.\n" - "Takes a raw signal and two gates: peak-min and peak-max (from a PeakDetector).\n" - "You can choose whether to emit an amplitude at each detected peak-max or " - "peak-min.\n" - "Amplitude is computed as (raw_peak_max - raw_peak_min) and held until the next " - "cycle.\n" - "An optional 'Strict cycles' mode enforces a full min→max→min (or max→min→max) " - "sequence before emitting the next amplitude.") - halp_meta(manual_url, "https://ossia.io/score-docs/") - // Documentation TODO - halp_meta(uuid, "f2ffb195-2e4e-4a34-b044-7662107ea0a7") - - /// When to emit the peak-to-peak amplitude. - enum class Mode - { - OnMax, ///< Output amplitude when a peak max event occurs (using last min). - OnMin ///< Output amplitude when a peak min event occurs (using last max). - }; - - struct - { - halp::data_port< - "Raw signal", - "Original signal whose peak-to-peak amplitude will be measured.", - float> - raw_signal; - - halp::data_port< - "Peak min gate", - "Boolean gate that goes high when a minimum is detected (e.g. from PeakDetector MIN mode).", - bool> - peak_min_gate; - - halp::data_port< - "Peak max gate", - "Boolean gate that goes high when a maximum is detected (e.g. from PeakDetector MAX mode).", - bool> - peak_max_gate; - - // How to define the cycle: - // - OnMax: emit amplitude when a max occurs (cycle min→max→min). - // - OnMin: emit amplitude when a min occurs (cycle max→min→max). - halp::enum_t mode{Mode::OnMax}; - - // If enabled, require a full min→max→min or max→min→max sequence - // before emitting the next amplitude (no reusing old extrema). - halp::toggle<"Strict cycles"> strict_cycles{true}; - - } inputs; - - struct - { - halp::data_port< - "Peak-to-peak amplitude", - "Peak-to--peak amplitude in raw units, computed as (peak_max_raw - peak_min_raw).\n" - "Value is updated when a peak event occurs according to the selected mode and held otherwise.", - float> - amplitude; - } outputs; - - halp::setup setup; - void prepare(halp::setup info); - - using tick = halp::tick; - void operator()(halp::tick t); - -private: - // Internal state - float last_min_ = 0.0f; - float last_max_ = 0.0f; - float last_amplitude_ = 0.0f; - - bool have_min_ = false; - bool have_max_ = false; - - bool prev_min_gate_ = false; - bool prev_max_gate_ = false; - - // Parameter watcher - halp::ParameterWatcher mode_watcher; -}; - -} // namespace puara_gestures::objects diff --git a/Puara/PeakDetection.cpp b/Puara/PeakDetection.cpp index db67264..4ef8939 100644 --- a/Puara/PeakDetection.cpp +++ b/Puara/PeakDetection.cpp @@ -1,51 +1,214 @@ #include "PeakDetection.hpp" +#include +#include + namespace puara_gestures::objects { +// Estimate the raw value at the detect-threshold crossing between two samples. +static inline float interp_raw_at(float d0, float d1, float r0, float r1, float thr) +{ + const float denom = (d1 - d0); + if (denom == 0.f) return r1; + const float t = std::clamp((thr - d0) / denom, 0.f, 1.f); + return r0 + (r1 - r0) * t; +} + void PeakDetection::prepare(halp::setup info) { setup = info; - // Initialize watchers to current UI state - trig_watch.last = inputs.trig_thresh; trig_watch.first = false; - reload_watch.last = inputs.reload_thresh;reload_watch.first = false; - fallback_watch.last = inputs.fallback_tol; fallback_watch.first = false; + outputs.peak_max = outputs.peak_min = outputs.peak_rising = outputs.peak_falling = false; + + outputs.peak_to_peak = last_p2p_; + outputs.max_above_threshold = last_mat_; + outputs.min_below_threshold = last_mbt_; + + has_prev_ = false; + prev_detect_ = 0.f; + prev_raw_ = 0.f; + + raw_at_rising_ = 0.f; + raw_at_falling_ = 0.f; + + track_max_ = track_min_ = false; + best_detect_max_ = -FLT_MAX; + best_detect_min_ = +FLT_MAX; + raw_at_max_candidate_ = 0.f; + raw_at_min_candidate_ = 0.f; + + raw_at_max_ = 0.f; + raw_at_min_ = 0.f; + + in_upper_half_ = false; + in_lower_half_ = false; + have_upper_max_ = false; + upper_raw_max_ = 0.f; + lower_raw_min_ = 0.f; + last_upper_raw_max_ = 0.f; } -void PeakDetection::operator()(halp::tick /*t*/) +void PeakDetection::operator()(halp::tick) { - const bool trig_changed = trig_watch.changed(inputs.trig_thresh); - const bool reload_changed = reload_watch.changed(inputs.reload_thresh); - const bool fallback_changed = fallback_watch.changed(inputs.fallback_tol); + const float d = inputs.detect_signal; + const float raw = inputs.raw_signal; + + // Parameters + const float trig = inputs.trig_thresh; + const float reload = inputs.reload_thresh; + const float fallback = inputs.fallback_tol; + + // Keep detector thresholds in sync with the UI. + for (auto* det : {&det_rising_, &det_falling_, &det_max_, &det_min_}) + { + det->triggerThreshold(trig); + det->reloadThreshold(reload); + det->fallbackTolerance(fallback); + } + + const AmplitudeMode amp_mode = inputs.amplitude_mode; + const P2PUpdateMode p2p_mode = inputs.p2p_update_mode; + + // Events derived from the detection signal. + // PEAK_FALLING fires on the downward crossing of triggerThreshold. + // reloadThreshold only re-arms the detector. + const bool rising_gate = det_rising_.put(d); // crosses UP triggerThreshold + const bool falling_gate = det_falling_.put(d); // crosses DOWN triggerThreshold + const bool pmax_gate = det_max_.put(d); // confirmed detect max + const bool pmin_gate = det_min_.put(d); // confirmed detect min + + outputs.peak_rising = rising_gate; + outputs.peak_falling = falling_gate; + outputs.peak_max = pmax_gate; + outputs.peak_min = pmin_gate; + + // First sample: initialize history and keep the held outputs. + if (!has_prev_) + { + outputs.peak_rising = outputs.peak_falling = outputs.peak_max = outputs.peak_min = false; + prev_detect_ = d; + prev_raw_ = raw; + has_prev_ = true; + return; + } + + // PeakLocked keeps the raw value paired with the strongest detect sample + // seen since the relevant trigger crossing. The >= / <= tests keep the + // latest sample on flat plateaus. + if (track_max_ && d >= best_detect_max_) { best_detect_max_ = d; raw_at_max_candidate_ = raw; } + if (track_min_ && d <= best_detect_min_) { best_detect_min_ = d; raw_at_min_candidate_ = raw; } + + // Rising trigger crossing: latch the raw baseline at the upward trigger crossing. + if (rising_gate) + { + raw_at_rising_ = interp_raw_at(prev_detect_, d, prev_raw_, raw, trig); + + if (amp_mode == AmplitudeMode::CycleExtrema) + { + // Rising closes the lower half-cycle. This finalizes the negative-side + // amplitude and, if the previous upper half-cycle is known, peak-to-peak. + if (in_lower_half_) + { + lower_raw_min_ = std::min(lower_raw_min_, raw_at_rising_); + last_mbt_ = std::max(0.f, raw_at_falling_ - lower_raw_min_); + + if (have_upper_max_) + last_p2p_ = std::max(0.f, last_upper_raw_max_ - lower_raw_min_); + } + + // Start the next upper half-cycle from this rising baseline. + in_lower_half_ = false; + in_upper_half_ = true; + upper_raw_max_ = raw_at_rising_; + } - if (trig_changed || reload_changed || fallback_changed) + // PeakLocked: start tracking the raw sample associated with the next detect max. + if (amp_mode == AmplitudeMode::PeakLocked) + { + track_max_ = true; + best_detect_max_ = -FLT_MAX; + raw_at_max_candidate_ = raw; + } + } + + // Falling trigger crossing: latch the raw baseline at the downward trigger crossing. + if (falling_gate) { - // Update only what actually changed, across all detectors - for (auto& d : det) + raw_at_falling_ = interp_raw_at(prev_detect_, d, prev_raw_, raw, trig); + + if (amp_mode == AmplitudeMode::CycleExtrema) { - if (trig_changed) - d.triggerThreshold(inputs.trig_thresh); + // Falling closes the upper half-cycle and finalizes the positive-side amplitude. + if (in_upper_half_) + { + upper_raw_max_ = std::max(upper_raw_max_, raw_at_falling_); + last_upper_raw_max_ = upper_raw_max_; + have_upper_max_ = true; + last_mat_ = std::max(0.f, upper_raw_max_ - raw_at_rising_); + } - if (reload_changed) - d.reloadThreshold(inputs.reload_thresh); + // Start the next lower half-cycle from this falling baseline. + in_upper_half_ = false; + in_lower_half_ = true; + lower_raw_min_ = raw_at_falling_; + } - if (fallback_changed) - d.fallbackTolerance(inputs.fallback_tol); + // PeakLocked: start tracking the raw sample associated with the next detect min. + if (amp_mode == AmplitudeMode::PeakLocked) + { + track_min_ = true; + best_detect_min_ = +FLT_MAX; + raw_at_min_candidate_ = raw; } + } - const float v = inputs.peakDetection_signal; - - // Compute each detector’s output - const bool rising = det[PEAK_RISING ].put(v); - const bool falling = det[PEAK_FALLING].put(v); - const bool pmax = det[PEAK_MAX ].put(v); - const bool pmin = det[PEAK_MIN ].put(v); - - // Output - outputs.peak_rising = rising; - outputs.peak_falling = falling; - outputs.peak_max = pmax; - outputs.peak_min = pmin; + + // CycleExtrema tracks raw extrema inside the active half-cycle. + if (amp_mode == AmplitudeMode::CycleExtrema) + { + if (in_upper_half_) + upper_raw_max_ = std::max(upper_raw_max_, raw); + + if (in_lower_half_) + lower_raw_min_ = std::min(lower_raw_min_, raw); + } + + // PeakLocked amplitudes update only when the detect maxima/minima are confirmed. + if (amp_mode == AmplitudeMode::PeakLocked) + { + if (pmax_gate) + { + raw_at_max_ = track_max_ ? raw_at_max_candidate_ : raw; + track_max_ = false; + + last_mat_ = std::max(0.f, raw_at_max_ - raw_at_rising_); + + if (p2p_mode == P2PUpdateMode::On_max || p2p_mode == P2PUpdateMode::On_both) + last_p2p_ = std::max(0.f, raw_at_max_ - raw_at_min_); + } + + if (pmin_gate) + { + raw_at_min_ = track_min_ ? raw_at_min_candidate_ : raw; + track_min_ = false; + + // Min-below is measured from the falling trigger baseline down to the detect-locked min. + last_mbt_ = std::max(0.f, raw_at_falling_ - raw_at_min_); + + if (p2p_mode == P2PUpdateMode::On_min || p2p_mode == P2PUpdateMode::On_both) + last_p2p_ = std::max(0.f, raw_at_max_ - raw_at_min_); + } + } + + // Publish held values every tick. + outputs.peak_to_peak = last_p2p_; + outputs.max_above_threshold = last_mat_; + outputs.min_below_threshold = last_mbt_; + + // Update history for interpolation on the next tick. + prev_detect_ = d; + prev_raw_ = raw; + has_prev_ = true; } -} // namespace +} // namespace puara_gestures::objects diff --git a/Puara/PeakDetection.hpp b/Puara/PeakDetection.hpp index c1132e7..20d5c89 100644 --- a/Puara/PeakDetection.hpp +++ b/Puara/PeakDetection.hpp @@ -2,102 +2,161 @@ #include "3rdparty/extras/PeakDetector.h" -#include +#include +#include #include #include #include #include + #include "halp_utils.hpp" namespace puara_gestures::objects { -// peak detector + class PeakDetection { public: + halp_meta(name, "Peak detection") halp_meta(category, "Analysis/Data") halp_meta(c_name, "Peak_detection") - halp_meta(author, "Luana Belinsky (adapted from Sofian Audry’s Plaquette)") - halp_meta( - description, - "Detects peaks in an incoming normalized signal. " - "Four detectors analyze the input to output triggers for rising, falling, " - "maximum, and minimum peaks. \n" - "Input is expected in the range [0, 1]. \n" - "Use normalization / scaling objects upstream if needed." - ) - halp_meta(manual_url, "https://plaquette.org/PeakDetector.html") + halp_meta(author, "Luana Belinsky") + halp_meta(description, + "Detects trigger crossings and peaks from a normalized detection signal, " + "and measures amplitude from a separate raw signal. " + "PeakLocked uses raw values paired with confirmed detect peaks. " + "CycleExtrema measures raw amplitude over two half-cycles: rising to " + "falling, then falling to next rising. " + "Peak-to-peak, max above threshold, and min below threshold are held " + "until the next update.") halp_meta(uuid, "031bc209-5097-44ac-99c1-ade065a0c02d") + enum class P2PUpdateMode { On_max, On_min, On_both }; + enum class AmplitudeMode { PeakLocked, CycleExtrema }; + struct { halp::data_port< - "Peak detection signal", - "Input signal. Float between 0 and 1", - float> - peakDetection_signal; + "Detection signal", + "Normalized signal used for trigger crossings and peak detection", + float> detect_signal; + + halp::data_port< + "Raw signal", + "Original signal used for amplitude measurement", + float> raw_signal; halp::knob_f32< - "Trigger threshold", - halp::range{0.0f, 1.0f, 0.5f}> - trig_thresh; // Level that starts detection + "Trigger threshold", + halp::range{0.f, 1.f, 0.5f}> trig_thresh; halp::knob_f32< - "Reload threshold", - halp::range{0.0f, 1.0f, 0.35f}> - reload_thresh; // Level below which the detector resets + "Reload threshold", + halp::range{0.f, 1.f, 0.35f}> reload_thresh; halp::knob_f32< - "Fallback tolerance", - halp::range{0.0, 1.0, 0.10f}> - fallback_tol; // Drop percentage between trigger and apex + "Fallback tolerance", + halp::range{0.f, 1.f, 0.1f}> fallback_tol; + + halp::enum_t + amplitude_mode{AmplitudeMode::PeakLocked}; + + halp::enum_t + p2p_update_mode{P2PUpdateMode::On_max}; + } inputs; + struct { - halp::data_port< - "Peak max", - "Boolean. True when a local maximum (apex) is detected.", - bool> - peak_max; + halp::data_port<"Peak max", "Pulse on local maximum", bool> peak_max; + halp::data_port<"Peak min", "Pulse on local minimum", bool> peak_min; - halp::data_port< - "Peak min", - "Boolean. True when a local minimum (valley) is detected.", - bool> - peak_min; + halp::data_port<"Peak rising", "Pulse on upward trigger-threshold crossing", bool> peak_rising; + halp::data_port<"Peak falling", "Pulse on downward trigger-threshold crossing", bool> peak_falling; - halp::data_port< - "Peak rising", - "Boolean. True when signal crosses upward above the trigger threshold.", - bool> - peak_rising; - - halp::data_port< - "Peak falling", - "Boolean. True when signal crosses downward below the trigger threshold.", - bool> - peak_falling; + halp::data_port<"Peak-to-peak", "Held peak-to-peak amplitude", float> peak_to_peak; + halp::data_port<"Max above threshold", "Held raw amplitude above the rising trigger baseline", float> max_above_threshold; + halp::data_port<"Min below threshold", "Held raw amplitude below the falling trigger baseline", float> min_below_threshold; } outputs; + halp::setup setup; + void prepare(halp::setup info); using tick = halp::tick; - void operator()(halp::tick t); + void operator()(halp::tick); + private: - // Index directly by the enum: 0=MAX, 1=MIN, 2=RISING, 3=FALLING - std::array det{ - PeakDetector{0.5f, PEAK_MAX}, - PeakDetector{0.5f, PEAK_MIN}, - PeakDetector{0.5f, PEAK_RISING}, - PeakDetector{0.5f, PEAK_FALLING}}; - - halp::ParameterWatcher trig_watch; - halp::ParameterWatcher reload_watch; - halp::ParameterWatcher fallback_watch; + + // -------------------------------------------------- + // Peak detectors + // -------------------------------------------------- + + PeakDetector det_rising_{0.5f, PEAK_RISING}; + PeakDetector det_falling_{0.5f, PEAK_FALLING}; + PeakDetector det_max_{0.5f, PEAK_MAX}; + PeakDetector det_min_{0.5f, PEAK_MIN}; + + + // -------------------------------------------------- + // Previous sample memory + // -------------------------------------------------- + + bool has_prev_ = false; + + float prev_detect_ = 0.f; + float prev_raw_ = 0.f; + + + // -------------------------------------------------- + // Raw baselines interpolated at the detect trigger crossings + // -------------------------------------------------- + + float raw_at_rising_ = 0.f; + float raw_at_falling_ = 0.f; + + + // -------------------------------------------------- + // PeakLocked candidate tracking + // -------------------------------------------------- + + bool track_max_ = false; + bool track_min_ = false; + + float best_detect_max_ = -FLT_MAX; + float best_detect_min_ = FLT_MAX; + + float raw_at_max_candidate_ = 0.f; + float raw_at_min_candidate_ = 0.f; + + float raw_at_max_ = 0.f; + float raw_at_min_ = 0.f; + + + // -------------------------------------------------- + // CycleExtrema half-cycle tracking + // -------------------------------------------------- + + bool in_upper_half_ = false; + bool in_lower_half_ = false; + bool have_upper_max_ = false; + + float upper_raw_max_ = 0.f; + float lower_raw_min_ = 0.f; + float last_upper_raw_max_ = 0.f; + + + // -------------------------------------------------- + // Held amplitude outputs + // -------------------------------------------------- + + float last_p2p_ = 0.f; + float last_mat_ = 0.f; + float last_mbt_ = 0.f; }; -} // namespace puara_gestures::objects +} From 49b4e0e691e048756a3339032203e872bb207f67 Mon Sep 17 00:00:00 2001 From: possibly-human <168475511+possibly-human@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:20:13 -0400 Subject: [PATCH 7/7] Removed CycleExtrema mode Removed CycleExtrema mode from PeakDetection and simplified the object to a single detect-based amplitude path. peak_to_peak, max_above_threshold, and min_below_threshold now all follow the detect-confirmed logic, while a new raw_peak_to_peak output is computed independently from raw slope-based turning points. --- Puara/PeakDetection.cpp | 109 ++++++++++++++-------------------------- Puara/PeakDetection.hpp | 33 +++++------- 2 files changed, 50 insertions(+), 92 deletions(-) diff --git a/Puara/PeakDetection.cpp b/Puara/PeakDetection.cpp index 4ef8939..3abb746 100644 --- a/Puara/PeakDetection.cpp +++ b/Puara/PeakDetection.cpp @@ -21,6 +21,7 @@ void PeakDetection::prepare(halp::setup info) outputs.peak_max = outputs.peak_min = outputs.peak_rising = outputs.peak_falling = false; outputs.peak_to_peak = last_p2p_; + outputs.raw_peak_to_peak = last_raw_p2p_; outputs.max_above_threshold = last_mat_; outputs.min_below_threshold = last_mbt_; @@ -40,12 +41,11 @@ void PeakDetection::prepare(halp::setup info) raw_at_max_ = 0.f; raw_at_min_ = 0.f; - in_upper_half_ = false; - in_lower_half_ = false; - have_upper_max_ = false; - upper_raw_max_ = 0.f; - lower_raw_min_ = 0.f; - last_upper_raw_max_ = 0.f; + prev_raw_slope_ = 0; + have_raw_max_ = false; + have_raw_min_ = false; + last_raw_max_ = 0.f; + last_raw_min_ = 0.f; } void PeakDetection::operator()(halp::tick) @@ -66,7 +66,6 @@ void PeakDetection::operator()(halp::tick) det->fallbackTolerance(fallback); } - const AmplitudeMode amp_mode = inputs.amplitude_mode; const P2PUpdateMode p2p_mode = inputs.p2p_update_mode; // Events derived from the detection signal. @@ -92,43 +91,40 @@ void PeakDetection::operator()(halp::tick) return; } - // PeakLocked keeps the raw value paired with the strongest detect sample + // Keep the raw value paired with the strongest detect sample // seen since the relevant trigger crossing. The >= / <= tests keep the // latest sample on flat plateaus. if (track_max_ && d >= best_detect_max_) { best_detect_max_ = d; raw_at_max_candidate_ = raw; } if (track_min_ && d <= best_detect_min_) { best_detect_min_ = d; raw_at_min_candidate_ = raw; } + // Raw peak-to-peak uses turning points detected directly on the raw signal. + const float raw_delta = raw - prev_raw_; + const int raw_slope = (raw_delta > 0.f) ? 1 : (raw_delta < 0.f ? -1 : prev_raw_slope_); + if (prev_raw_slope_ > 0 && raw_slope < 0) + { + last_raw_max_ = prev_raw_; + have_raw_max_ = true; + if (have_raw_min_) + last_raw_p2p_ = std::max(0.f, last_raw_max_ - last_raw_min_); + } + else if (prev_raw_slope_ < 0 && raw_slope > 0) + { + last_raw_min_ = prev_raw_; + have_raw_min_ = true; + if (have_raw_max_) + last_raw_p2p_ = std::max(0.f, last_raw_max_ - last_raw_min_); + } + prev_raw_slope_ = raw_slope; + // Rising trigger crossing: latch the raw baseline at the upward trigger crossing. if (rising_gate) { raw_at_rising_ = interp_raw_at(prev_detect_, d, prev_raw_, raw, trig); - if (amp_mode == AmplitudeMode::CycleExtrema) - { - // Rising closes the lower half-cycle. This finalizes the negative-side - // amplitude and, if the previous upper half-cycle is known, peak-to-peak. - if (in_lower_half_) - { - lower_raw_min_ = std::min(lower_raw_min_, raw_at_rising_); - last_mbt_ = std::max(0.f, raw_at_falling_ - lower_raw_min_); - - if (have_upper_max_) - last_p2p_ = std::max(0.f, last_upper_raw_max_ - lower_raw_min_); - } - - // Start the next upper half-cycle from this rising baseline. - in_lower_half_ = false; - in_upper_half_ = true; - upper_raw_max_ = raw_at_rising_; - } - - // PeakLocked: start tracking the raw sample associated with the next detect max. - if (amp_mode == AmplitudeMode::PeakLocked) - { - track_max_ = true; - best_detect_max_ = -FLT_MAX; - raw_at_max_candidate_ = raw; - } + // Start tracking the raw sample associated with the next detect max. + track_max_ = true; + best_detect_max_ = -FLT_MAX; + raw_at_max_candidate_ = raw; } // Falling trigger crossing: latch the raw baseline at the downward trigger crossing. @@ -136,45 +132,13 @@ void PeakDetection::operator()(halp::tick) { raw_at_falling_ = interp_raw_at(prev_detect_, d, prev_raw_, raw, trig); - if (amp_mode == AmplitudeMode::CycleExtrema) - { - // Falling closes the upper half-cycle and finalizes the positive-side amplitude. - if (in_upper_half_) - { - upper_raw_max_ = std::max(upper_raw_max_, raw_at_falling_); - last_upper_raw_max_ = upper_raw_max_; - have_upper_max_ = true; - last_mat_ = std::max(0.f, upper_raw_max_ - raw_at_rising_); - } - - // Start the next lower half-cycle from this falling baseline. - in_upper_half_ = false; - in_lower_half_ = true; - lower_raw_min_ = raw_at_falling_; - } - - // PeakLocked: start tracking the raw sample associated with the next detect min. - if (amp_mode == AmplitudeMode::PeakLocked) - { - track_min_ = true; - best_detect_min_ = +FLT_MAX; - raw_at_min_candidate_ = raw; - } - - } - - // CycleExtrema tracks raw extrema inside the active half-cycle. - if (amp_mode == AmplitudeMode::CycleExtrema) - { - if (in_upper_half_) - upper_raw_max_ = std::max(upper_raw_max_, raw); - - if (in_lower_half_) - lower_raw_min_ = std::min(lower_raw_min_, raw); + // Start tracking the raw sample associated with the next detect min. + track_min_ = true; + best_detect_min_ = +FLT_MAX; + raw_at_min_candidate_ = raw; } - // PeakLocked amplitudes update only when the detect maxima/minima are confirmed. - if (amp_mode == AmplitudeMode::PeakLocked) + // Detect-based amplitudes update only when the detect maxima/minima are confirmed. { if (pmax_gate) { @@ -192,7 +156,7 @@ void PeakDetection::operator()(halp::tick) raw_at_min_ = track_min_ ? raw_at_min_candidate_ : raw; track_min_ = false; - // Min-below is measured from the falling trigger baseline down to the detect-locked min. + // Min-below is measured from the falling trigger baseline down to the detect min. last_mbt_ = std::max(0.f, raw_at_falling_ - raw_at_min_); if (p2p_mode == P2PUpdateMode::On_min || p2p_mode == P2PUpdateMode::On_both) @@ -202,6 +166,7 @@ void PeakDetection::operator()(halp::tick) // Publish held values every tick. outputs.peak_to_peak = last_p2p_; + outputs.raw_peak_to_peak = last_raw_p2p_; outputs.max_above_threshold = last_mat_; outputs.min_below_threshold = last_mbt_; diff --git a/Puara/PeakDetection.hpp b/Puara/PeakDetection.hpp index 20d5c89..5e49ec5 100644 --- a/Puara/PeakDetection.hpp +++ b/Puara/PeakDetection.hpp @@ -25,15 +25,12 @@ class PeakDetection halp_meta(description, "Detects trigger crossings and peaks from a normalized detection signal, " "and measures amplitude from a separate raw signal. " - "PeakLocked uses raw values paired with confirmed detect peaks. " - "CycleExtrema measures raw amplitude over two half-cycles: rising to " - "falling, then falling to next rising. " - "Peak-to-peak, max above threshold, and min below threshold are held " - "until the next update.") + "Peak-to-peak, max above threshold, and min below threshold use raw " + "values paired with confirmed detect peaks. " + "Raw peak-to-peak is computed independently from raw turning points.") halp_meta(uuid, "031bc209-5097-44ac-99c1-ade065a0c02d") enum class P2PUpdateMode { On_max, On_min, On_both }; - enum class AmplitudeMode { PeakLocked, CycleExtrema }; struct { @@ -59,9 +56,6 @@ class PeakDetection "Fallback tolerance", halp::range{0.f, 1.f, 0.1f}> fallback_tol; - halp::enum_t - amplitude_mode{AmplitudeMode::PeakLocked}; - halp::enum_t p2p_update_mode{P2PUpdateMode::On_max}; @@ -76,7 +70,8 @@ class PeakDetection halp::data_port<"Peak rising", "Pulse on upward trigger-threshold crossing", bool> peak_rising; halp::data_port<"Peak falling", "Pulse on downward trigger-threshold crossing", bool> peak_falling; - halp::data_port<"Peak-to-peak", "Held peak-to-peak amplitude", float> peak_to_peak; + halp::data_port<"Peak-to-peak", "Held detect-locked peak-to-peak amplitude", float> peak_to_peak; + halp::data_port<"Raw peak-to-peak", "Held raw peak-to-peak amplitude from raw turning points", float> raw_peak_to_peak; halp::data_port<"Max above threshold", "Held raw amplitude above the rising trigger baseline", float> max_above_threshold; halp::data_port<"Min below threshold", "Held raw amplitude below the falling trigger baseline", float> min_below_threshold; } outputs; @@ -121,7 +116,7 @@ class PeakDetection // -------------------------------------------------- - // PeakLocked candidate tracking + // Detect-based candidate tracking // -------------------------------------------------- bool track_max_ = false; @@ -137,17 +132,14 @@ class PeakDetection float raw_at_min_ = 0.f; + // Raw turning-point tracking for raw peak-to-peak // -------------------------------------------------- - // CycleExtrema half-cycle tracking - // -------------------------------------------------- - - bool in_upper_half_ = false; - bool in_lower_half_ = false; - bool have_upper_max_ = false; - float upper_raw_max_ = 0.f; - float lower_raw_min_ = 0.f; - float last_upper_raw_max_ = 0.f; + int prev_raw_slope_ = 0; + bool have_raw_max_ = false; + bool have_raw_min_ = false; + float last_raw_max_ = 0.f; + float last_raw_min_ = 0.f; // -------------------------------------------------- @@ -155,6 +147,7 @@ class PeakDetection // -------------------------------------------------- float last_p2p_ = 0.f; + float last_raw_p2p_ = 0.f; float last_mat_ = 0.f; float last_mbt_ = 0.f; };