dspx 1.2.3 → 1.3.0
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 +40 -78
- package/binding.gyp +10 -0
- package/dist/FilterBankDesign.d.ts +233 -0
- package/dist/FilterBankDesign.d.ts.map +1 -0
- package/dist/FilterBankDesign.js +247 -0
- package/dist/FilterBankDesign.js.map +1 -0
- package/dist/advanced-dsp.d.ts +6 -6
- package/dist/advanced-dsp.d.ts.map +1 -1
- package/dist/advanced-dsp.js +35 -12
- package/dist/advanced-dsp.js.map +1 -1
- package/dist/backends.d.ts +0 -103
- package/dist/backends.d.ts.map +1 -1
- package/dist/backends.js +0 -217
- package/dist/backends.js.map +1 -1
- package/dist/bindings.d.ts +216 -17
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +503 -42
- package/dist/bindings.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +67 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +38 -8
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +84 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -2
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/scripts/add-dispose-to-tests.js +145 -0
- package/src/native/DspPipeline.cc +777 -143
- package/src/native/DspPipeline.h +13 -0
- package/src/native/FilterBankDesignBindings.cc +241 -0
- package/src/native/IDspStage.h +24 -0
- package/src/native/UtilityBindings.cc +130 -0
- package/src/native/adapters/ClipDetectionStage.h +15 -4
- package/src/native/adapters/ConvolutionStage.h +101 -0
- package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
- package/src/native/adapters/DecimatorStage.h +80 -0
- package/src/native/adapters/DifferentiatorStage.h +13 -0
- package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
- package/src/native/adapters/FilterBankStage.cc +336 -0
- package/src/native/adapters/FilterBankStage.h +170 -0
- package/src/native/adapters/FilterStage.cc +139 -1
- package/src/native/adapters/FilterStage.h +4 -0
- package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
- package/src/native/adapters/IntegratorStage.h +15 -0
- package/src/native/adapters/InterpolatorStage.h +51 -0
- package/src/native/adapters/LinearRegressionStage.h +40 -0
- package/src/native/adapters/LmsStage.h +63 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
- package/src/native/adapters/MovingAverageStage.h +119 -0
- package/src/native/adapters/PeakDetectionStage.h +53 -0
- package/src/native/adapters/RectifyStage.h +14 -0
- package/src/native/adapters/ResamplerStage.h +67 -0
- package/src/native/adapters/RlsStage.h +76 -0
- package/src/native/adapters/RmsStage.h +73 -1
- package/src/native/adapters/SnrStage.h +45 -0
- package/src/native/adapters/SscStage.h +65 -0
- package/src/native/adapters/StftStage.h +62 -0
- package/src/native/adapters/VarianceStage.h +60 -1
- package/src/native/adapters/WampStage.h +59 -0
- package/src/native/adapters/WaveformLengthStage.h +51 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +65 -1
- package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
- package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
- package/src/native/core/FilterBankDesign.h +266 -0
- package/src/native/core/Policies.h +124 -0
- package/src/native/utils/CircularBufferArray.cc +2 -1
- package/src/native/utils/Toon.h +195 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/CumulativeMovingAverageFilter.h"
|
|
5
|
+
#include "../utils/SimdOps.h"
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <string>
|
|
9
|
+
|
|
10
|
+
namespace dsp::adapters
|
|
11
|
+
{
|
|
12
|
+
enum class CmaMode
|
|
13
|
+
{
|
|
14
|
+
Batch,
|
|
15
|
+
Moving
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class CumulativeMovingAverageStage : public IDspStage
|
|
19
|
+
{
|
|
20
|
+
public:
|
|
21
|
+
/**
|
|
22
|
+
* @brief Constructs a new Cumulative Moving Average Stage.
|
|
23
|
+
* @param mode The averaging mode (Batch or Moving).
|
|
24
|
+
*/
|
|
25
|
+
explicit CumulativeMovingAverageStage(CmaMode mode)
|
|
26
|
+
: m_mode(mode)
|
|
27
|
+
{
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Return the type identifier for this stage
|
|
31
|
+
const char *getType() const override
|
|
32
|
+
{
|
|
33
|
+
return "cumulativeMovingAverage";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// This is the implementation of the interface method
|
|
37
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
|
|
38
|
+
{
|
|
39
|
+
if (m_mode == CmaMode::Batch)
|
|
40
|
+
{
|
|
41
|
+
processBatch(buffer, numSamples, numChannels);
|
|
42
|
+
}
|
|
43
|
+
else // CmaMode::Moving
|
|
44
|
+
{
|
|
45
|
+
processMoving(buffer, numSamples, numChannels);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Serialize the stage's state to a Napi::Object
|
|
50
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
51
|
+
{
|
|
52
|
+
Napi::Object state = Napi::Object::New(env);
|
|
53
|
+
std::string modeStr = (m_mode == CmaMode::Moving) ? "moving" : "batch";
|
|
54
|
+
state.Set("mode", modeStr);
|
|
55
|
+
|
|
56
|
+
if (m_mode == CmaMode::Moving)
|
|
57
|
+
{
|
|
58
|
+
state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
|
|
59
|
+
|
|
60
|
+
// Serialize each channel's filter state
|
|
61
|
+
Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
|
|
62
|
+
for (size_t i = 0; i < m_filters.size(); ++i)
|
|
63
|
+
{
|
|
64
|
+
Napi::Object channelState = Napi::Object::New(env);
|
|
65
|
+
|
|
66
|
+
// Get the filter's internal state
|
|
67
|
+
auto [sum, count] = m_filters[i].getState();
|
|
68
|
+
|
|
69
|
+
channelState.Set("sum", Napi::Number::New(env, sum));
|
|
70
|
+
channelState.Set("count", Napi::Number::New(env, static_cast<uint32_t>(count)));
|
|
71
|
+
|
|
72
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
73
|
+
}
|
|
74
|
+
state.Set("channels", channelsArray);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Deserialize and restore the stage's state
|
|
81
|
+
void deserializeState(const Napi::Object &state) override
|
|
82
|
+
{
|
|
83
|
+
std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
|
|
84
|
+
CmaMode newMode = (modeStr == "moving") ? CmaMode::Moving : CmaMode::Batch;
|
|
85
|
+
|
|
86
|
+
if (newMode != m_mode)
|
|
87
|
+
{
|
|
88
|
+
throw std::runtime_error("CumulativeMovingAverage mode mismatch during deserialization");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (m_mode == CmaMode::Moving)
|
|
92
|
+
{
|
|
93
|
+
// Get number of channels
|
|
94
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
95
|
+
|
|
96
|
+
// Recreate filters
|
|
97
|
+
m_filters.clear();
|
|
98
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
99
|
+
{
|
|
100
|
+
m_filters.emplace_back();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Restore each channel's state
|
|
104
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
105
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
106
|
+
{
|
|
107
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
108
|
+
|
|
109
|
+
float sum = channelState.Get("sum").As<Napi::Number>().FloatValue();
|
|
110
|
+
size_t count = channelState.Get("count").As<Napi::Number>().Uint32Value();
|
|
111
|
+
|
|
112
|
+
// Restore the filter's state
|
|
113
|
+
m_filters[i].setState(sum, count);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Reset all filters to initial state
|
|
119
|
+
void reset() override
|
|
120
|
+
{
|
|
121
|
+
for (auto &filter : m_filters)
|
|
122
|
+
{
|
|
123
|
+
filter.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// TOON Binary Serialization
|
|
128
|
+
void serializeToon(dsp::toon::Serializer &serializer) const override
|
|
129
|
+
{
|
|
130
|
+
serializer.startObject();
|
|
131
|
+
|
|
132
|
+
// 1. Mode
|
|
133
|
+
serializer.writeString("mode");
|
|
134
|
+
serializer.writeString((m_mode == CmaMode::Moving) ? "moving" : "batch");
|
|
135
|
+
|
|
136
|
+
if (m_mode == CmaMode::Moving)
|
|
137
|
+
{
|
|
138
|
+
// 2. Channels
|
|
139
|
+
serializer.writeString("channels");
|
|
140
|
+
serializer.startArray();
|
|
141
|
+
|
|
142
|
+
for (const auto &filter : m_filters)
|
|
143
|
+
{
|
|
144
|
+
auto [sum, count] = filter.getState();
|
|
145
|
+
|
|
146
|
+
serializer.startObject();
|
|
147
|
+
|
|
148
|
+
serializer.writeString("sum");
|
|
149
|
+
serializer.writeFloat(sum);
|
|
150
|
+
|
|
151
|
+
serializer.writeString("count");
|
|
152
|
+
serializer.writeInt32(static_cast<int32_t>(count));
|
|
153
|
+
|
|
154
|
+
serializer.endObject();
|
|
155
|
+
}
|
|
156
|
+
serializer.endArray();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
serializer.endObject();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// TOON Binary Deserialization
|
|
163
|
+
void deserializeToon(dsp::toon::Deserializer &deserializer) override
|
|
164
|
+
{
|
|
165
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_START);
|
|
166
|
+
|
|
167
|
+
// 1. Mode
|
|
168
|
+
std::string key = deserializer.readString(); // "mode"
|
|
169
|
+
std::string modeStr = deserializer.readString();
|
|
170
|
+
|
|
171
|
+
CmaMode newMode = (modeStr == "moving") ? CmaMode::Moving : CmaMode::Batch;
|
|
172
|
+
if (newMode != m_mode)
|
|
173
|
+
{
|
|
174
|
+
throw std::runtime_error("CumulativeMovingAverage mode mismatch during TOON deserialization");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (modeStr == "moving")
|
|
178
|
+
{
|
|
179
|
+
// 2. Channels
|
|
180
|
+
key = deserializer.readString(); // "channels"
|
|
181
|
+
deserializer.consumeToken(dsp::toon::T_ARRAY_START);
|
|
182
|
+
|
|
183
|
+
m_filters.clear();
|
|
184
|
+
|
|
185
|
+
while (deserializer.peekToken() != dsp::toon::T_ARRAY_END)
|
|
186
|
+
{
|
|
187
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_START);
|
|
188
|
+
|
|
189
|
+
// Sum
|
|
190
|
+
deserializer.readString(); // "sum"
|
|
191
|
+
float sum = deserializer.readFloat();
|
|
192
|
+
|
|
193
|
+
// Count
|
|
194
|
+
deserializer.readString(); // "count"
|
|
195
|
+
int32_t count = deserializer.readInt32();
|
|
196
|
+
|
|
197
|
+
// Reconstruct filter
|
|
198
|
+
m_filters.emplace_back();
|
|
199
|
+
m_filters.back().setState(sum, static_cast<size_t>(count));
|
|
200
|
+
|
|
201
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_END);
|
|
202
|
+
}
|
|
203
|
+
deserializer.consumeToken(dsp::toon::T_ARRAY_END);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_END);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private:
|
|
210
|
+
/**
|
|
211
|
+
* @brief Batch mode: Compute cumulative moving average over entire buffer per channel.
|
|
212
|
+
* Each sample in the channel is replaced by the cumulative average up to that point.
|
|
213
|
+
*/
|
|
214
|
+
void processBatch(float *buffer, size_t numSamples, int numChannels)
|
|
215
|
+
{
|
|
216
|
+
// Process each channel independently
|
|
217
|
+
for (int c = 0; c < numChannels; ++c)
|
|
218
|
+
{
|
|
219
|
+
float sum = 0.0f;
|
|
220
|
+
size_t count = 0;
|
|
221
|
+
|
|
222
|
+
// Process all samples for this channel
|
|
223
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
224
|
+
{
|
|
225
|
+
sum += buffer[i];
|
|
226
|
+
count++;
|
|
227
|
+
// CMA = sum / count
|
|
228
|
+
buffer[i] = sum / static_cast<float>(count);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @brief Moving mode: Statefully process samples using CMA filters with continuity.
|
|
235
|
+
* @param buffer The interleaved audio buffer.
|
|
236
|
+
* @param numSamples The total number of samples.
|
|
237
|
+
* @param numChannels The number of channels.
|
|
238
|
+
*/
|
|
239
|
+
void processMoving(float *buffer, size_t numSamples, int numChannels)
|
|
240
|
+
{
|
|
241
|
+
// Lazily initialize our filters, one for each channel
|
|
242
|
+
if (m_filters.size() != static_cast<size_t>(numChannels))
|
|
243
|
+
{
|
|
244
|
+
m_filters.clear();
|
|
245
|
+
for (int i = 0; i < numChannels; ++i)
|
|
246
|
+
{
|
|
247
|
+
m_filters.emplace_back();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Process the buffer sample by sample, de-interleaving
|
|
252
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
253
|
+
{
|
|
254
|
+
int channel = i % numChannels;
|
|
255
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
CmaMode m_mode;
|
|
260
|
+
// Separate filter instance for each channel's state
|
|
261
|
+
std::vector<dsp::core::CumulativeMovingAverageFilter<float>> m_filters;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
} // namespace dsp::adapters
|
|
@@ -215,6 +215,86 @@ namespace dsp
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
void serializeToon(dsp::toon::Serializer &s) const override
|
|
219
|
+
{
|
|
220
|
+
s.startObject();
|
|
221
|
+
|
|
222
|
+
s.writeString("factor");
|
|
223
|
+
s.writeInt32(decimationFactor_);
|
|
224
|
+
|
|
225
|
+
s.writeString("order");
|
|
226
|
+
s.writeInt32(filterOrder_);
|
|
227
|
+
|
|
228
|
+
s.writeString("sampleRate");
|
|
229
|
+
s.writeDouble(sampleRate_);
|
|
230
|
+
|
|
231
|
+
s.writeString("phaseIndex");
|
|
232
|
+
s.writeInt32(phaseIndex_);
|
|
233
|
+
|
|
234
|
+
s.writeString("numChannels");
|
|
235
|
+
s.writeInt32(numChannels_);
|
|
236
|
+
|
|
237
|
+
s.writeString("stateBuffer");
|
|
238
|
+
s.writeFloatArray(stateBuffer_);
|
|
239
|
+
|
|
240
|
+
s.writeString("stateIndices");
|
|
241
|
+
s.startArray();
|
|
242
|
+
for (size_t idx : stateIndices_)
|
|
243
|
+
{
|
|
244
|
+
s.writeInt32(static_cast<int32_t>(idx));
|
|
245
|
+
}
|
|
246
|
+
s.endArray();
|
|
247
|
+
|
|
248
|
+
s.endObject();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
void deserializeToon(dsp::toon::Deserializer &d) override
|
|
252
|
+
{
|
|
253
|
+
d.consumeToken(dsp::toon::T_OBJECT_START);
|
|
254
|
+
|
|
255
|
+
std::string key = d.readString(); // "factor"
|
|
256
|
+
int factor = d.readInt32();
|
|
257
|
+
if (factor != decimationFactor_)
|
|
258
|
+
{
|
|
259
|
+
throw std::runtime_error("Decimator factor mismatch during TOON deserialization");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
key = d.readString(); // "order"
|
|
263
|
+
int order = d.readInt32();
|
|
264
|
+
if (order != filterOrder_)
|
|
265
|
+
{
|
|
266
|
+
throw std::runtime_error("Decimator order mismatch during TOON deserialization");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
key = d.readString(); // "sampleRate"
|
|
270
|
+
d.readDouble(); // Skip sampleRate
|
|
271
|
+
|
|
272
|
+
key = d.readString(); // "phaseIndex"
|
|
273
|
+
phaseIndex_ = d.readInt32();
|
|
274
|
+
|
|
275
|
+
key = d.readString(); // "numChannels"
|
|
276
|
+
int numCh = d.readInt32();
|
|
277
|
+
|
|
278
|
+
if (numCh != numChannels_)
|
|
279
|
+
{
|
|
280
|
+
initializeStateBuffers(numCh);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
key = d.readString(); // "stateBuffer"
|
|
284
|
+
stateBuffer_ = d.readFloatArray();
|
|
285
|
+
|
|
286
|
+
key = d.readString(); // "stateIndices"
|
|
287
|
+
d.consumeToken(dsp::toon::T_ARRAY_START);
|
|
288
|
+
stateIndices_.clear();
|
|
289
|
+
while (d.peekToken() != dsp::toon::T_ARRAY_END)
|
|
290
|
+
{
|
|
291
|
+
stateIndices_.push_back(static_cast<size_t>(d.readInt32()));
|
|
292
|
+
}
|
|
293
|
+
d.consumeToken(dsp::toon::T_ARRAY_END);
|
|
294
|
+
|
|
295
|
+
d.consumeToken(dsp::toon::T_OBJECT_END);
|
|
296
|
+
}
|
|
297
|
+
|
|
218
298
|
/**
|
|
219
299
|
* Get decimation factor
|
|
220
300
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include "../IDspStage.h"
|
|
4
|
+
#include "../utils/Toon.h"
|
|
4
5
|
#include <stdexcept>
|
|
5
6
|
#include <string>
|
|
6
7
|
#include <vector>
|
|
@@ -92,6 +93,18 @@ namespace dsp::adapters
|
|
|
92
93
|
std::fill(m_prev_sample.begin(), m_prev_sample.end(), 0.0f);
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
void serializeToon(dsp::toon::Serializer &s) const override
|
|
97
|
+
{
|
|
98
|
+
s.writeInt32(m_num_channels);
|
|
99
|
+
s.writeFloatArray(m_prev_sample);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
void deserializeToon(dsp::toon::Deserializer &d) override
|
|
103
|
+
{
|
|
104
|
+
m_num_channels = d.readInt32();
|
|
105
|
+
m_prev_sample = d.readFloatArray();
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
bool isResizing() const override { return false; }
|
|
96
109
|
|
|
97
110
|
private:
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/ExponentialMovingAverageFilter.h"
|
|
5
|
+
#include "../utils/SimdOps.h"
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <string>
|
|
10
|
+
|
|
11
|
+
namespace dsp::adapters
|
|
12
|
+
{
|
|
13
|
+
enum class EmaMode
|
|
14
|
+
{
|
|
15
|
+
Batch,
|
|
16
|
+
Moving
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class ExponentialMovingAverageStage : public IDspStage
|
|
20
|
+
{
|
|
21
|
+
public:
|
|
22
|
+
/**
|
|
23
|
+
* @brief Constructs a new Exponential Moving Average Stage.
|
|
24
|
+
* @param mode The averaging mode (Batch or Moving).
|
|
25
|
+
* @param alpha The smoothing factor (0 < α ≤ 1).
|
|
26
|
+
*/
|
|
27
|
+
explicit ExponentialMovingAverageStage(EmaMode mode, float alpha)
|
|
28
|
+
: m_mode(mode),
|
|
29
|
+
m_alpha(alpha)
|
|
30
|
+
{
|
|
31
|
+
if (alpha <= 0.0f || alpha > 1.0f)
|
|
32
|
+
{
|
|
33
|
+
throw std::invalid_argument("ExponentialMovingAverage: alpha must be in range (0, 1]");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the type identifier for this stage
|
|
38
|
+
const char *getType() const override
|
|
39
|
+
{
|
|
40
|
+
return "exponentialMovingAverage";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// This is the implementation of the interface method
|
|
44
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
|
|
45
|
+
{
|
|
46
|
+
if (m_mode == EmaMode::Batch)
|
|
47
|
+
{
|
|
48
|
+
processBatch(buffer, numSamples, numChannels);
|
|
49
|
+
}
|
|
50
|
+
else // EmaMode::Moving
|
|
51
|
+
{
|
|
52
|
+
processMoving(buffer, numSamples, numChannels);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Serialize the stage's state to a Napi::Object
|
|
57
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
58
|
+
{
|
|
59
|
+
Napi::Object state = Napi::Object::New(env);
|
|
60
|
+
std::string modeStr = (m_mode == EmaMode::Moving) ? "moving" : "batch";
|
|
61
|
+
state.Set("mode", modeStr);
|
|
62
|
+
state.Set("alpha", Napi::Number::New(env, m_alpha));
|
|
63
|
+
|
|
64
|
+
if (m_mode == EmaMode::Moving)
|
|
65
|
+
{
|
|
66
|
+
state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
|
|
67
|
+
|
|
68
|
+
// Serialize each channel's filter state
|
|
69
|
+
Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
|
|
70
|
+
for (size_t i = 0; i < m_filters.size(); ++i)
|
|
71
|
+
{
|
|
72
|
+
Napi::Object channelState = Napi::Object::New(env);
|
|
73
|
+
|
|
74
|
+
// Get the filter's internal state
|
|
75
|
+
auto [ema, initialized] = m_filters[i].getState();
|
|
76
|
+
|
|
77
|
+
channelState.Set("ema", Napi::Number::New(env, ema));
|
|
78
|
+
channelState.Set("initialized", Napi::Boolean::New(env, initialized));
|
|
79
|
+
|
|
80
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
81
|
+
}
|
|
82
|
+
state.Set("channels", channelsArray);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Deserialize and restore the stage's state
|
|
89
|
+
void deserializeState(const Napi::Object &state) override
|
|
90
|
+
{
|
|
91
|
+
std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
|
|
92
|
+
EmaMode newMode = (modeStr == "moving") ? EmaMode::Moving : EmaMode::Batch;
|
|
93
|
+
|
|
94
|
+
if (newMode != m_mode)
|
|
95
|
+
{
|
|
96
|
+
throw std::runtime_error("ExponentialMovingAverage mode mismatch during deserialization");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
float alpha = state.Get("alpha").As<Napi::Number>().FloatValue();
|
|
100
|
+
if (std::abs(alpha - m_alpha) > 1e-6f)
|
|
101
|
+
{
|
|
102
|
+
throw std::runtime_error("ExponentialMovingAverage alpha mismatch during deserialization");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (m_mode == EmaMode::Moving)
|
|
106
|
+
{
|
|
107
|
+
// Get number of channels
|
|
108
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
109
|
+
|
|
110
|
+
// Recreate filters
|
|
111
|
+
m_filters.clear();
|
|
112
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
113
|
+
{
|
|
114
|
+
m_filters.emplace_back(m_alpha);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Restore each channel's state
|
|
118
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
119
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
120
|
+
{
|
|
121
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
122
|
+
|
|
123
|
+
float ema = channelState.Get("ema").As<Napi::Number>().FloatValue();
|
|
124
|
+
bool initialized = channelState.Get("initialized").As<Napi::Boolean>().Value();
|
|
125
|
+
|
|
126
|
+
// Restore the filter's state
|
|
127
|
+
m_filters[i].setState(ema, initialized);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Reset all filters to initial state
|
|
133
|
+
void reset() override
|
|
134
|
+
{
|
|
135
|
+
for (auto &filter : m_filters)
|
|
136
|
+
{
|
|
137
|
+
filter.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// TOON Binary Serialization
|
|
142
|
+
void serializeToon(dsp::toon::Serializer &serializer) const override
|
|
143
|
+
{
|
|
144
|
+
serializer.startObject();
|
|
145
|
+
|
|
146
|
+
// 1. Mode
|
|
147
|
+
serializer.writeString("mode");
|
|
148
|
+
serializer.writeString((m_mode == EmaMode::Moving) ? "moving" : "batch");
|
|
149
|
+
|
|
150
|
+
// 2. Alpha
|
|
151
|
+
serializer.writeString("alpha");
|
|
152
|
+
serializer.writeFloat(m_alpha);
|
|
153
|
+
|
|
154
|
+
if (m_mode == EmaMode::Moving)
|
|
155
|
+
{
|
|
156
|
+
// 3. Channels
|
|
157
|
+
serializer.writeString("channels");
|
|
158
|
+
serializer.startArray();
|
|
159
|
+
|
|
160
|
+
for (const auto &filter : m_filters)
|
|
161
|
+
{
|
|
162
|
+
auto [ema, initialized] = filter.getState();
|
|
163
|
+
|
|
164
|
+
serializer.startObject();
|
|
165
|
+
|
|
166
|
+
serializer.writeString("ema");
|
|
167
|
+
serializer.writeFloat(ema);
|
|
168
|
+
|
|
169
|
+
serializer.writeString("initialized");
|
|
170
|
+
serializer.writeBool(initialized);
|
|
171
|
+
|
|
172
|
+
serializer.endObject();
|
|
173
|
+
}
|
|
174
|
+
serializer.endArray();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
serializer.endObject();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// TOON Binary Deserialization
|
|
181
|
+
void deserializeToon(dsp::toon::Deserializer &deserializer) override
|
|
182
|
+
{
|
|
183
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_START);
|
|
184
|
+
|
|
185
|
+
// 1. Mode
|
|
186
|
+
std::string key = deserializer.readString(); // "mode"
|
|
187
|
+
std::string modeStr = deserializer.readString();
|
|
188
|
+
|
|
189
|
+
EmaMode newMode = (modeStr == "moving") ? EmaMode::Moving : EmaMode::Batch;
|
|
190
|
+
if (newMode != m_mode)
|
|
191
|
+
{
|
|
192
|
+
throw std::runtime_error("ExponentialMovingAverage mode mismatch during TOON deserialization");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 2. Alpha
|
|
196
|
+
key = deserializer.readString(); // "alpha"
|
|
197
|
+
float alpha = deserializer.readFloat();
|
|
198
|
+
if (std::abs(alpha - m_alpha) > 1e-6f)
|
|
199
|
+
{
|
|
200
|
+
throw std::runtime_error("ExponentialMovingAverage alpha mismatch during TOON deserialization");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (modeStr == "moving")
|
|
204
|
+
{
|
|
205
|
+
// 3. Channels
|
|
206
|
+
key = deserializer.readString(); // "channels"
|
|
207
|
+
deserializer.consumeToken(dsp::toon::T_ARRAY_START);
|
|
208
|
+
|
|
209
|
+
m_filters.clear();
|
|
210
|
+
|
|
211
|
+
while (deserializer.peekToken() != dsp::toon::T_ARRAY_END)
|
|
212
|
+
{
|
|
213
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_START);
|
|
214
|
+
|
|
215
|
+
// EMA value
|
|
216
|
+
deserializer.readString(); // "ema"
|
|
217
|
+
float ema = deserializer.readFloat();
|
|
218
|
+
|
|
219
|
+
// Initialized flag
|
|
220
|
+
deserializer.readString(); // "initialized"
|
|
221
|
+
bool initialized = deserializer.readBool();
|
|
222
|
+
|
|
223
|
+
// Reconstruct filter
|
|
224
|
+
m_filters.emplace_back(m_alpha);
|
|
225
|
+
m_filters.back().setState(ema, initialized);
|
|
226
|
+
|
|
227
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_END);
|
|
228
|
+
}
|
|
229
|
+
deserializer.consumeToken(dsp::toon::T_ARRAY_END);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
deserializer.consumeToken(dsp::toon::T_OBJECT_END);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private:
|
|
236
|
+
/**
|
|
237
|
+
* @brief Batch mode: Compute exponential moving average over entire buffer per channel.
|
|
238
|
+
* Each sample in the channel is replaced by the progressive EMA.
|
|
239
|
+
*/
|
|
240
|
+
void processBatch(float *buffer, size_t numSamples, int numChannels)
|
|
241
|
+
{
|
|
242
|
+
// Process each channel independently
|
|
243
|
+
for (int c = 0; c < numChannels; ++c)
|
|
244
|
+
{
|
|
245
|
+
// Initialize EMA with first sample of this channel
|
|
246
|
+
float ema = (c < numSamples) ? buffer[c] : 0.0f;
|
|
247
|
+
|
|
248
|
+
// Process all samples for this channel
|
|
249
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
250
|
+
{
|
|
251
|
+
// EMA formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
|
|
252
|
+
ema = m_alpha * buffer[i] + (1.0f - m_alpha) * ema;
|
|
253
|
+
buffer[i] = ema;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @brief Moving mode: Statefully process samples using EMA filters with continuity.
|
|
260
|
+
* @param buffer The interleaved audio buffer.
|
|
261
|
+
* @param numSamples The total number of samples.
|
|
262
|
+
* @param numChannels The number of channels.
|
|
263
|
+
*/
|
|
264
|
+
void processMoving(float *buffer, size_t numSamples, int numChannels)
|
|
265
|
+
{
|
|
266
|
+
// Lazily initialize our filters, one for each channel
|
|
267
|
+
if (m_filters.size() != static_cast<size_t>(numChannels))
|
|
268
|
+
{
|
|
269
|
+
m_filters.clear();
|
|
270
|
+
for (int i = 0; i < numChannels; ++i)
|
|
271
|
+
{
|
|
272
|
+
m_filters.emplace_back(m_alpha);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Process the buffer sample by sample, de-interleaving
|
|
277
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
278
|
+
{
|
|
279
|
+
int channel = i % numChannels;
|
|
280
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
EmaMode m_mode;
|
|
285
|
+
float m_alpha;
|
|
286
|
+
// Separate filter instance for each channel's state
|
|
287
|
+
std::vector<dsp::core::ExponentialMovingAverageFilter<float>> m_filters;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
} // namespace dsp::adapters
|