dspx 1.2.4 → 1.3.1
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 +270 -17
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +566 -43
- 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 +699 -126
- 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/AmplifyStage.h +148 -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 +122 -0
- 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 +72 -0
- package/src/native/adapters/SnrStage.h +45 -0
- package/src/native/adapters/SquareStage.h +78 -0
- package/src/native/adapters/SscStage.h +65 -0
- package/src/native/adapters/StftStage.h +62 -0
- package/src/native/adapters/VarianceStage.h +59 -0
- package/src/native/adapters/WampStage.h +59 -0
- package/src/native/adapters/WaveformLengthStage.h +51 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +64 -0
- 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/SimdOps.h +67 -0
- package/src/native/utils/Toon.h +195 -0
package/src/native/DspPipeline.h
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include <memory>
|
|
5
5
|
#include <unordered_map>
|
|
6
6
|
#include <functional>
|
|
7
|
+
#include <atomic>
|
|
7
8
|
#include "IDspStage.h"
|
|
8
9
|
|
|
9
10
|
namespace dsp
|
|
@@ -23,6 +24,12 @@ namespace dsp
|
|
|
23
24
|
// This is the async "process" method called by the TS processor
|
|
24
25
|
Napi::Value ProcessAsync(const Napi::CallbackInfo &info);
|
|
25
26
|
|
|
27
|
+
// The Synchronous method for JS workers/clusters
|
|
28
|
+
Napi::Value ProcessSync(const Napi::CallbackInfo &info);
|
|
29
|
+
|
|
30
|
+
// Dispose method for explicit teardown
|
|
31
|
+
Napi::Value Dispose(const Napi::CallbackInfo &info);
|
|
32
|
+
|
|
26
33
|
// State management methods (for Redis persistence from TypeScript)
|
|
27
34
|
Napi::Value SaveState(const Napi::CallbackInfo &info);
|
|
28
35
|
Napi::Value LoadState(const Napi::CallbackInfo &info);
|
|
@@ -40,6 +47,12 @@ namespace dsp
|
|
|
40
47
|
|
|
41
48
|
// This is the "pipeline": a vector of our abstract filter stages
|
|
42
49
|
std::vector<std::unique_ptr<IDspStage>> m_stages;
|
|
50
|
+
|
|
51
|
+
// uses shared_ptr so the lock survices if the JS object is GC'd early
|
|
52
|
+
std::shared_ptr<std::atomic<bool>> m_isBusy;
|
|
53
|
+
|
|
54
|
+
// Flag to track if pipeline has been disposed
|
|
55
|
+
bool m_disposed = false;
|
|
43
56
|
};
|
|
44
57
|
|
|
45
58
|
} // namespace dsp
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Bank Design Bindings
|
|
3
|
+
*
|
|
4
|
+
* N-API bindings for filter bank design utilities.
|
|
5
|
+
* Exposes stateless filter bank generation to TypeScript.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include <napi.h>
|
|
9
|
+
#include "core/FilterBankDesign.h"
|
|
10
|
+
|
|
11
|
+
namespace dsp
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* Design a filter bank (N-API binding)
|
|
15
|
+
*
|
|
16
|
+
* @param info[0] - Options object with:
|
|
17
|
+
* - scale: 'linear' | 'log' | 'mel' | 'bark'
|
|
18
|
+
* - type: 'butterworth' | 'chebyshev'
|
|
19
|
+
* - count: number of bands
|
|
20
|
+
* - sampleRate: sample rate in Hz
|
|
21
|
+
* - frequencyRange: [minFreq, maxFreq]
|
|
22
|
+
* - order: filter order (default: 2)
|
|
23
|
+
* - rippleDb: Chebyshev ripple (default: 0.5)
|
|
24
|
+
*
|
|
25
|
+
* @return Array of { b: number[], a: number[] } coefficient objects
|
|
26
|
+
*/
|
|
27
|
+
Napi::Value DesignFilterBank(const Napi::CallbackInfo &info)
|
|
28
|
+
{
|
|
29
|
+
Napi::Env env = info.Env();
|
|
30
|
+
|
|
31
|
+
// Validate input
|
|
32
|
+
if (info.Length() < 1 || !info[0].IsObject())
|
|
33
|
+
{
|
|
34
|
+
Napi::TypeError::New(env, "Options object required")
|
|
35
|
+
.ThrowAsJavaScriptException();
|
|
36
|
+
return env.Null();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Napi::Object options = info[0].As<Napi::Object>();
|
|
40
|
+
|
|
41
|
+
try
|
|
42
|
+
{
|
|
43
|
+
// Parse options
|
|
44
|
+
core::FilterBankDesign::DesignOptions opts;
|
|
45
|
+
|
|
46
|
+
// Parse scale
|
|
47
|
+
if (!options.Has("scale") || !options.Get("scale").IsString())
|
|
48
|
+
{
|
|
49
|
+
throw std::invalid_argument("scale (string) is required");
|
|
50
|
+
}
|
|
51
|
+
std::string scaleStr = options.Get("scale").As<Napi::String>().Utf8Value();
|
|
52
|
+
|
|
53
|
+
if (scaleStr == "mel")
|
|
54
|
+
{
|
|
55
|
+
opts.scale = core::FilterBankDesign::Scale::Mel;
|
|
56
|
+
}
|
|
57
|
+
else if (scaleStr == "bark")
|
|
58
|
+
{
|
|
59
|
+
opts.scale = core::FilterBankDesign::Scale::Bark;
|
|
60
|
+
}
|
|
61
|
+
else if (scaleStr == "log")
|
|
62
|
+
{
|
|
63
|
+
opts.scale = core::FilterBankDesign::Scale::Log;
|
|
64
|
+
}
|
|
65
|
+
else if (scaleStr == "linear")
|
|
66
|
+
{
|
|
67
|
+
opts.scale = core::FilterBankDesign::Scale::Linear;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
throw std::invalid_argument("Invalid scale: must be 'linear', 'log', 'mel', or 'bark'");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Parse type (optional, defaults to butterworth)
|
|
75
|
+
opts.type = core::FilterBankDesign::Type::Butterworth;
|
|
76
|
+
if (options.Has("type") && options.Get("type").IsString())
|
|
77
|
+
{
|
|
78
|
+
std::string typeStr = options.Get("type").As<Napi::String>().Utf8Value();
|
|
79
|
+
if (typeStr == "chebyshev" || typeStr == "chebyshev1")
|
|
80
|
+
{
|
|
81
|
+
opts.type = core::FilterBankDesign::Type::Chebyshev1;
|
|
82
|
+
}
|
|
83
|
+
else if (typeStr != "butterworth")
|
|
84
|
+
{
|
|
85
|
+
throw std::invalid_argument("Invalid type: must be 'butterworth' or 'chebyshev'");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse count
|
|
90
|
+
if (!options.Has("count") || !options.Get("count").IsNumber())
|
|
91
|
+
{
|
|
92
|
+
throw std::invalid_argument("count (number) is required");
|
|
93
|
+
}
|
|
94
|
+
opts.count = options.Get("count").As<Napi::Number>().Int32Value();
|
|
95
|
+
|
|
96
|
+
// Parse sampleRate
|
|
97
|
+
if (!options.Has("sampleRate") || !options.Get("sampleRate").IsNumber())
|
|
98
|
+
{
|
|
99
|
+
throw std::invalid_argument("sampleRate (number) is required");
|
|
100
|
+
}
|
|
101
|
+
opts.sampleRate = options.Get("sampleRate").As<Napi::Number>().DoubleValue();
|
|
102
|
+
|
|
103
|
+
// Parse frequencyRange
|
|
104
|
+
if (!options.Has("frequencyRange") || !options.Get("frequencyRange").IsArray())
|
|
105
|
+
{
|
|
106
|
+
throw std::invalid_argument("frequencyRange ([min, max]) is required");
|
|
107
|
+
}
|
|
108
|
+
Napi::Array range = options.Get("frequencyRange").As<Napi::Array>();
|
|
109
|
+
if (range.Length() < 2)
|
|
110
|
+
{
|
|
111
|
+
throw std::invalid_argument("frequencyRange must have at least 2 elements");
|
|
112
|
+
}
|
|
113
|
+
opts.minFreq = range.Get((uint32_t)0).As<Napi::Number>().DoubleValue();
|
|
114
|
+
opts.maxFreq = range.Get((uint32_t)1).As<Napi::Number>().DoubleValue();
|
|
115
|
+
|
|
116
|
+
// Parse order (optional, defaults to 2)
|
|
117
|
+
opts.order = 2;
|
|
118
|
+
if (options.Has("order") && options.Get("order").IsNumber())
|
|
119
|
+
{
|
|
120
|
+
opts.order = options.Get("order").As<Napi::Number>().Int32Value();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Parse rippleDb (optional, defaults to 0.5)
|
|
124
|
+
opts.rippleDb = 0.5;
|
|
125
|
+
if (options.Has("rippleDb") && options.Get("rippleDb").IsNumber())
|
|
126
|
+
{
|
|
127
|
+
opts.rippleDb = options.Get("rippleDb").As<Napi::Number>().DoubleValue();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Design the filter bank
|
|
131
|
+
auto bank = core::FilterBankDesign::design(opts);
|
|
132
|
+
|
|
133
|
+
// Convert to JavaScript array
|
|
134
|
+
Napi::Array result = Napi::Array::New(env, bank.size());
|
|
135
|
+
|
|
136
|
+
for (size_t i = 0; i < bank.size(); ++i)
|
|
137
|
+
{
|
|
138
|
+
Napi::Object coeffs = Napi::Object::New(env);
|
|
139
|
+
|
|
140
|
+
// Convert b coefficients
|
|
141
|
+
Napi::Array b = Napi::Array::New(env, bank[i].b.size());
|
|
142
|
+
for (size_t j = 0; j < bank[i].b.size(); ++j)
|
|
143
|
+
{
|
|
144
|
+
b.Set(j, Napi::Number::New(env, bank[i].b[j]));
|
|
145
|
+
}
|
|
146
|
+
coeffs.Set("b", b);
|
|
147
|
+
|
|
148
|
+
// Convert a coefficients
|
|
149
|
+
Napi::Array a = Napi::Array::New(env, bank[i].a.size());
|
|
150
|
+
for (size_t j = 0; j < bank[i].a.size(); ++j)
|
|
151
|
+
{
|
|
152
|
+
a.Set(j, Napi::Number::New(env, bank[i].a[j]));
|
|
153
|
+
}
|
|
154
|
+
coeffs.Set("a", a);
|
|
155
|
+
|
|
156
|
+
result.Set(i, coeffs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
catch (const std::exception &e)
|
|
162
|
+
{
|
|
163
|
+
Napi::Error::New(env, std::string("Filter bank design failed: ") + e.what())
|
|
164
|
+
.ThrowAsJavaScriptException();
|
|
165
|
+
return env.Null();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get filter bank frequency boundaries (N-API binding)
|
|
171
|
+
*
|
|
172
|
+
* Useful for visualization and debugging
|
|
173
|
+
*
|
|
174
|
+
* @param info[0] - Same options object as designFilterBank
|
|
175
|
+
* @return Array of boundary frequencies in Hz
|
|
176
|
+
*/
|
|
177
|
+
Napi::Value GetFilterBankBoundaries(const Napi::CallbackInfo &info)
|
|
178
|
+
{
|
|
179
|
+
Napi::Env env = info.Env();
|
|
180
|
+
|
|
181
|
+
if (info.Length() < 1 || !info[0].IsObject())
|
|
182
|
+
{
|
|
183
|
+
Napi::TypeError::New(env, "Options object required")
|
|
184
|
+
.ThrowAsJavaScriptException();
|
|
185
|
+
return env.Null();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Napi::Object options = info[0].As<Napi::Object>();
|
|
189
|
+
|
|
190
|
+
try
|
|
191
|
+
{
|
|
192
|
+
// Parse options (same as DesignFilterBank)
|
|
193
|
+
core::FilterBankDesign::DesignOptions opts;
|
|
194
|
+
|
|
195
|
+
std::string scaleStr = options.Get("scale").As<Napi::String>().Utf8Value();
|
|
196
|
+
if (scaleStr == "mel")
|
|
197
|
+
opts.scale = core::FilterBankDesign::Scale::Mel;
|
|
198
|
+
else if (scaleStr == "bark")
|
|
199
|
+
opts.scale = core::FilterBankDesign::Scale::Bark;
|
|
200
|
+
else if (scaleStr == "log")
|
|
201
|
+
opts.scale = core::FilterBankDesign::Scale::Log;
|
|
202
|
+
else
|
|
203
|
+
opts.scale = core::FilterBankDesign::Scale::Linear;
|
|
204
|
+
|
|
205
|
+
opts.count = options.Get("count").As<Napi::Number>().Int32Value();
|
|
206
|
+
|
|
207
|
+
Napi::Array range = options.Get("frequencyRange").As<Napi::Array>();
|
|
208
|
+
opts.minFreq = range.Get((uint32_t)0).As<Napi::Number>().DoubleValue();
|
|
209
|
+
opts.maxFreq = range.Get((uint32_t)1).As<Napi::Number>().DoubleValue();
|
|
210
|
+
|
|
211
|
+
// Get boundaries
|
|
212
|
+
auto boundaries = core::FilterBankDesign::getBoundaries(opts);
|
|
213
|
+
|
|
214
|
+
// Convert to JavaScript array
|
|
215
|
+
Napi::Array result = Napi::Array::New(env, boundaries.size());
|
|
216
|
+
for (size_t i = 0; i < boundaries.size(); ++i)
|
|
217
|
+
{
|
|
218
|
+
result.Set(i, Napi::Number::New(env, boundaries[i]));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
catch (const std::exception &e)
|
|
224
|
+
{
|
|
225
|
+
Napi::Error::New(env, std::string("Get boundaries failed: ") + e.what())
|
|
226
|
+
.ThrowAsJavaScriptException();
|
|
227
|
+
return env.Null();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Register filter bank design bindings
|
|
233
|
+
* Call this from your main addon Init function
|
|
234
|
+
*/
|
|
235
|
+
void RegisterFilterBankDesignBindings(Napi::Env env, Napi::Object exports)
|
|
236
|
+
{
|
|
237
|
+
exports.Set("designFilterBank", Napi::Function::New(env, DesignFilterBank));
|
|
238
|
+
exports.Set("getFilterBankBoundaries", Napi::Function::New(env, GetFilterBankBoundaries));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
} // namespace dsp
|
package/src/native/IDspStage.h
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
#include <napi.h>
|
|
3
3
|
#include <cstring> // for std::memcpy
|
|
4
|
+
#include "utils/Toon.h"
|
|
4
5
|
|
|
5
6
|
namespace dsp
|
|
6
7
|
{
|
|
@@ -98,6 +99,29 @@ namespace dsp
|
|
|
98
99
|
*/
|
|
99
100
|
virtual void deserializeState(const Napi::Object &state) = 0;
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @brief Serializes internal state to the efficient TOON binary format.
|
|
104
|
+
* Default implementation throws, forcing derived classes to implement it
|
|
105
|
+
* if TOON support is required.
|
|
106
|
+
*
|
|
107
|
+
* @param serializer The TOON serializer to write binary data to.
|
|
108
|
+
*/
|
|
109
|
+
virtual void serializeToon(toon::Serializer &serializer) const
|
|
110
|
+
{
|
|
111
|
+
// Fallback: throw if not implemented for a specific stage
|
|
112
|
+
throw std::runtime_error("TOON serialization not implemented for this stage type");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @brief Deserializes internal state from TOON binary format.
|
|
117
|
+
*
|
|
118
|
+
* @param deserializer The TOON deserializer to read binary data from.
|
|
119
|
+
*/
|
|
120
|
+
virtual void deserializeToon(toon::Deserializer &deserializer)
|
|
121
|
+
{
|
|
122
|
+
throw std::runtime_error("TOON deserialization not implemented for this stage type");
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
/**
|
|
102
126
|
* @brief Resets the stage's internal state to initial values.
|
|
103
127
|
*/
|
|
@@ -203,6 +203,134 @@ namespace dsp
|
|
|
203
203
|
return output;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* @brief Interleaves an array of planar Float32Arrays into a single Float32Array.
|
|
208
|
+
*
|
|
209
|
+
* Takes an array of Float32Arrays (one per channel) and combines them into a
|
|
210
|
+
* single interleaved buffer.
|
|
211
|
+
*
|
|
212
|
+
* @param info[0] - An array of Float32Array objects.
|
|
213
|
+
* @returns A new Float32Array containing the interleaved data.
|
|
214
|
+
*/
|
|
215
|
+
Napi::Value Interleave(const Napi::CallbackInfo &info)
|
|
216
|
+
{
|
|
217
|
+
Napi::Env env = info.Env();
|
|
218
|
+
|
|
219
|
+
if (info.Length() < 1 || !info[0].IsArray())
|
|
220
|
+
{
|
|
221
|
+
Napi::TypeError::New(env, "Expected an array of Float32Arrays").ThrowAsJavaScriptException();
|
|
222
|
+
return env.Null();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
Napi::Array planarArray = info[0].As<Napi::Array>();
|
|
226
|
+
uint32_t numChannels = planarArray.Length();
|
|
227
|
+
|
|
228
|
+
if (numChannels == 0)
|
|
229
|
+
{
|
|
230
|
+
return Napi::Float32Array::New(env, 0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
std::vector<Napi::Float32Array> channels;
|
|
234
|
+
size_t channelLength = 0;
|
|
235
|
+
|
|
236
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
237
|
+
{
|
|
238
|
+
Napi::Value val = planarArray.Get(i);
|
|
239
|
+
if (!val.IsTypedArray() || val.As<Napi::TypedArray>().TypedArrayType() != napi_float32_array)
|
|
240
|
+
{
|
|
241
|
+
Napi::TypeError::New(env, "All elements in the array must be Float32Arrays").ThrowAsJavaScriptException();
|
|
242
|
+
return env.Null();
|
|
243
|
+
}
|
|
244
|
+
Napi::Float32Array channel = val.As<Napi::Float32Array>();
|
|
245
|
+
if (i == 0)
|
|
246
|
+
{
|
|
247
|
+
channelLength = channel.ElementLength();
|
|
248
|
+
}
|
|
249
|
+
else if (channel.ElementLength() != channelLength)
|
|
250
|
+
{
|
|
251
|
+
Napi::TypeError::New(env, "All channels must have the same length").ThrowAsJavaScriptException();
|
|
252
|
+
return env.Null();
|
|
253
|
+
}
|
|
254
|
+
channels.push_back(channel);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
size_t interleavedLength = numChannels * channelLength;
|
|
258
|
+
Napi::Float32Array interleaved = Napi::Float32Array::New(env, interleavedLength);
|
|
259
|
+
float *outputData = interleaved.Data();
|
|
260
|
+
|
|
261
|
+
for (size_t i = 0; i < channelLength; ++i)
|
|
262
|
+
{
|
|
263
|
+
for (uint32_t c = 0; c < numChannels; ++c)
|
|
264
|
+
{
|
|
265
|
+
outputData[i * numChannels + c] = channels[c].Data()[i];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return interleaved;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @brief Deinterleaves a single Float32Array into an array of planar Float32Arrays.
|
|
274
|
+
*
|
|
275
|
+
* Takes an interleaved Float32Array and a channel count, and separates them
|
|
276
|
+
* into an array of Float32Arrays (one per channel).
|
|
277
|
+
*
|
|
278
|
+
* @param info[0] - The interleaved Float32Array.
|
|
279
|
+
* @param info[1] - The number of channels.
|
|
280
|
+
* @returns An array of new Float32Arrays, one for each channel.
|
|
281
|
+
*/
|
|
282
|
+
Napi::Value Deinterleave(const Napi::CallbackInfo &info)
|
|
283
|
+
{
|
|
284
|
+
Napi::Env env = info.Env();
|
|
285
|
+
|
|
286
|
+
if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsNumber())
|
|
287
|
+
{
|
|
288
|
+
Napi::TypeError::New(env, "Expected an interleaved Float32Array and a channel count").ThrowAsJavaScriptException();
|
|
289
|
+
return env.Null();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
Napi::TypedArray typedArray = info[0].As<Napi::TypedArray>();
|
|
293
|
+
if (typedArray.TypedArrayType() != napi_float32_array)
|
|
294
|
+
{
|
|
295
|
+
Napi::TypeError::New(env, "Input must be a Float32Array").ThrowAsJavaScriptException();
|
|
296
|
+
return env.Null();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Napi::Float32Array interleaved = typedArray.As<Napi::Float32Array>();
|
|
300
|
+
int numChannels = info[1].As<Napi::Number>().Int32Value();
|
|
301
|
+
|
|
302
|
+
if (numChannels <= 0)
|
|
303
|
+
{
|
|
304
|
+
Napi::TypeError::New(env, "Number of channels must be positive").ThrowAsJavaScriptException();
|
|
305
|
+
return env.Null();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
size_t interleavedLength = interleaved.ElementLength();
|
|
309
|
+
if (interleavedLength % numChannels != 0)
|
|
310
|
+
{
|
|
311
|
+
Napi::RangeError::New(env, "Interleaved length is not divisible by the number of channels").ThrowAsJavaScriptException();
|
|
312
|
+
return env.Null();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
size_t channelLength = interleavedLength / numChannels;
|
|
316
|
+
const float *inputData = interleaved.Data();
|
|
317
|
+
|
|
318
|
+
Napi::Array planarArray = Napi::Array::New(env, numChannels);
|
|
319
|
+
|
|
320
|
+
for (int c = 0; c < numChannels; ++c)
|
|
321
|
+
{
|
|
322
|
+
Napi::Float32Array channel = Napi::Float32Array::New(env, channelLength);
|
|
323
|
+
float *channelData = channel.Data();
|
|
324
|
+
for (size_t i = 0; i < channelLength; ++i)
|
|
325
|
+
{
|
|
326
|
+
channelData[i] = inputData[i * numChannels + c];
|
|
327
|
+
}
|
|
328
|
+
planarArray.Set(c, channel);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return planarArray;
|
|
332
|
+
}
|
|
333
|
+
|
|
206
334
|
/**
|
|
207
335
|
* @brief Initialize utility function bindings
|
|
208
336
|
*/
|
|
@@ -210,6 +338,8 @@ namespace dsp
|
|
|
210
338
|
{
|
|
211
339
|
exports.Set("dotProduct", Napi::Function::New(env, DotProduct));
|
|
212
340
|
exports.Set("detrend", Napi::Function::New(env, Detrend));
|
|
341
|
+
exports.Set("interleave", Napi::Function::New(env, Interleave));
|
|
342
|
+
exports.Set("deinterleave", Napi::Function::New(env, Deinterleave));
|
|
213
343
|
return exports;
|
|
214
344
|
}
|
|
215
345
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#ifndef DSP_ADAPTERS_AMPLIFYSTAGE_H
|
|
2
|
+
#define DSP_ADAPTERS_AMPLIFYSTAGE_H
|
|
3
|
+
|
|
4
|
+
#include "IDspStage.h"
|
|
5
|
+
#include "../utils/SimdOps.h"
|
|
6
|
+
#include <cstring>
|
|
7
|
+
|
|
8
|
+
// Platform detection for SIMD
|
|
9
|
+
#if defined(__AVX2__)
|
|
10
|
+
#include <immintrin.h>
|
|
11
|
+
#elif defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
|
|
12
|
+
#include <emmintrin.h>
|
|
13
|
+
#elif defined(__ARM_NEON) || defined(__aarch64__)
|
|
14
|
+
#include <arm_neon.h>
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
namespace dsp
|
|
18
|
+
{
|
|
19
|
+
namespace adapters
|
|
20
|
+
{
|
|
21
|
+
/**
|
|
22
|
+
* Amplify (Gain) Stage
|
|
23
|
+
* Multiplies all samples by a constant gain factor.
|
|
24
|
+
* SIMD-optimized for maximum throughput (AVX2/SSE2/NEON).
|
|
25
|
+
* Useful for scaling signals to appropriate amplitude ranges.
|
|
26
|
+
*/
|
|
27
|
+
class AmplifyStage : public IDspStage
|
|
28
|
+
{
|
|
29
|
+
public:
|
|
30
|
+
explicit AmplifyStage(float gain = 1.0f)
|
|
31
|
+
: m_gain(gain)
|
|
32
|
+
{
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void process(float *data, size_t numSamples, int channels, const float *timestamps = nullptr) override
|
|
36
|
+
{
|
|
37
|
+
// SIMD-optimized gain multiplication
|
|
38
|
+
#if defined(__AVX2__)
|
|
39
|
+
// AVX2: Process 8 floats at a time
|
|
40
|
+
const size_t simd_width = 8;
|
|
41
|
+
const size_t simd_count = numSamples / simd_width;
|
|
42
|
+
const size_t simd_end = simd_count * simd_width;
|
|
43
|
+
|
|
44
|
+
const __m256 gain_vec = _mm256_set1_ps(m_gain);
|
|
45
|
+
|
|
46
|
+
for (size_t i = 0; i < simd_end; i += simd_width)
|
|
47
|
+
{
|
|
48
|
+
__m256 samples = _mm256_loadu_ps(&data[i]);
|
|
49
|
+
samples = _mm256_mul_ps(samples, gain_vec);
|
|
50
|
+
_mm256_storeu_ps(&data[i], samples);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle remainder
|
|
54
|
+
for (size_t i = simd_end; i < numSamples; ++i)
|
|
55
|
+
{
|
|
56
|
+
data[i] *= m_gain;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#elif defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
|
|
60
|
+
// SSE2: Process 4 floats at a time
|
|
61
|
+
const size_t simd_width = 4;
|
|
62
|
+
const size_t simd_count = numSamples / simd_width;
|
|
63
|
+
const size_t simd_end = simd_count * simd_width;
|
|
64
|
+
|
|
65
|
+
const __m128 gain_vec = _mm_set1_ps(m_gain);
|
|
66
|
+
|
|
67
|
+
for (size_t i = 0; i < simd_end; i += simd_width)
|
|
68
|
+
{
|
|
69
|
+
__m128 samples = _mm_loadu_ps(&data[i]);
|
|
70
|
+
samples = _mm_mul_ps(samples, gain_vec);
|
|
71
|
+
_mm_storeu_ps(&data[i], samples);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle remainder
|
|
75
|
+
for (size_t i = simd_end; i < numSamples; ++i)
|
|
76
|
+
{
|
|
77
|
+
data[i] *= m_gain;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#elif defined(__ARM_NEON) || defined(__aarch64__)
|
|
81
|
+
// ARM NEON: Process 4 floats at a time
|
|
82
|
+
const size_t simd_width = 4;
|
|
83
|
+
const size_t simd_count = numSamples / simd_width;
|
|
84
|
+
const size_t simd_end = simd_count * simd_width;
|
|
85
|
+
|
|
86
|
+
const float32x4_t gain_vec = vdupq_n_f32(m_gain);
|
|
87
|
+
|
|
88
|
+
for (size_t i = 0; i < simd_end; i += simd_width)
|
|
89
|
+
{
|
|
90
|
+
float32x4_t samples = vld1q_f32(&data[i]);
|
|
91
|
+
samples = vmulq_f32(samples, gain_vec);
|
|
92
|
+
vst1q_f32(&data[i], samples);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle remainder
|
|
96
|
+
for (size_t i = simd_end; i < numSamples; ++i)
|
|
97
|
+
{
|
|
98
|
+
data[i] *= m_gain;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#else
|
|
102
|
+
// Scalar fallback (compiler may auto-vectorize)
|
|
103
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
104
|
+
{
|
|
105
|
+
data[i] *= m_gain;
|
|
106
|
+
}
|
|
107
|
+
#endif
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const char *getType() const override
|
|
111
|
+
{
|
|
112
|
+
return "amplify";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
116
|
+
{
|
|
117
|
+
// Stateless - return empty object
|
|
118
|
+
return Napi::Object::New(env);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
void deserializeState(const Napi::Object &state) override
|
|
122
|
+
{
|
|
123
|
+
// Stateless - nothing to restore
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Serialization
|
|
127
|
+
void serializeToon(dsp::toon::Serializer &s) const override
|
|
128
|
+
{
|
|
129
|
+
// Stateless - no data to serialize
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
void deserializeToon(dsp::toon::Deserializer &d) override
|
|
133
|
+
{
|
|
134
|
+
// Stateless - no data to deserialize
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
void reset() override
|
|
138
|
+
{
|
|
139
|
+
// Stateless - no internal state to reset
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private:
|
|
143
|
+
float m_gain;
|
|
144
|
+
};
|
|
145
|
+
} // namespace adapters
|
|
146
|
+
} // namespace dsp
|
|
147
|
+
|
|
148
|
+
#endif // DSP_ADAPTERS_AMPLIFYSTAGE_H
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include "../IDspStage.h"
|
|
4
|
+
#include "../utils/Toon.h"
|
|
4
5
|
#include <cmath>
|
|
5
6
|
#include <stdexcept>
|
|
6
7
|
#include <string>
|
|
@@ -9,19 +10,19 @@ namespace dsp::adapters
|
|
|
9
10
|
{
|
|
10
11
|
/**
|
|
11
12
|
* @brief Clip Detection Stage - Detects when samples exceed a threshold.
|
|
12
|
-
*
|
|
13
|
+
*
|
|
13
14
|
* This stage analyzes the input signal and outputs a binary (0.0 or 1.0) indicator
|
|
14
15
|
* showing where clipping occurs. This is useful for:
|
|
15
16
|
* - Audio clipping detection (overload prevention)
|
|
16
17
|
* - Saturation detection in ADC signals
|
|
17
18
|
* - Quality control in data acquisition
|
|
18
|
-
*
|
|
19
|
+
*
|
|
19
20
|
* **Output:**
|
|
20
21
|
* - 1.0 when |sample| >= threshold (clipping detected)
|
|
21
22
|
* - 0.0 when |sample| < threshold (no clipping)
|
|
22
|
-
*
|
|
23
|
+
*
|
|
23
24
|
* **Processing:** Stateless, processes each sample independently
|
|
24
|
-
*
|
|
25
|
+
*
|
|
25
26
|
* @example
|
|
26
27
|
* ```cpp
|
|
27
28
|
* // Detect audio clipping at 0.95 (5% headroom)
|
|
@@ -77,6 +78,16 @@ namespace dsp::adapters
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
inline void serializeToon(dsp::toon::Serializer &s) const override
|
|
82
|
+
{
|
|
83
|
+
s.writeFloat(m_threshold);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
inline void deserializeToon(dsp::toon::Deserializer &d) override
|
|
87
|
+
{
|
|
88
|
+
m_threshold = d.readFloat();
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
void reset() override
|
|
81
92
|
{
|
|
82
93
|
// No state to reset (stateless stage)
|