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.
- package/dist/bindings.d.ts +33 -10
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +68 -12
- package/dist/bindings.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/src/native/DspPipeline.cc +132 -27
- package/src/native/adapters/FilterStage.cc +17 -1
- package/src/native/adapters/PeakDetectionStage.h +181 -68
- package/src/native/adapters/RmsStage.h +1 -1
- package/src/native/adapters/VarianceStage.h +1 -1
- package/src/native/adapters/ZScoreNormalizeStage.h +1 -1
- package/src/native/core/PeakDetection.h +225 -0
|
@@ -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
|
-
|
|
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 ¶ms)
|
|
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
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
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
|
|
1412
|
+
uint32_t savedStageCount = stagesArray.Length();
|
|
1361
1413
|
|
|
1362
|
-
//
|
|
1363
|
-
|
|
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
|
-
//
|
|
1372
|
-
|
|
1417
|
+
// Index to track our position in the *current* pipeline (m_stages)
|
|
1418
|
+
size_t currentIdx = 0;
|
|
1373
1419
|
|
|
1374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* **
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
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),
|
|
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 (
|
|
70
|
+
if (m_mode == "moving")
|
|
52
71
|
{
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|