dspx 1.1.5 → 1.2.3
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/README.md +31 -0
- package/dist/bindings.d.ts +59 -14
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +93 -16
- 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 +22 -1
- package/src/native/adapters/ConvolutionStage.h +254 -183
- package/src/native/adapters/PeakDetectionStage.h +181 -68
- package/src/native/core/PeakDetection.h +225 -0
|
@@ -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
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <algorithm>
|
|
7
|
+
#include "../utils/SimdOps.h"
|
|
8
|
+
|
|
9
|
+
namespace dsp::core
|
|
10
|
+
{
|
|
11
|
+
/**
|
|
12
|
+
* @brief Post-processing pass to enforce minimum peak distance.
|
|
13
|
+
* Modifies the output buffer in-place.
|
|
14
|
+
*/
|
|
15
|
+
inline void apply_min_peak_distance(float *output, size_t size, int minPeakDistance)
|
|
16
|
+
{
|
|
17
|
+
if (minPeakDistance <= 1)
|
|
18
|
+
{
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
int cooldown = 0;
|
|
23
|
+
for (size_t i = 0; i < size; ++i)
|
|
24
|
+
{
|
|
25
|
+
if (output[i] == 1.0f)
|
|
26
|
+
{
|
|
27
|
+
if (cooldown == 0)
|
|
28
|
+
{
|
|
29
|
+
// This is a valid peak, start cooldown
|
|
30
|
+
cooldown = minPeakDistance - 1;
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
{
|
|
34
|
+
// This peak is suppressed
|
|
35
|
+
output[i] = 0.0f;
|
|
36
|
+
cooldown--; // Still decrement cooldown
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (cooldown > 0)
|
|
40
|
+
{
|
|
41
|
+
cooldown--;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @brief Scalar (flexible) delayed peak detection.
|
|
48
|
+
* This is the fallback for windowSize != 3.
|
|
49
|
+
*/
|
|
50
|
+
inline void find_peaks_batch_scalar_delayed(const float *input, float *output, size_t size, float threshold, int windowSize)
|
|
51
|
+
{
|
|
52
|
+
// We check a peak at [i-k] when we see sample [i].
|
|
53
|
+
// This maintains the "delayed" logic.
|
|
54
|
+
const int k = (windowSize - 1) / 2;
|
|
55
|
+
const int end_idx = windowSize - 1;
|
|
56
|
+
|
|
57
|
+
if (size < windowSize)
|
|
58
|
+
{
|
|
59
|
+
return; // Not enough samples
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (size_t i = end_idx; i < size; ++i)
|
|
63
|
+
{
|
|
64
|
+
const size_t peak_idx = i - k; // The candidate peak index
|
|
65
|
+
const float candidate = input[peak_idx];
|
|
66
|
+
|
|
67
|
+
if (candidate < threshold)
|
|
68
|
+
{
|
|
69
|
+
output[peak_idx] = 0.0f;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
bool is_max = true;
|
|
74
|
+
// Check all neighbors from [i-k-k] to [i-k+k] (which is [i-end_idx] to [i])
|
|
75
|
+
for (int j = -k; j <= k; ++j)
|
|
76
|
+
{
|
|
77
|
+
if (j == 0)
|
|
78
|
+
continue; // Don't compare to self
|
|
79
|
+
|
|
80
|
+
if (candidate <= input[peak_idx + j])
|
|
81
|
+
{
|
|
82
|
+
is_max = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
output[peak_idx] = is_max ? 1.0f : 0.0f;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @brief Stateless (batch) peak detection using the 3-point delayed method.
|
|
93
|
+
*
|
|
94
|
+
* Finds local maxima in a buffer.
|
|
95
|
+
*
|
|
96
|
+
* @param input The input signal buffer.
|
|
97
|
+
* @param output The output buffer (will be filled with 1.0 at peaks, 0.0 otherwise).
|
|
98
|
+
* @param size The number of samples in the input/output buffers.
|
|
99
|
+
* @param threshold The minimum value for a peak to be considered.
|
|
100
|
+
* @param windowSize The local neighborhood size (must be odd, >= 3).
|
|
101
|
+
* @param minPeakDistance The minimum samples between peaks.
|
|
102
|
+
*
|
|
103
|
+
* @note Output edges (0..k and size-k..size) will be 0.
|
|
104
|
+
*/
|
|
105
|
+
inline void find_peaks_batch_delayed(const float *input, float *output, size_t size, float threshold, int windowSize, int minPeakDistance)
|
|
106
|
+
{
|
|
107
|
+
// Initialize output to 0.0
|
|
108
|
+
std::fill(output, output + size, 0.0f);
|
|
109
|
+
|
|
110
|
+
if (windowSize < 3 || windowSize % 2 == 0)
|
|
111
|
+
{
|
|
112
|
+
return; // Invalid window size
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (size < windowSize)
|
|
116
|
+
{
|
|
117
|
+
return; // Not enough samples
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (windowSize == 3)
|
|
121
|
+
{
|
|
122
|
+
// --- Optimized SIMD Path for windowSize = 3 ---
|
|
123
|
+
size_t i = 2;
|
|
124
|
+
|
|
125
|
+
#if defined(SIMD_AVX2)
|
|
126
|
+
const size_t simd_width = 8;
|
|
127
|
+
const size_t simd_limit = (size >= simd_width + 2) ? (size - simd_width) : 0;
|
|
128
|
+
const __m256 v_thresh = _mm256_set1_ps(threshold);
|
|
129
|
+
const __m256 v_ones = _mm256_set1_ps(1.0f);
|
|
130
|
+
|
|
131
|
+
for (i = 2; i <= simd_limit; i += simd_width)
|
|
132
|
+
{
|
|
133
|
+
__m256 v_prev_prev = _mm256_loadu_ps(&input[i - 2]);
|
|
134
|
+
__m256 v_prev = _mm256_loadu_ps(&input[i - 1]);
|
|
135
|
+
__m256 v_current = _mm256_loadu_ps(&input[i]);
|
|
136
|
+
|
|
137
|
+
__m256 mask1 = _mm256_cmp_ps(v_prev, v_prev_prev, _CMP_GT_OQ);
|
|
138
|
+
__m256 mask2 = _mm256_cmp_ps(v_prev, v_current, _CMP_GT_OQ);
|
|
139
|
+
__m256 mask3 = _mm256_cmp_ps(v_prev, v_thresh, _CMP_GE_OQ);
|
|
140
|
+
|
|
141
|
+
__m256 mask = _mm256_and_ps(_mm256_and_ps(mask1, mask2), mask3);
|
|
142
|
+
__m256 v_result = _mm256_and_ps(mask, v_ones);
|
|
143
|
+
_mm256_storeu_ps(&output[i - 1], v_result);
|
|
144
|
+
}
|
|
145
|
+
#elif defined(SIMD_SSE2)
|
|
146
|
+
// ... (Same SSE2 logic as before) ...
|
|
147
|
+
const size_t simd_width = 4;
|
|
148
|
+
const size_t simd_limit = (size >= simd_width + 2) ? (size - simd_width) : 0;
|
|
149
|
+
const __m128 v_thresh = _mm_set1_ps(threshold);
|
|
150
|
+
const __m128 v_ones = _mm_set1_ps(1.0f);
|
|
151
|
+
|
|
152
|
+
for (i = 2; i <= simd_limit; i += simd_width)
|
|
153
|
+
{
|
|
154
|
+
__m128 v_prev_prev = _mm_loadu_ps(&input[i - 2]);
|
|
155
|
+
__m128 v_prev = _mm_loadu_ps(&input[i - 1]);
|
|
156
|
+
__m128 v_current = _mm_loadu_ps(&input[i]);
|
|
157
|
+
|
|
158
|
+
__m128 mask1 = _mm_cmpgt_ps(v_prev, v_prev_prev);
|
|
159
|
+
__m128 mask2 = _mm_cmpgt_ps(v_prev, v_current);
|
|
160
|
+
__m128 mask3 = _mm_cmpge_ps(v_prev, v_thresh);
|
|
161
|
+
|
|
162
|
+
__m128 mask = _mm_and_ps(_mm_and_ps(mask1, mask2), mask3);
|
|
163
|
+
__m128 v_result = _mm_and_ps(mask, v_ones);
|
|
164
|
+
_mm_storeu_ps(&output[i - 1], v_result);
|
|
165
|
+
}
|
|
166
|
+
#elif defined(SIMD_NEON)
|
|
167
|
+
// ... (Same NEON logic as before) ...
|
|
168
|
+
const size_t simd_width = 4;
|
|
169
|
+
const size_t simd_limit = (size >= simd_width + 2) ? (size - simd_width) : 0;
|
|
170
|
+
const float32x4_t v_thresh = vdupq_n_f32(threshold);
|
|
171
|
+
const float32x4_t v_ones = vdupq_n_f32(1.0f);
|
|
172
|
+
const float32x4_t v_zeros = vdupq_n_f32(0.0f);
|
|
173
|
+
|
|
174
|
+
for (i = 2; i <= simd_limit; i += simd_width)
|
|
175
|
+
{
|
|
176
|
+
float32x4_t v_prev_prev = vld1q_f32(&input[i - 2]);
|
|
177
|
+
float32x4_t v_prev = vld1q_f32(&input[i - 1]);
|
|
178
|
+
float32x4_t v_current = vld1q_f32(&input[i]);
|
|
179
|
+
|
|
180
|
+
uint32x4_t mask1 = vcgtq_f32(v_prev, v_prev_prev);
|
|
181
|
+
uint32x4_t mask2 = vcgtq_f32(v_prev, v_current);
|
|
182
|
+
uint32x4_t mask3 = vcgeq_f32(v_prev, v_thresh);
|
|
183
|
+
|
|
184
|
+
uint32x4_t mask = vandq_u32(vandq_u32(mask1, mask2), mask3);
|
|
185
|
+
float32x4_t v_result = vbslq_f32(mask, v_ones, v_zeros);
|
|
186
|
+
vst1q_f32(&output[i - 1], v_result);
|
|
187
|
+
}
|
|
188
|
+
#endif
|
|
189
|
+
// Handle remainder for windowSize = 3
|
|
190
|
+
for (; i < size; ++i)
|
|
191
|
+
{
|
|
192
|
+
const float prev_prev = input[i - 2];
|
|
193
|
+
const float prev = input[i - 1];
|
|
194
|
+
const float current = input[i];
|
|
195
|
+
|
|
196
|
+
bool prev_is_peak = (prev > prev_prev) && (prev > current) && (prev >= threshold);
|
|
197
|
+
if (prev_is_peak)
|
|
198
|
+
{
|
|
199
|
+
output[i - 1] = 1.0f;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
{
|
|
205
|
+
// --- Scalar Fallback Path for windowSize != 3 ---
|
|
206
|
+
find_peaks_batch_scalar_delayed(input, output, size, threshold, windowSize);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Post-processing for Minimum Peak Distance ---
|
|
210
|
+
if (minPeakDistance > 1)
|
|
211
|
+
{
|
|
212
|
+
apply_min_peak_distance(output, size, minPeakDistance);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @brief Stateless (batch) peak detection for frequency domain.
|
|
218
|
+
*/
|
|
219
|
+
inline void find_freq_peaks_batch(const float *input, float *output, size_t size, float threshold, int windowSize, int minPeakDistance)
|
|
220
|
+
{
|
|
221
|
+
// The algorithm is identical to the time-domain batch version.
|
|
222
|
+
find_peaks_batch_delayed(input, output, size, threshold, windowSize, minPeakDistance);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
} // namespace dsp::core
|