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,336 @@
|
|
|
1
|
+
#include "FilterBankStage.h"
|
|
2
|
+
#include <iostream>
|
|
3
|
+
|
|
4
|
+
namespace dsp::adapters
|
|
5
|
+
{
|
|
6
|
+
|
|
7
|
+
FilterBankStage::FilterBankStage(const std::vector<FilterDefinition> &definitions, int numInputChannels)
|
|
8
|
+
: m_definitions(definitions), m_numInputChannels(numInputChannels)
|
|
9
|
+
{
|
|
10
|
+
if (definitions.empty())
|
|
11
|
+
{
|
|
12
|
+
throw std::runtime_error("FilterBank: definitions cannot be empty");
|
|
13
|
+
}
|
|
14
|
+
if (numInputChannels <= 0)
|
|
15
|
+
{
|
|
16
|
+
throw std::runtime_error("FilterBank: Invalid channel count");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
initializeFilters();
|
|
20
|
+
}
|
|
21
|
+
FilterBankStage::~FilterBankStage()
|
|
22
|
+
{
|
|
23
|
+
try
|
|
24
|
+
{
|
|
25
|
+
// Explicitly clear filters first to ensure IirFilter destructors run
|
|
26
|
+
// before scratch buffers are destroyed
|
|
27
|
+
for (size_t ch = 0; ch < m_filters.size(); ++ch)
|
|
28
|
+
{
|
|
29
|
+
m_filters[ch].clear(); // Destroys all unique_ptrs in this channel
|
|
30
|
+
}
|
|
31
|
+
m_filters.clear(); // Clear outer vector
|
|
32
|
+
|
|
33
|
+
// Clear scratch buffers
|
|
34
|
+
m_planarInput.clear();
|
|
35
|
+
m_planarOutput.clear();
|
|
36
|
+
}
|
|
37
|
+
catch (const std::exception &e)
|
|
38
|
+
{
|
|
39
|
+
std::cerr << "[FilterBankStage] ERROR in destructor: " << e.what() << std::endl;
|
|
40
|
+
}
|
|
41
|
+
catch (...)
|
|
42
|
+
{
|
|
43
|
+
std::cerr << "[FilterBankStage] UNKNOWN ERROR in destructor" << std::endl;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void FilterBankStage::processResizing(const float *inputBuffer, size_t inputSize,
|
|
48
|
+
float *outputBuffer, size_t &outputSize,
|
|
49
|
+
int numChannels, const float *timestamps)
|
|
50
|
+
{
|
|
51
|
+
if (numChannels != m_numInputChannels)
|
|
52
|
+
{
|
|
53
|
+
throw std::runtime_error("FilterBank: Input channel mismatch");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
size_t samplesPerChannel = inputSize / numChannels;
|
|
57
|
+
size_t numBands = m_definitions.size();
|
|
58
|
+
size_t totalOutputChannels = numChannels * numBands;
|
|
59
|
+
|
|
60
|
+
outputSize = samplesPerChannel * totalOutputChannels;
|
|
61
|
+
|
|
62
|
+
ensureScratchSize(samplesPerChannel);
|
|
63
|
+
|
|
64
|
+
std::vector<float *> inputPtrs(numChannels);
|
|
65
|
+
for (int i = 0; i < numChannels; ++i)
|
|
66
|
+
{
|
|
67
|
+
inputPtrs[i] = m_planarInput[i].data();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (numChannels == 2)
|
|
71
|
+
{
|
|
72
|
+
dsp::simd::deinterleave2Ch(inputBuffer, inputPtrs[0], inputPtrs[1], samplesPerChannel);
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
{
|
|
76
|
+
dsp::simd::deinterleaveNCh(inputBuffer, inputPtrs.data(), numChannels, samplesPerChannel);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (int ch = 0; ch < numChannels; ++ch)
|
|
80
|
+
{
|
|
81
|
+
for (size_t band = 0; band < numBands; ++band)
|
|
82
|
+
{
|
|
83
|
+
int outChIndex = (ch * static_cast<int>(numBands)) + static_cast<int>(band);
|
|
84
|
+
|
|
85
|
+
m_filters[ch][band]->process(
|
|
86
|
+
m_planarInput[ch].data(),
|
|
87
|
+
m_planarOutput[outChIndex].data(),
|
|
88
|
+
samplesPerChannel);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
std::vector<const float *> outputPtrs(totalOutputChannels);
|
|
93
|
+
for (size_t i = 0; i < totalOutputChannels; ++i)
|
|
94
|
+
{
|
|
95
|
+
outputPtrs[i] = m_planarOutput[i].data();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
dsp::simd::interleaveNCh(outputPtrs.data(), outputBuffer, static_cast<int>(totalOutputChannels), samplesPerChannel);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
void FilterBankStage::initializeFilters()
|
|
102
|
+
{
|
|
103
|
+
m_filters.resize(m_numInputChannels);
|
|
104
|
+
for (int ch = 0; ch < m_numInputChannels; ++ch)
|
|
105
|
+
{
|
|
106
|
+
m_filters[ch].clear();
|
|
107
|
+
for (const auto &def : m_definitions)
|
|
108
|
+
{
|
|
109
|
+
// Convert double coefficients to float for IirFilter
|
|
110
|
+
std::vector<float> b_float(def.b.begin(), def.b.end());
|
|
111
|
+
std::vector<float> a_float(def.a.begin(), def.a.end());
|
|
112
|
+
|
|
113
|
+
// Create stateful IIR filter for each band
|
|
114
|
+
m_filters[ch].push_back(
|
|
115
|
+
std::make_unique<dsp::core::IirFilter<float>>(b_float, a_float, true));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
void FilterBankStage::ensureScratchSize(size_t samplesPerChannel)
|
|
121
|
+
{
|
|
122
|
+
if (m_planarInput.size() != static_cast<size_t>(m_numInputChannels))
|
|
123
|
+
{
|
|
124
|
+
m_planarInput.resize(m_numInputChannels);
|
|
125
|
+
}
|
|
126
|
+
for (size_t i = 0; i < m_planarInput.size(); ++i)
|
|
127
|
+
{
|
|
128
|
+
if (m_planarInput[i].size() < samplesPerChannel)
|
|
129
|
+
{
|
|
130
|
+
m_planarInput[i].resize(samplesPerChannel, 0.0f);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
size_t numOut = m_numInputChannels * m_definitions.size();
|
|
135
|
+
|
|
136
|
+
if (m_planarOutput.size() != numOut)
|
|
137
|
+
{
|
|
138
|
+
m_planarOutput.resize(numOut);
|
|
139
|
+
}
|
|
140
|
+
for (size_t i = 0; i < m_planarOutput.size(); ++i)
|
|
141
|
+
{
|
|
142
|
+
if (m_planarOutput[i].size() < samplesPerChannel)
|
|
143
|
+
{
|
|
144
|
+
m_planarOutput[i].resize(samplesPerChannel, 0.0f);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
void FilterBankStage::reset()
|
|
150
|
+
{
|
|
151
|
+
for (auto &ch : m_filters)
|
|
152
|
+
{
|
|
153
|
+
for (auto &filter : ch)
|
|
154
|
+
{
|
|
155
|
+
filter->reset();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Napi::Object FilterBankStage::serializeState(Napi::Env env) const
|
|
161
|
+
{
|
|
162
|
+
Napi::Object state = Napi::Object::New(env);
|
|
163
|
+
state.Set("type", getType());
|
|
164
|
+
state.Set("numInputChannels", m_numInputChannels);
|
|
165
|
+
state.Set("numBands", m_definitions.size());
|
|
166
|
+
|
|
167
|
+
// Serialize filter states as nested arrays: channels[bands[state]]
|
|
168
|
+
Napi::Array channels = Napi::Array::New(env, m_filters.size());
|
|
169
|
+
for (size_t ch = 0; ch < m_filters.size(); ++ch)
|
|
170
|
+
{
|
|
171
|
+
Napi::Array bands = Napi::Array::New(env, m_filters[ch].size());
|
|
172
|
+
for (size_t band = 0; band < m_filters[ch].size(); ++band)
|
|
173
|
+
{
|
|
174
|
+
const auto &filter = m_filters[ch][band];
|
|
175
|
+
|
|
176
|
+
Napi::Object filterState = Napi::Object::New(env);
|
|
177
|
+
|
|
178
|
+
// Get filter state (input and output history)
|
|
179
|
+
auto [inputHistory, outputHistory] = filter->getState();
|
|
180
|
+
|
|
181
|
+
// Serialize input history
|
|
182
|
+
Napi::Array inputArray = Napi::Array::New(env, inputHistory.size());
|
|
183
|
+
for (size_t i = 0; i < inputHistory.size(); ++i)
|
|
184
|
+
{
|
|
185
|
+
inputArray.Set(i, inputHistory[i]);
|
|
186
|
+
}
|
|
187
|
+
filterState.Set("inputHistory", inputArray);
|
|
188
|
+
|
|
189
|
+
// Serialize output history
|
|
190
|
+
Napi::Array outputArray = Napi::Array::New(env, outputHistory.size());
|
|
191
|
+
for (size_t i = 0; i < outputHistory.size(); ++i)
|
|
192
|
+
{
|
|
193
|
+
outputArray.Set(i, outputHistory[i]);
|
|
194
|
+
}
|
|
195
|
+
filterState.Set("outputHistory", outputArray);
|
|
196
|
+
|
|
197
|
+
bands.Set(band, filterState);
|
|
198
|
+
}
|
|
199
|
+
channels.Set(ch, bands);
|
|
200
|
+
}
|
|
201
|
+
state.Set("filterStates", channels);
|
|
202
|
+
|
|
203
|
+
return state;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
void FilterBankStage::deserializeState(const Napi::Object &state)
|
|
207
|
+
{
|
|
208
|
+
if (!state.Has("filterStates"))
|
|
209
|
+
{
|
|
210
|
+
throw std::runtime_error("FilterBank: Missing filterStates in serialized data");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Napi::Array channels = state.Get("filterStates").As<Napi::Array>();
|
|
214
|
+
if (channels.Length() != m_filters.size())
|
|
215
|
+
{
|
|
216
|
+
throw std::runtime_error("FilterBank: Channel count mismatch in deserialization");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (size_t ch = 0; ch < channels.Length(); ++ch)
|
|
220
|
+
{
|
|
221
|
+
Napi::Array bands = channels.Get(ch).As<Napi::Array>();
|
|
222
|
+
if (bands.Length() != m_filters[ch].size())
|
|
223
|
+
{
|
|
224
|
+
throw std::runtime_error("FilterBank: Band count mismatch in deserialization");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (size_t band = 0; band < bands.Length(); ++band)
|
|
228
|
+
{
|
|
229
|
+
Napi::Object filterState = bands.Get(band).As<Napi::Object>();
|
|
230
|
+
|
|
231
|
+
// Deserialize input history
|
|
232
|
+
std::vector<float> inputHistory;
|
|
233
|
+
if (filterState.Has("inputHistory"))
|
|
234
|
+
{
|
|
235
|
+
Napi::Array inputArray = filterState.Get("inputHistory").As<Napi::Array>();
|
|
236
|
+
inputHistory.reserve(inputArray.Length());
|
|
237
|
+
for (size_t i = 0; i < inputArray.Length(); ++i)
|
|
238
|
+
{
|
|
239
|
+
inputHistory.push_back(inputArray.Get(i).As<Napi::Number>().FloatValue());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Deserialize output history
|
|
244
|
+
std::vector<float> outputHistory;
|
|
245
|
+
if (filterState.Has("outputHistory"))
|
|
246
|
+
{
|
|
247
|
+
Napi::Array outputArray = filterState.Get("outputHistory").As<Napi::Array>();
|
|
248
|
+
outputHistory.reserve(outputArray.Length());
|
|
249
|
+
for (size_t i = 0; i < outputArray.Length(); ++i)
|
|
250
|
+
{
|
|
251
|
+
outputHistory.push_back(outputArray.Get(i).As<Napi::Number>().FloatValue());
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Restore filter state
|
|
256
|
+
m_filters[ch][band]->setState(inputHistory, outputHistory);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
void FilterBankStage::serializeToon(toon::Serializer &serializer) const
|
|
262
|
+
{
|
|
263
|
+
// Serialize configuration
|
|
264
|
+
serializer.writeInt32(m_numInputChannels);
|
|
265
|
+
serializer.writeInt32(static_cast<int32_t>(m_definitions.size()));
|
|
266
|
+
|
|
267
|
+
// Serialize each filter's state
|
|
268
|
+
for (const auto &ch : m_filters)
|
|
269
|
+
{
|
|
270
|
+
for (const auto &filter : ch)
|
|
271
|
+
{
|
|
272
|
+
// Get filter state (input and output history)
|
|
273
|
+
auto [inputHistory, outputHistory] = filter->getState();
|
|
274
|
+
|
|
275
|
+
// Write input history
|
|
276
|
+
serializer.writeInt32(static_cast<int32_t>(inputHistory.size()));
|
|
277
|
+
for (float val : inputHistory)
|
|
278
|
+
{
|
|
279
|
+
serializer.writeFloat(val);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Write output history
|
|
283
|
+
serializer.writeInt32(static_cast<int32_t>(outputHistory.size()));
|
|
284
|
+
for (float val : outputHistory)
|
|
285
|
+
{
|
|
286
|
+
serializer.writeFloat(val);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
void FilterBankStage::deserializeToon(toon::Deserializer &deserializer)
|
|
293
|
+
{
|
|
294
|
+
// Read and validate configuration
|
|
295
|
+
int numChannels = deserializer.readInt32();
|
|
296
|
+
int32_t numBands = deserializer.readInt32();
|
|
297
|
+
|
|
298
|
+
if (numChannels != m_numInputChannels)
|
|
299
|
+
{
|
|
300
|
+
throw std::runtime_error("FilterBank TOON: Channel count mismatch");
|
|
301
|
+
}
|
|
302
|
+
if (static_cast<size_t>(numBands) != m_definitions.size())
|
|
303
|
+
{
|
|
304
|
+
throw std::runtime_error("FilterBank TOON: Band count mismatch");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Restore each filter's state
|
|
308
|
+
for (auto &ch : m_filters)
|
|
309
|
+
{
|
|
310
|
+
for (auto &filter : ch)
|
|
311
|
+
{
|
|
312
|
+
// Read input history
|
|
313
|
+
int32_t inputSize = deserializer.readInt32();
|
|
314
|
+
std::vector<float> inputHistory;
|
|
315
|
+
inputHistory.reserve(inputSize);
|
|
316
|
+
for (int32_t i = 0; i < inputSize; ++i)
|
|
317
|
+
{
|
|
318
|
+
inputHistory.push_back(deserializer.readFloat());
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Read output history
|
|
322
|
+
int32_t outputSize = deserializer.readInt32();
|
|
323
|
+
std::vector<float> outputHistory;
|
|
324
|
+
outputHistory.reserve(outputSize);
|
|
325
|
+
for (int32_t i = 0; i < outputSize; ++i)
|
|
326
|
+
{
|
|
327
|
+
outputHistory.push_back(deserializer.readFloat());
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Restore filter state
|
|
331
|
+
filter->setState(inputHistory, outputHistory);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
} // namespace dsp::adapters
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include "../IDspStage.h"
|
|
3
|
+
#include "../core/IirFilter.h"
|
|
4
|
+
#include "../utils/SimdOps.h"
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
|
|
9
|
+
namespace dsp::adapters
|
|
10
|
+
{
|
|
11
|
+
/**
|
|
12
|
+
* @brief Lightweight struct to pass filter coefficients from JavaScript
|
|
13
|
+
*/
|
|
14
|
+
struct FilterDefinition
|
|
15
|
+
{
|
|
16
|
+
std::vector<double> b; // Feedforward coefficients
|
|
17
|
+
std::vector<double> a; // Feedback coefficients
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @brief Filter Bank Stage - Splits N input channels into N × M sub-bands
|
|
22
|
+
*
|
|
23
|
+
* Architecture:
|
|
24
|
+
* - Input: N channels (interleaved)
|
|
25
|
+
* - Output: N × M channels (interleaved, channel-major layout)
|
|
26
|
+
* - Layout: [ (Ch1_Band1, Ch1_Band2, ...), (Ch2_Band1, Ch2_Band2, ...) ]
|
|
27
|
+
*
|
|
28
|
+
* Optimization Strategy (Planar-Core):
|
|
29
|
+
* 1. De-interleave input to planar scratch buffers (cache-efficient)
|
|
30
|
+
* 2. Process filters on contiguous planar data (SIMD-friendly)
|
|
31
|
+
* 3. Interleave output back to standard format
|
|
32
|
+
*
|
|
33
|
+
* Use Cases:
|
|
34
|
+
* - Speech recognition: Mel-scale filter banks (20-40 bands)
|
|
35
|
+
* - Audio compression: Bark-scale for psychoacoustic analysis
|
|
36
|
+
* - Musical analysis: Octave bands (log scale)
|
|
37
|
+
* - Research: Linear-scale frequency analysis
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 2 input channels → 20 output channels (10 bands per channel)
|
|
41
|
+
* // Input: [L, R, L, R, ...]
|
|
42
|
+
* // Output: [L_B1, L_B2, ..., L_B10, R_B1, R_B2, ..., R_B10, ...]
|
|
43
|
+
*/
|
|
44
|
+
class FilterBankStage : public dsp::IDspStage
|
|
45
|
+
{
|
|
46
|
+
public:
|
|
47
|
+
/**
|
|
48
|
+
* @brief Construct a filter bank stage
|
|
49
|
+
* @param bandDefinitions Array of filter coefficient definitions (one per band)
|
|
50
|
+
* @param numInputChannels Number of input channels
|
|
51
|
+
*/
|
|
52
|
+
FilterBankStage(const std::vector<FilterDefinition> &bandDefinitions, int numInputChannels);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @brief Destructor - ensures proper cleanup of filters and scratch buffers
|
|
56
|
+
*/
|
|
57
|
+
virtual ~FilterBankStage();
|
|
58
|
+
|
|
59
|
+
const char *getType() const override { return "filterBank"; }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @brief This stage expands the buffer size (N channels → N×M channels)
|
|
63
|
+
* @return Always true
|
|
64
|
+
*/
|
|
65
|
+
bool isResizing() const override { return true; }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @brief Returns output channel count = inputChannels × numBands
|
|
69
|
+
* @return Output channel count
|
|
70
|
+
*/
|
|
71
|
+
int getOutputChannels() const override
|
|
72
|
+
{
|
|
73
|
+
return m_numInputChannels * static_cast<int>(m_definitions.size());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @brief Calculate output buffer size for resizing
|
|
78
|
+
* @param inputSize Input buffer size in samples
|
|
79
|
+
* @return Output buffer size in samples
|
|
80
|
+
*/
|
|
81
|
+
size_t calculateOutputSize(size_t inputSize) const override
|
|
82
|
+
{
|
|
83
|
+
return (inputSize / m_numInputChannels) * getOutputChannels();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @brief Standard process() throws because this stage requires resizing
|
|
88
|
+
*/
|
|
89
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps) override
|
|
90
|
+
{
|
|
91
|
+
throw std::runtime_error("FilterBank requires processResizing");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @brief Process audio data with buffer resizing
|
|
96
|
+
*
|
|
97
|
+
* Algorithm:
|
|
98
|
+
* 1. De-interleave input to planar scratch buffers
|
|
99
|
+
* 2. Apply each band's filter to each input channel (planar processing)
|
|
100
|
+
* 3. Interleave results to output in channel-major order
|
|
101
|
+
*
|
|
102
|
+
* @param inputBuffer Source interleaved buffer
|
|
103
|
+
* @param inputSize Input size in samples
|
|
104
|
+
* @param outputBuffer Destination buffer (caller must allocate)
|
|
105
|
+
* @param outputSize Reference to store actual output size
|
|
106
|
+
* @param numChannels Number of input channels
|
|
107
|
+
* @param timestamps Optional timestamp array
|
|
108
|
+
*/
|
|
109
|
+
void processResizing(const float *inputBuffer, size_t inputSize,
|
|
110
|
+
float *outputBuffer, size_t &outputSize,
|
|
111
|
+
int numChannels, const float *timestamps) override;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @brief Reset all filter states to initial conditions
|
|
115
|
+
*/
|
|
116
|
+
void reset() override;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @brief Serialize filter bank state to JavaScript object
|
|
120
|
+
* @param env N-API environment
|
|
121
|
+
* @return Napi::Object containing all filter states
|
|
122
|
+
*/
|
|
123
|
+
Napi::Object serializeState(Napi::Env env) const override;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @brief Restore filter bank state from JavaScript object
|
|
127
|
+
* @param state Napi::Object containing serialized filter states
|
|
128
|
+
*/
|
|
129
|
+
void deserializeState(const Napi::Object &state) override;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @brief Serialize state to TOON binary format
|
|
133
|
+
* @param serializer TOON serializer instance
|
|
134
|
+
*/
|
|
135
|
+
void serializeToon(toon::Serializer &serializer) const override;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @brief Deserialize state from TOON binary format
|
|
139
|
+
* @param deserializer TOON deserializer instance
|
|
140
|
+
*/
|
|
141
|
+
void deserializeToon(toon::Deserializer &deserializer) override;
|
|
142
|
+
|
|
143
|
+
private:
|
|
144
|
+
/**
|
|
145
|
+
* @brief Initialize the 2D matrix of IIR filters [channel][band]
|
|
146
|
+
*/
|
|
147
|
+
void initializeFilters();
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @brief Ensure scratch buffers are allocated and sized correctly
|
|
151
|
+
* @param samplesPerChannel Number of samples per channel to process
|
|
152
|
+
*/
|
|
153
|
+
void ensureScratchSize(size_t samplesPerChannel);
|
|
154
|
+
|
|
155
|
+
// Configuration
|
|
156
|
+
int m_numInputChannels;
|
|
157
|
+
std::vector<FilterDefinition> m_definitions;
|
|
158
|
+
|
|
159
|
+
// 2D Matrix of filters: m_filters[channelIndex][bandIndex]
|
|
160
|
+
std::vector<std::vector<std::unique_ptr<dsp::core::IirFilter<float>>>> m_filters;
|
|
161
|
+
|
|
162
|
+
// Optimization: Persistent scratch buffers in planar layout
|
|
163
|
+
// m_planarInput[channel][sample] - de-interleaved input
|
|
164
|
+
std::vector<std::vector<float>> m_planarInput;
|
|
165
|
+
|
|
166
|
+
// m_planarOutput[flatOutputChannelIndex][sample] - filtered planar data
|
|
167
|
+
std::vector<std::vector<float>> m_planarOutput;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
} // namespace dsp::adapters
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "FilterStage.h"
|
|
2
2
|
#include <stdexcept>
|
|
3
|
+
#include <iostream> // For optional debug logging
|
|
3
4
|
|
|
4
5
|
namespace dsp::adapters
|
|
5
6
|
{
|
|
@@ -153,6 +154,22 @@ namespace dsp::adapters
|
|
|
153
154
|
Napi::Object state = Napi::Object::New(env);
|
|
154
155
|
state.Set("filterType", m_isFir ? "fir" : "iir");
|
|
155
156
|
|
|
157
|
+
// --- SAVE COEFFICIENTS (New) ---
|
|
158
|
+
Napi::Array bArray = Napi::Array::New(env, m_bCoeffs.size());
|
|
159
|
+
for (size_t i = 0; i < m_bCoeffs.size(); ++i)
|
|
160
|
+
{
|
|
161
|
+
bArray.Set(i, Napi::Number::New(env, m_bCoeffs[i]));
|
|
162
|
+
}
|
|
163
|
+
state.Set("bCoeffs", bArray);
|
|
164
|
+
|
|
165
|
+
Napi::Array aArray = Napi::Array::New(env, m_aCoeffs.size());
|
|
166
|
+
for (size_t i = 0; i < m_aCoeffs.size(); ++i)
|
|
167
|
+
{
|
|
168
|
+
aArray.Set(i, Napi::Number::New(env, m_aCoeffs[i]));
|
|
169
|
+
}
|
|
170
|
+
state.Set("aCoeffs", aArray);
|
|
171
|
+
// --------------------------------
|
|
172
|
+
|
|
156
173
|
if (m_isFir)
|
|
157
174
|
{
|
|
158
175
|
state.Set("numChannels", static_cast<uint32_t>(m_firFilters.size()));
|
|
@@ -287,4 +304,125 @@ namespace dsp::adapters
|
|
|
287
304
|
}
|
|
288
305
|
}
|
|
289
306
|
|
|
290
|
-
|
|
307
|
+
void FilterStage::serializeToon(dsp::toon::Serializer &s) const
|
|
308
|
+
{
|
|
309
|
+
// Serialize filter type
|
|
310
|
+
s.writeString(m_isFir ? "fir" : "iir");
|
|
311
|
+
|
|
312
|
+
// Serialize coefficients
|
|
313
|
+
s.writeInt32(static_cast<int32_t>(m_bCoeffs.size()));
|
|
314
|
+
for (double coeff : m_bCoeffs)
|
|
315
|
+
{
|
|
316
|
+
s.writeDouble(coeff);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
s.writeInt32(static_cast<int32_t>(m_aCoeffs.size()));
|
|
320
|
+
for (double coeff : m_aCoeffs)
|
|
321
|
+
{
|
|
322
|
+
s.writeDouble(coeff);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Serialize filter states
|
|
326
|
+
if (m_isFir)
|
|
327
|
+
{
|
|
328
|
+
s.writeInt32(static_cast<int32_t>(m_firFilters.size()));
|
|
329
|
+
for (const auto &filter : m_firFilters)
|
|
330
|
+
{
|
|
331
|
+
if (filter)
|
|
332
|
+
{
|
|
333
|
+
auto [stateBuffer, stateIndex] = filter->getState();
|
|
334
|
+
s.writeFloatArray(stateBuffer);
|
|
335
|
+
s.writeInt32(static_cast<int32_t>(stateIndex));
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
{
|
|
339
|
+
s.writeInt32(0); // Empty state buffer size
|
|
340
|
+
s.writeInt32(0); // stateIndex
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else // IIR
|
|
345
|
+
{
|
|
346
|
+
s.writeInt32(static_cast<int32_t>(m_iirFilters.size()));
|
|
347
|
+
for (const auto &filter : m_iirFilters)
|
|
348
|
+
{
|
|
349
|
+
if (filter)
|
|
350
|
+
{
|
|
351
|
+
auto [xState, yState] = filter->getState();
|
|
352
|
+
s.writeFloatArray(xState);
|
|
353
|
+
s.writeFloatArray(yState);
|
|
354
|
+
}
|
|
355
|
+
else
|
|
356
|
+
{
|
|
357
|
+
s.writeInt32(0); // Empty xState size
|
|
358
|
+
s.writeInt32(0); // Empty yState size
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
void FilterStage::deserializeToon(dsp::toon::Deserializer &d)
|
|
365
|
+
{
|
|
366
|
+
// Deserialize filter type
|
|
367
|
+
std::string filterType = d.readString();
|
|
368
|
+
bool isFir = (filterType == "fir");
|
|
369
|
+
|
|
370
|
+
if (isFir != m_isFir)
|
|
371
|
+
{
|
|
372
|
+
throw std::runtime_error("FilterStage TOON load: filter type mismatch");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Deserialize and validate coefficients
|
|
376
|
+
int32_t bSize = d.readInt32();
|
|
377
|
+
if (bSize != static_cast<int32_t>(m_bCoeffs.size()))
|
|
378
|
+
{
|
|
379
|
+
throw std::runtime_error("FilterStage TOON load: bCoeffs size mismatch");
|
|
380
|
+
}
|
|
381
|
+
for (int32_t i = 0; i < bSize; ++i)
|
|
382
|
+
{
|
|
383
|
+
d.readDouble(); // Skip validation for now
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
int32_t aSize = d.readInt32();
|
|
387
|
+
if (aSize != static_cast<int32_t>(m_aCoeffs.size()))
|
|
388
|
+
{
|
|
389
|
+
throw std::runtime_error("FilterStage TOON load: aCoeffs size mismatch");
|
|
390
|
+
}
|
|
391
|
+
for (int32_t i = 0; i < aSize; ++i)
|
|
392
|
+
{
|
|
393
|
+
d.readDouble(); // Skip validation for now
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Deserialize filter states
|
|
397
|
+
int32_t numChannels = d.readInt32();
|
|
398
|
+
initializeFilters(numChannels);
|
|
399
|
+
|
|
400
|
+
if (m_isFir)
|
|
401
|
+
{
|
|
402
|
+
for (int32_t i = 0; i < numChannels && i < static_cast<int32_t>(m_firFilters.size()); ++i)
|
|
403
|
+
{
|
|
404
|
+
std::vector<float> stateBuffer = d.readFloatArray();
|
|
405
|
+
int32_t stateIndex = d.readInt32();
|
|
406
|
+
|
|
407
|
+
if (m_firFilters[i] && !stateBuffer.empty())
|
|
408
|
+
{
|
|
409
|
+
m_firFilters[i]->setState(stateBuffer, static_cast<size_t>(stateIndex));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else // IIR
|
|
414
|
+
{
|
|
415
|
+
for (int32_t i = 0; i < numChannels && i < static_cast<int32_t>(m_iirFilters.size()); ++i)
|
|
416
|
+
{
|
|
417
|
+
std::vector<float> xState = d.readFloatArray();
|
|
418
|
+
std::vector<float> yState = d.readFloatArray();
|
|
419
|
+
|
|
420
|
+
if (m_iirFilters[i] && !xState.empty())
|
|
421
|
+
{
|
|
422
|
+
m_iirFilters[i]->setState(xState, yState);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
} // namespace dsp::adapters
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "../IDspStage.h"
|
|
4
4
|
#include "../core/IirFilter.h"
|
|
5
5
|
#include "../core/FirFilter.h"
|
|
6
|
+
#include "../utils/Toon.h"
|
|
6
7
|
#include <memory>
|
|
7
8
|
#include <vector>
|
|
8
9
|
#include <string>
|
|
@@ -23,6 +24,9 @@ namespace dsp::adapters
|
|
|
23
24
|
Napi::Object serializeState(Napi::Env env) const override;
|
|
24
25
|
void deserializeState(const Napi::Object &state) override;
|
|
25
26
|
|
|
27
|
+
void serializeToon(dsp::toon::Serializer &serializer) const override;
|
|
28
|
+
void deserializeToon(dsp::toon::Deserializer &deserializer) override;
|
|
29
|
+
|
|
26
30
|
private:
|
|
27
31
|
void initializeFilters(int numChannels);
|
|
28
32
|
|