dspx 1.1.6 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -882,9 +882,30 @@ namespace dsp
882
882
  throw std::invalid_argument("PeakDetection: requires 'threshold' parameter");
883
883
  }
884
884
 
885
+ if (!params.Has("mode"))
886
+ {
887
+ throw std::invalid_argument("PeakDetection: requires 'mode' parameter");
888
+ }
889
+
885
890
  float threshold = params.Get("threshold").As<Napi::Number>().FloatValue();
886
891
 
887
- return std::make_unique<dsp::adapters::PeakDetectionStage>(threshold);
892
+ // Optional mode and domain parameters
893
+ std::string mode = params.Has("mode")
894
+ ? params.Get("mode").As<Napi::String>().Utf8Value()
895
+ : "moving";
896
+ std::string domain = params.Has("domain")
897
+ ? params.Get("domain").As<Napi::String>().Utf8Value()
898
+ : "time";
899
+
900
+ // Get new optional windowSize and minPeakDistance
901
+ int windowSize = params.Has("windowSize")
902
+ ? params.Get("windowSize").As<Napi::Number>().Int32Value()
903
+ : 3;
904
+ int minPeakDistance = params.Has("minPeakDistance")
905
+ ? params.Get("minPeakDistance").As<Napi::Number>().Int32Value()
906
+ : 1;
907
+
908
+ return std::make_unique<dsp::adapters::PeakDetectionStage>(threshold, mode, domain, windowSize, minPeakDistance);
888
909
  };
889
910
 
890
911
  // ===================================================================
@@ -931,6 +952,37 @@ namespace dsp
931
952
 
932
953
  return std::make_unique<dsp::adapters::SnrStage>(window_size);
933
954
  };
955
+
956
+ // ===================================================================
957
+ // GENERIC FILTER RESTORATION (FIR & IIR)
958
+ // ===================================================================
959
+ auto filterFactory = [](const Napi::Object &params)
960
+ {
961
+ if (!params.Has("bCoeffs") || !params.Has("aCoeffs"))
962
+ {
963
+ throw std::invalid_argument("FilterStage: State missing 'bCoeffs' or 'aCoeffs'. Cannot reconstruct.");
964
+ }
965
+
966
+ Napi::Array bArray = params.Get("bCoeffs").As<Napi::Array>();
967
+ Napi::Array aArray = params.Get("aCoeffs").As<Napi::Array>();
968
+
969
+ std::vector<double> bCoeffs(bArray.Length());
970
+ for (uint32_t i = 0; i < bArray.Length(); ++i)
971
+ {
972
+ bCoeffs[i] = bArray.Get(i).As<Napi::Number>().DoubleValue();
973
+ }
974
+
975
+ std::vector<double> aCoeffs(aArray.Length());
976
+ for (uint32_t i = 0; i < aArray.Length(); ++i)
977
+ {
978
+ aCoeffs[i] = aArray.Get(i).As<Napi::Number>().DoubleValue();
979
+ }
980
+
981
+ return std::make_unique<dsp::adapters::FilterStage>(bCoeffs, aCoeffs);
982
+ };
983
+
984
+ m_stageFactories["filter:fir"] = filterFactory;
985
+ m_stageFactories["filter:iir"] = filterFactory;
934
986
  }
935
987
 
936
988
  /**
@@ -1319,18 +1371,21 @@ namespace dsp
1319
1371
  Napi::Function stringify = JSON.Get("stringify").As<Napi::Function>();
1320
1372
  return stringify.Call(JSON, {stateObj});
1321
1373
  }
1322
-
1323
1374
  /**
1324
- * Load pipeline state from JSON string
1325
- * TypeScript retrieves this from Redis and passes it here
1326
- *
1327
- * Accepts: JSON string with pipeline configuration
1375
+ * Load pipeline state from JSON string with Smart Merge Strategy.
1376
+ * * Logic:
1377
+ * 1. Iterate through the saved state history.
1378
+ * 2. If a saved stage matches the current pipeline's next stage (by type),
1379
+ * restore the state into the existing stage instance.
1380
+ * 3. If there is a mismatch (e.g. saved has 'Rectify' but current has 'Filter'),
1381
+ * dynamically create the 'Rectify' stage using the factory and insert it.
1382
+ * 4. Finally, append any remaining stages from the current pipeline definition.
1383
+ * * Result: The pipeline becomes a fusion of [Restorable Saved Stages] + [Current Stages]
1328
1384
  */
1329
1385
  Napi::Value DspPipeline::LoadState(const Napi::CallbackInfo &info)
1330
1386
  {
1331
1387
  Napi::Env env = info.Env();
1332
1388
 
1333
- // Validate input
1334
1389
  if (info.Length() < 1 || !info[0].IsString())
1335
1390
  {
1336
1391
  Napi::TypeError::New(env, "Expected state JSON string as first argument")
@@ -1342,12 +1397,10 @@ namespace dsp
1342
1397
 
1343
1398
  try
1344
1399
  {
1345
- // Parse JSON string using JavaScript's JSON.parse
1346
1400
  Napi::Object JSON = env.Global().Get("JSON").As<Napi::Object>();
1347
1401
  Napi::Function parse = JSON.Get("parse").As<Napi::Function>();
1348
1402
  Napi::Object stateObj = parse.Call(JSON, {Napi::String::New(env, stateJson)}).As<Napi::Object>();
1349
1403
 
1350
- // Validate state object has required fields
1351
1404
  if (!stateObj.Has("stages"))
1352
1405
  {
1353
1406
  Napi::Error::New(env, "Invalid state: missing 'stages' field")
@@ -1355,34 +1408,86 @@ namespace dsp
1355
1408
  return Napi::Boolean::New(env, false);
1356
1409
  }
1357
1410
 
1358
- // Get stages array
1359
1411
  Napi::Array stagesArray = stateObj.Get("stages").As<Napi::Array>();
1360
- uint32_t stageCount = stagesArray.Length();
1412
+ uint32_t savedStageCount = stagesArray.Length();
1361
1413
 
1362
- // Validate stage count matches
1363
- if (stageCount != m_stages.size())
1364
- {
1365
- Napi::Error::New(env, "Stage count mismatch: expected " +
1366
- std::to_string(m_stages.size()) + " but got " + std::to_string(stageCount))
1367
- .ThrowAsJavaScriptException();
1368
- return Napi::Boolean::New(env, false);
1369
- }
1414
+ // Temporary container for the new merged pipeline structure
1415
+ std::vector<std::unique_ptr<IDspStage>> newStages;
1370
1416
 
1371
- // Log restoration
1372
- std::cout << "Restoring pipeline state with " << stageCount << " stages" << std::endl;
1417
+ // Index to track our position in the *current* pipeline (m_stages)
1418
+ size_t currentIdx = 0;
1373
1419
 
1374
- // Restore each stage's state
1375
- for (uint32_t i = 0; i < stageCount; ++i)
1420
+ for (uint32_t i = 0; i < savedStageCount; ++i)
1376
1421
  {
1377
1422
  Napi::Object stageConfig = stagesArray.Get(i).As<Napi::Object>();
1378
- if (stageConfig.Has("state"))
1423
+
1424
+ if (!stageConfig.Has("type") || !stageConfig.Has("state"))
1425
+ {
1426
+ continue;
1427
+ }
1428
+
1429
+ std::string savedType = stageConfig.Get("type").As<Napi::String>().Utf8Value();
1430
+ Napi::Object stageState = stageConfig.Get("state").As<Napi::Object>();
1431
+
1432
+ // 1. Try to match with the current pipeline stage
1433
+ bool matched = false;
1434
+ if (currentIdx < m_stages.size())
1435
+ {
1436
+ // Check if types match (e.g. both are "movingAverage")
1437
+ if (std::string(m_stages[currentIdx]->getType()) == savedType)
1438
+ {
1439
+ // MATCH: Restore state into the existing stage instance
1440
+ // This preserves any specific config the user might have set on the current stage
1441
+ m_stages[currentIdx]->deserializeState(stageState);
1442
+ newStages.push_back(std::move(m_stages[currentIdx]));
1443
+ currentIdx++;
1444
+ matched = true;
1445
+ }
1446
+ }
1447
+
1448
+ // 2. If mismatch, try to reconstruct the stage from the saved state
1449
+ if (!matched)
1379
1450
  {
1380
- Napi::Object stageState = stageConfig.Get("state").As<Napi::Object>();
1381
- m_stages[i]->deserializeState(stageState);
1451
+ auto it = m_stageFactories.find(savedType);
1452
+ if (it != m_stageFactories.end())
1453
+ {
1454
+ try
1455
+ {
1456
+ // Attempt to create stage using the state object as the config params.
1457
+ // (Assumes the state object contains necessary constructor args like 'windowSize')
1458
+ auto newStage = it->second(stageState);
1459
+
1460
+ // Restore the full internal buffer state
1461
+ newStage->deserializeState(stageState);
1462
+
1463
+ // Add to our new pipeline
1464
+ newStages.push_back(std::move(newStage));
1465
+ }
1466
+ catch (const std::exception &e)
1467
+ {
1468
+ // If reconstruction fails (e.g. missing params), log warning and skip
1469
+ std::cerr << "Warning: Failed to reconstruct stage " << savedType
1470
+ << ": " << e.what() << std::endl;
1471
+ }
1472
+ }
1473
+ else
1474
+ {
1475
+ // Factory not found (e.g. generic FilterStage 'filter:fir' which requires explicit coefficients)
1476
+ std::cerr << "Warning: Unknown stage type '" << savedType
1477
+ << "' in saved state (no factory available). Skipping." << std::endl;
1478
+ }
1382
1479
  }
1383
1480
  }
1384
1481
 
1385
- std::cout << "State restoration complete!" << std::endl;
1482
+ // 3. Append any remaining stages from the user's current pipeline definition
1483
+ while (currentIdx < m_stages.size())
1484
+ {
1485
+ newStages.push_back(std::move(m_stages[currentIdx]));
1486
+ currentIdx++;
1487
+ }
1488
+
1489
+ // Replace the pipeline with the new merged structure
1490
+ m_stages = std::move(newStages);
1386
1491
 
1387
1492
  return Napi::Boolean::New(env, true);
1388
1493
  }
@@ -153,6 +153,22 @@ namespace dsp::adapters
153
153
  Napi::Object state = Napi::Object::New(env);
154
154
  state.Set("filterType", m_isFir ? "fir" : "iir");
155
155
 
156
+ // --- SAVE COEFFICIENTS (New) ---
157
+ Napi::Array bArray = Napi::Array::New(env, m_bCoeffs.size());
158
+ for (size_t i = 0; i < m_bCoeffs.size(); ++i)
159
+ {
160
+ bArray.Set(i, Napi::Number::New(env, m_bCoeffs[i]));
161
+ }
162
+ state.Set("bCoeffs", bArray);
163
+
164
+ Napi::Array aArray = Napi::Array::New(env, m_aCoeffs.size());
165
+ for (size_t i = 0; i < m_aCoeffs.size(); ++i)
166
+ {
167
+ aArray.Set(i, Napi::Number::New(env, m_aCoeffs[i]));
168
+ }
169
+ state.Set("aCoeffs", aArray);
170
+ // --------------------------------
171
+
156
172
  if (m_isFir)
157
173
  {
158
174
  state.Set("numChannels", static_cast<uint32_t>(m_firFilters.size()));
@@ -287,4 +303,4 @@ namespace dsp::adapters
287
303
  }
288
304
  }
289
305
 
290
- } // namespace dsp::adapters
306
+ } // namespace dsp::adapters
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include "../IDspStage.h"
4
+ #include "../core/PeakDetection.h"
4
5
  #include <cmath>
5
6
  #include <stdexcept>
6
7
  #include <string>
@@ -11,34 +12,52 @@ namespace dsp::adapters
11
12
  /**
12
13
  * @brief Peak Detection Stage - Detects local maxima in a signal.
13
14
  *
14
- * This stage identifies peaks (local maxima) in the input signal using a simple
15
- * three-point comparison method. A peak is detected when:
16
- * 1. The previous sample is greater than the sample before it
17
- * 2. The previous sample is greater than the current sample
18
- * 3. The previous sample exceeds the threshold
15
+ * ... (docs updated) ...
19
16
  *
20
- * **Use Cases:**
21
- * - Heart rate detection (R-peaks in ECG)
22
- * - Event detection in sensor data
23
- * - Tempo detection in audio
24
- * - Spike detection in neural recordings
25
- *
26
- * **Output:**
27
- * - 1.0 at peak locations
28
- * - 0.0 elsewhere
29
- *
30
- * **State:** Maintains 2 previous samples per channel for continuity
17
+ * **Parameters:**
18
+ * - threshold: Minimum value for a peak.
19
+ * - mode: 'moving' (stateful) or 'batch' (stateless).
20
+ * - domain: 'time' or 'frequency'.
21
+ * - windowSize: (Batch mode only) Local neighborhood size (odd, >= 3). Default 3.
22
+ * - minPeakDistance: Minimum samples between peaks. Default 1.
31
23
  */
32
24
  class PeakDetectionStage : public IDspStage
33
25
  {
34
26
  public:
35
- explicit PeakDetectionStage(float threshold)
36
- : m_threshold(threshold), m_num_channels(0)
27
+ explicit PeakDetectionStage(float threshold, std::string mode = "moving", std::string domain = "time", int windowSize = 3, int minPeakDistance = 1)
28
+ : m_threshold(threshold),
29
+ m_mode(std::move(mode)),
30
+ m_domain(std::move(domain)),
31
+ m_windowSize(windowSize),
32
+ m_minPeakDistance(minPeakDistance),
33
+ m_num_channels(0)
37
34
  {
38
35
  if (threshold < 0.0f)
39
36
  {
40
37
  throw std::invalid_argument("PeakDetection: threshold must be >= 0");
41
38
  }
39
+ if (m_mode != "moving" && m_mode != "batch")
40
+ {
41
+ throw std::invalid_argument("PeakDetection: mode must be 'moving' or 'batch'");
42
+ }
43
+ if (m_domain != "time" && m_domain != "frequency")
44
+ {
45
+ throw std::invalid_argument("PeakDetection: domain must be 'time' or 'frequency'");
46
+ }
47
+ if (windowSize < 3 || windowSize % 2 == 0)
48
+ {
49
+ throw std::invalid_argument("PeakDetection: windowSize must be an odd number >= 3");
50
+ }
51
+ if (minPeakDistance < 1)
52
+ {
53
+ throw std::invalid_argument("PeakDetection: minPeakDistance must be >= 1");
54
+ }
55
+
56
+ // In moving mode, we only support the 3-point window
57
+ if (m_mode == "moving")
58
+ {
59
+ m_windowSize = 3;
60
+ }
42
61
  }
43
62
 
44
63
  const char *getType() const override
@@ -48,77 +67,43 @@ namespace dsp::adapters
48
67
 
49
68
  void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
50
69
  {
51
- if (m_num_channels != numChannels || m_prev_sample.empty())
70
+ if (m_mode == "moving")
52
71
  {
53
- m_num_channels = numChannels;
54
- m_prev_sample.resize(numChannels, 0.0f);
55
- m_prev_prev_sample.resize(numChannels, 0.0f);
72
+ // Note: processMoving implicitly uses m_windowSize = 3
73
+ processMoving(buffer, numSamples, numChannels);
56
74
  }
57
-
58
- size_t samplesPerChannel = numSamples / numChannels;
59
-
60
- for (int ch = 0; ch < numChannels; ++ch)
75
+ else // m_mode == "batch"
61
76
  {
62
- float prev_prev = m_prev_prev_sample[ch];
63
- float prev = m_prev_sample[ch];
64
-
65
- for (size_t i = 0; i < samplesPerChannel; ++i)
66
- {
67
- size_t idx = i * numChannels + ch;
68
- float current = buffer[idx];
69
-
70
- // Check if `prev` (the previous sample) was a peak
71
- // We can now confirm this since we have the current sample
72
- bool prev_is_peak = (prev > prev_prev) && (prev > current) && (prev >= m_threshold);
73
-
74
- if (i > 0)
75
- {
76
- // Write peak marker BACK to the previous sample's position
77
- buffer[idx - numChannels] = prev_is_peak ? 1.0f : 0.0f;
78
- }
79
- else
80
- {
81
- // For the first sample, we mark it if the prev (from previous process call) was a peak
82
- buffer[idx] = prev_is_peak ? 1.0f : 0.0f;
83
- }
84
-
85
- // Shift history forward
86
- prev_prev = prev;
87
- prev = current;
88
- }
89
-
90
- // The last sample in the buffer can't be confirmed as a peak yet
91
- // (we need to see the next sample), so set it to 0
92
- // BUT: Don't overwrite if there's only 1 sample and we just wrote the pending result at i==0
93
- if (samplesPerChannel > 1)
94
- {
95
- buffer[(samplesPerChannel - 1) * numChannels + ch] = 0.0f;
96
- }
97
-
98
- // Save state for next process call
99
- m_prev_prev_sample[ch] = prev_prev;
100
- m_prev_sample[ch] = prev;
77
+ processBatch(buffer, numSamples, numChannels);
101
78
  }
102
79
  }
80
+
103
81
  Napi::Object serializeState(Napi::Env env) const override
104
82
  {
105
83
  Napi::Object state = Napi::Object::New(env);
106
84
  state.Set("threshold", m_threshold);
107
85
  state.Set("numChannels", m_num_channels);
86
+ state.Set("mode", m_mode);
87
+ state.Set("domain", m_domain);
88
+ state.Set("windowSize", m_windowSize);
89
+ state.Set("minPeakDistance", m_minPeakDistance);
108
90
 
109
- if (!m_prev_sample.empty())
91
+ if (m_mode == "moving" && !m_prev_sample.empty())
110
92
  {
111
93
  Napi::Array prevArray = Napi::Array::New(env, m_prev_sample.size());
112
94
  Napi::Array prevPrevArray = Napi::Array::New(env, m_prev_prev_sample.size());
95
+ Napi::Array cooldownArray = Napi::Array::New(env, m_peakCooldown.size());
113
96
 
114
97
  for (size_t i = 0; i < m_prev_sample.size(); ++i)
115
98
  {
116
99
  prevArray.Set(i, m_prev_sample[i]);
117
100
  prevPrevArray.Set(i, m_prev_prev_sample[i]);
101
+ cooldownArray.Set(i, m_peakCooldown[i]);
118
102
  }
119
103
 
120
104
  state.Set("prevSample", prevArray);
121
105
  state.Set("prevPrevSample", prevPrevArray);
106
+ state.Set("peakCooldown", cooldownArray);
122
107
  }
123
108
 
124
109
  return state;
@@ -130,19 +115,30 @@ namespace dsp::adapters
130
115
  m_threshold = state.Get("threshold").As<Napi::Number>().FloatValue();
131
116
  if (state.Has("numChannels"))
132
117
  m_num_channels = state.Get("numChannels").As<Napi::Number>().Int32Value();
118
+ if (state.Has("mode"))
119
+ m_mode = state.Get("mode").As<Napi::String>().Utf8Value();
120
+ if (state.Has("domain"))
121
+ m_domain = state.Get("domain").As<Napi::String>().Utf8Value();
122
+ if (state.Has("windowSize"))
123
+ m_windowSize = state.Get("windowSize").As<Napi::Number>().Int32Value();
124
+ if (state.Has("minPeakDistance"))
125
+ m_minPeakDistance = state.Get("minPeakDistance").As<Napi::Number>().Int32Value();
133
126
 
134
- if (state.Has("prevSample"))
127
+ if (m_mode == "moving" && state.Has("prevSample"))
135
128
  {
136
129
  Napi::Array prevArray = state.Get("prevSample").As<Napi::Array>();
137
130
  Napi::Array prevPrevArray = state.Get("prevPrevSample").As<Napi::Array>();
131
+ Napi::Array cooldownArray = state.Get("peakCooldown").As<Napi::Array>();
138
132
 
139
133
  m_prev_sample.clear();
140
134
  m_prev_prev_sample.clear();
135
+ m_peakCooldown.clear();
141
136
 
142
137
  for (size_t i = 0; i < prevArray.Length(); ++i)
143
138
  {
144
139
  m_prev_sample.push_back(prevArray.Get(i).As<Napi::Number>().FloatValue());
145
140
  m_prev_prev_sample.push_back(prevPrevArray.Get(i).As<Napi::Number>().FloatValue());
141
+ m_peakCooldown.push_back(cooldownArray.Get(i).As<Napi::Number>().Int32Value());
146
142
  }
147
143
  }
148
144
  }
@@ -151,15 +147,132 @@ namespace dsp::adapters
151
147
  {
152
148
  std::fill(m_prev_sample.begin(), m_prev_sample.end(), 0.0f);
153
149
  std::fill(m_prev_prev_sample.begin(), m_prev_prev_sample.end(), 0.0f);
150
+ std::fill(m_peakCooldown.begin(), m_peakCooldown.end(), 0);
154
151
  }
155
152
 
156
153
  bool isResizing() const override { return false; }
157
154
 
158
155
  private:
156
+ /**
157
+ * @brief Stateful ("moving") peak detection processing (WindowSize = 3 only).
158
+ */
159
+ void processMoving(float *buffer, size_t numSamples, int numChannels)
160
+ {
161
+ if (m_num_channels != numChannels || m_prev_sample.empty())
162
+ {
163
+ m_num_channels = numChannels;
164
+ m_prev_sample.resize(numChannels, 0.0f);
165
+ m_prev_prev_sample.resize(numChannels, 0.0f);
166
+ m_peakCooldown.resize(numChannels, 0); // Resize cooldown state
167
+ }
168
+
169
+ size_t samplesPerChannel = numSamples / numChannels;
170
+
171
+ for (int ch = 0; ch < numChannels; ++ch)
172
+ {
173
+ float prev_prev = m_prev_prev_sample[ch];
174
+ float prev = m_prev_sample[ch];
175
+ int &cooldown = m_peakCooldown[ch]; // Get reference to cooldown counter
176
+
177
+ for (size_t i = 0; i < samplesPerChannel; ++i)
178
+ {
179
+ size_t idx = i * numChannels + ch;
180
+ float current = buffer[idx];
181
+
182
+ // Decrement cooldown if active
183
+ if (cooldown > 0)
184
+ {
185
+ cooldown--;
186
+ }
187
+
188
+ // Check if `prev` (the previous sample) was a peak
189
+ bool prev_is_peak = (cooldown == 0) && // Must not be in cooldown
190
+ (prev > prev_prev) &&
191
+ (prev > current) &&
192
+ (prev >= m_threshold);
193
+
194
+ if (i > 0)
195
+ {
196
+ buffer[idx - numChannels] = prev_is_peak ? 1.0f : 0.0f;
197
+ }
198
+ else
199
+ {
200
+ buffer[idx] = prev_is_peak ? 1.0f : 0.0f;
201
+ }
202
+
203
+ // If we found a peak, reset the cooldown
204
+ if (prev_is_peak)
205
+ {
206
+ cooldown = m_minPeakDistance - 1;
207
+ }
208
+
209
+ // Shift history forward
210
+ prev_prev = prev;
211
+ prev = current;
212
+ }
213
+
214
+ if (samplesPerChannel > 1)
215
+ {
216
+ buffer[(samplesPerChannel - 1) * numChannels + ch] = 0.0f;
217
+ }
218
+
219
+ m_prev_prev_sample[ch] = prev_prev;
220
+ m_prev_sample[ch] = prev;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * @brief Stateless ("batch") peak detection processing.
226
+ */
227
+ void processBatch(float *buffer, size_t numSamples, int numChannels)
228
+ {
229
+ if (m_planar_input.size() < numSamples / numChannels)
230
+ {
231
+ m_planar_input.resize(numSamples / numChannels);
232
+ m_planar_output.resize(numSamples / numChannels);
233
+ }
234
+
235
+ size_t samplesPerChannel = numSamples / numChannels;
236
+
237
+ // Select the correct core function
238
+ void (*peak_func)(const float *, float *, size_t, float, int, int) =
239
+ (m_domain == "frequency")
240
+ ? dsp::core::find_freq_peaks_batch
241
+ : dsp::core::find_peaks_batch_delayed;
242
+
243
+ for (int ch = 0; ch < numChannels; ++ch)
244
+ {
245
+ for (size_t i = 0; i < samplesPerChannel; ++i)
246
+ {
247
+ m_planar_input[i] = buffer[i * numChannels + ch];
248
+ }
249
+
250
+ // Process using the core function, passing all parameters
251
+ peak_func(m_planar_input.data(), m_planar_output.data(), samplesPerChannel, m_threshold, m_windowSize, m_minPeakDistance);
252
+
253
+ for (size_t i = 0; i < samplesPerChannel; ++i)
254
+ {
255
+ buffer[i * numChannels + ch] = m_planar_output[i];
256
+ }
257
+ }
258
+ }
259
+
260
+ // --- Member Variables ---
159
261
  float m_threshold;
262
+ std::string m_mode;
263
+ std::string m_domain;
264
+ int m_windowSize;
265
+ int m_minPeakDistance;
160
266
  int m_num_channels;
267
+
268
+ // State for "moving" mode
161
269
  std::vector<float> m_prev_sample;
162
270
  std::vector<float> m_prev_prev_sample;
271
+ std::vector<int> m_peakCooldown; // Cooldown counter per channel
272
+
273
+ // Temporary planar buffers for "batch" mode
274
+ std::vector<float> m_planar_input;
275
+ std::vector<float> m_planar_output;
163
276
  };
164
277
 
165
- } // namespace dsp::adapters
278
+ } // namespace dsp::adapters
@@ -152,7 +152,7 @@ namespace dsp::adapters
152
152
  {
153
153
  actualSumOfSquares += val * val;
154
154
  }
155
- const float tolerance = 0.0001f * std::max(1.0f, std::abs(actualSumOfSquares));
155
+ const float tolerance = 0.001f * std::max(1.0f, std::abs(actualSumOfSquares));
156
156
  if (std::abs(runningSumOfSquares - actualSumOfSquares) > tolerance)
157
157
  {
158
158
  throw std::runtime_error(
@@ -165,7 +165,7 @@ namespace dsp::adapters
165
165
  std::to_string(runningSum));
166
166
  }
167
167
 
168
- const float toleranceSq = 0.0001f * std::max(1.0f, std::abs(actualSumOfSquares));
168
+ const float toleranceSq = 0.001f * std::max(1.0f, std::abs(actualSumOfSquares));
169
169
  if (std::abs(runningSumOfSquares - actualSumOfSquares) > toleranceSq)
170
170
  {
171
171
  throw std::runtime_error(
@@ -170,7 +170,7 @@ namespace dsp::adapters
170
170
  std::to_string(runningSum));
171
171
  }
172
172
 
173
- const float toleranceSq = 0.0001f * std::max(1.0f, std::abs(actualSumOfSquares));
173
+ const float toleranceSq = 0.001f * std::max(1.0f, std::abs(actualSumOfSquares));
174
174
  if (std::abs(runningSumOfSquares - actualSumOfSquares) > toleranceSq)
175
175
  {
176
176
  throw std::runtime_error(