1+ #include " EdaRtFeatures.hpp"
2+
3+ namespace puara_gestures ::objects
4+ {
5+
6+ float EdaRtFeatures::onepole_lp (float y, float x, double dt, float fc)
7+ {
8+ if (fc <= 0 .0f || dt <= 0.0 ) return x;
9+ const double a = std::exp (-2.0 * M_PI * static_cast <double >(fc) * dt);
10+ return static_cast <float >(a * y + (1.0 - a) * x);
11+ }
12+
13+ void EdaRtFeatures::prepare (halp::setup info)
14+ {
15+ setup = info;
16+ _have_input = false ;
17+ _last_x = -1e9f;
18+ _time = 0.0 ;
19+ _scl = 0 .0f ;
20+ _baseline = 0 .0f ;
21+
22+ _state = State::Idle;
23+ _onset_base = 0 .0f ;
24+ _onset_time = 0.0 ;
25+ _peak_val = 0 .0f ;
26+ _peak_time = 0.0 ;
27+ _confirm_acc = 0.0 ;
28+ _last_peak_t = -1e9 ;
29+ _rec_target = 0 .0f ;
30+
31+ for (int i=0 ; i<VALLEY_WIN; ++i){ _val_p[i]=0 .0f ; _val_t [i]=0.0 ; }
32+ _val_i = 0 ;
33+ _h_amp = 0 .0f ;
34+ _h_rise = 0 .0f ;
35+ _h_dur = 0 .0f ;
36+
37+ outputs.scl = 0 .0f ;
38+ outputs.scr = 0 .0f ;
39+ outputs.scr_event = 0 ;
40+ outputs.scr_amplitude_us = 0 .0f ;
41+ outputs.scr_rise_time_s = 0 .0f ;
42+ outputs.scr_duration_s = 0 .0f ;
43+ }
44+
45+ void EdaRtFeatures::operator ()(halp::tick t)
46+ {
47+ const double dt = (setup.rate > 0.0 )
48+ ? ((t.frames > 0 ) ? static_cast <double >(t.frames ) / setup.rate : 1.0 / setup.rate )
49+ : 1.0 / 50.0 ;
50+
51+ outputs.scr_event = 0 ;
52+
53+ if (inputs.reset ) { prepare (setup); return ; }
54+
55+ const float x = inputs.signal ;
56+
57+ // ── startup: wait for first nonzero sample ────────────────────────────────
58+ if (!_have_input)
59+ {
60+ if (x == 0 .0f ) { return ; }
61+ _have_input = true ;
62+ _scl = x;
63+ _baseline = 0 .0f ;
64+ _time = 0.0 ;
65+ }
66+
67+ // ── always write held outputs────────────
68+ outputs.scr_amplitude_us = _h_amp;
69+ outputs.scr_rise_time_s = _h_rise;
70+ outputs.scr_duration_s = _h_dur;
71+
72+ // ── per new sample: update tonic, phasic, valley buffer, baseline ──────────
73+ const bool new_sample = (x != _last_x);
74+ if (new_sample)
75+ {
76+ _last_x = x;
77+ _time += dt;
78+
79+ _scl = onepole_lp (_scl, x, dt, 0 .05f );
80+ }
81+ const float phasic = x - _scl;
82+
83+ outputs.scl = _scl;
84+ outputs.scr = phasic;
85+
86+ if (new_sample)
87+ {
88+ // push to valley ring buffer
89+ _val_p[_val_i % VALLEY_WIN] = phasic;
90+ _val_t [_val_i % VALLEY_WIN] = _time;
91+ ++_val_i;
92+
93+ // slow baseline EMA
94+ const float ba = static_cast <float >(1.0 - std::exp (-dt / 10.0 ));
95+ _baseline += (phasic - _baseline) * ba;
96+ }
97+
98+ // ── detection thresholds ──────────────────────────────────────────────────
99+ static constexpr float MIN_AMP = 0 .04f ; // µS
100+ static constexpr float ONSET_TH = 0 .05f ; // above baseline to start rising
101+ static constexpr float DROP_TH = 0 .012f ; // fall below peak to accumulate confirm
102+ static constexpr double CONFIRM_S = 0.30 ; // seconds of falling to confirm peak
103+ static constexpr double MIN_DIST = 1.0 ; // refractory seconds
104+ static constexpr double MAX_REC = 10.0 ; // max recovery seconds
105+
106+ if (!new_sample) return ;
107+
108+ // re-arm when phasic settles near baseline
109+ if (phasic <= _baseline + 0 .3f * ONSET_TH)
110+ _rearmed = true ;
111+
112+ // ── always emit held values ───────────────────────────────────────────────
113+ outputs.scr_amplitude_us = _h_amp;
114+ outputs.scr_rise_time_s = _h_rise;
115+ outputs.scr_duration_s = _h_dur;
116+
117+ // ── state machine ─────────────────────────────────────────────────────────
118+ switch (_state)
119+ {
120+ case State::Idle:
121+ {
122+ const bool refract_ok = (_time - _last_peak_t >= MIN_DIST);
123+ if (_rearmed && refract_ok && (phasic - _baseline) >= ONSET_TH)
124+ {
125+ _rearmed = false ;
126+ _state = State::Rising;
127+ // onset = valley (minimum phasic) in lookback window
128+ _onset_base = phasic; _onset_time = _time;
129+ for (int i=0 ; i<VALLEY_WIN; ++i)
130+ if (_val_p[i] < _onset_base){ _onset_base=_val_p[i]; _onset_time=_val_t [i]; }
131+ _peak_val = phasic;
132+ _peak_time = _time;
133+ _confirm_acc = 0.0 ;
134+ }
135+ break ;
136+ }
137+
138+ case State::Rising:
139+ {
140+ if (phasic > _peak_val)
141+ {
142+ _peak_val = phasic;
143+ _peak_time = _time;
144+ _confirm_acc = 0.0 ;
145+ }
146+ else if (phasic <= _peak_val - DROP_TH)
147+ {
148+ _confirm_acc += dt;
149+ }
150+ else
151+ {
152+ _confirm_acc = 0.0 ;
153+ }
154+
155+ if (_confirm_acc >= CONFIRM_S)
156+ {
157+ // peak confirmed
158+ const float amp = _peak_val - _onset_base;
159+ const double rise_t = _peak_time - _onset_time;
160+
161+ if (amp >= MIN_AMP)
162+ {
163+ _h_amp = amp;
164+ _h_rise = std::min (static_cast <float >(rise_t ), 3 .0f );
165+
166+ outputs.scr_event = 1 ;
167+ outputs.scr_amplitude_us = _h_amp;
168+ outputs.scr_rise_time_s = _h_rise;
169+
170+ _last_peak_t = _peak_time;
171+ _rec_target = _onset_base + amp * 0 .5f ; // 50% recovery
172+ _state = State::Recovering;
173+ }
174+ else
175+ {
176+ _state = State::Idle;
177+ }
178+ }
179+ break ;
180+ }
181+
182+ case State::Recovering:
183+ {
184+ if (phasic <= _rec_target || _time - _peak_time > MAX_REC)
185+ {
186+ _h_dur = static_cast <float >(_time - _onset_time);
187+ outputs.scr_duration_s = _h_dur;
188+ _state = State::Idle;
189+ }
190+ break ;
191+ }
192+ }
193+ }
194+
195+ } // namespace puara_gestures::objects
0 commit comments