dspx 1.4.2 → 1.4.8
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 +4 -2
- package/binding.gyp +1 -1
- package/package.json +11 -3
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/src/native/core/FirFilter.cc +26 -0
- package/src/native/core/FirFilterNeon.h +60 -0
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
A modern DSP library built for Node.js backends processing real-time biosignals, audio streams, and sensor data. Features native C++ filters with full state serialization (to Redis, S3, or any storage backend), enabling seamless processing across service restarts and distributed workers.
|
|
13
13
|
|
|
14
|
+
[View the benchmarks](https://github.com/A-KGeorge/dspx-benchmark/)
|
|
15
|
+
|
|
14
16
|
---
|
|
15
17
|
|
|
16
18
|
## ✨ Features
|
|
@@ -475,7 +477,7 @@ const clean = notch.process(noisySignal);
|
|
|
475
477
|
import { createDspPipeline, Convolution } from "dspx";
|
|
476
478
|
const pipeline = createDspPipeline();
|
|
477
479
|
pipeline.addStage(
|
|
478
|
-
new Convolution({ kernel: OPTIMAL_LOWPASS_COEFFS.cutoff_0_2 })
|
|
480
|
+
new Convolution({ kernel: OPTIMAL_LOWPASS_COEFFS.cutoff_0_2 }),
|
|
479
481
|
);
|
|
480
482
|
|
|
481
483
|
// ✅ Zero Python dependency - coefficients ship with the library!
|
|
@@ -1661,7 +1663,7 @@ const saveBreaker = new CircuitBreaker(
|
|
|
1661
1663
|
timeout: 2000, // Fail if >2s
|
|
1662
1664
|
errorThresholdPercentage: 50, // Trip after 50% failures
|
|
1663
1665
|
resetTimeout: 30000, // Try recovery after 30s
|
|
1664
|
-
}
|
|
1666
|
+
},
|
|
1665
1667
|
);
|
|
1666
1668
|
|
|
1667
1669
|
saveBreaker.fallback(() => {
|
package/binding.gyp
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
'OTHER_CPLUSPLUSFLAGS+': [ '-msse3', '-mavx', '-mavx2' ]
|
|
94
94
|
}
|
|
95
95
|
}],
|
|
96
|
-
# Condition for arm64 architecture (Android, iOS, M1/M2 Macs,
|
|
96
|
+
# Condition for arm64 architecture (Android, iOS, M1/M2 Macs, etc.)
|
|
97
97
|
['target_arch=="arm64"', {
|
|
98
98
|
# ARMv8-a baseline: NEON + FP support (compatible with all ARMv8 CPUs)
|
|
99
99
|
"cflags+": [ "-march=armv8-a+fp+simd" ],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dspx",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.8",
|
|
4
4
|
"description": "High-performance DSP library with native C++ acceleration and Redis state persistence",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build:ts": "tsc",
|
|
22
22
|
"build:native": "node-gyp rebuild",
|
|
23
23
|
"build": "npm run build:native && npm run build:ts",
|
|
24
|
-
"prebuildify": "prebuildify --napi --strip --target 18.0.0 --target 20.0.0 --target 22.0.0",
|
|
24
|
+
"prebuildify": "prebuildify --napi --strip --target 18.0.0 --target 20.0.0 --target 22.0.0 --target 24.0.0",
|
|
25
25
|
"changeset": "changeset",
|
|
26
26
|
"version": "changeset version",
|
|
27
27
|
"publish-packages": "changeset publish"
|
|
@@ -33,7 +33,11 @@
|
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
36
|
-
"url": "https://github.com/A-KGeorge/dspx"
|
|
36
|
+
"url": "git+https://github.com/A-KGeorge/dspx.git"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"provenance": true
|
|
37
41
|
},
|
|
38
42
|
"keywords": [
|
|
39
43
|
"dsp",
|
|
@@ -47,6 +51,10 @@
|
|
|
47
51
|
"author": "Alan Kochukalam George",
|
|
48
52
|
"license": "Apache-2.0",
|
|
49
53
|
"type": "module",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0",
|
|
56
|
+
"npm": ">=11.5.1"
|
|
57
|
+
},
|
|
50
58
|
"dependencies": {
|
|
51
59
|
"cross-env": "^7.0.3",
|
|
52
60
|
"node-addon-api": "^8.5.0",
|
|
Binary file
|
|
@@ -318,6 +318,19 @@ namespace dsp
|
|
|
318
318
|
template <typename T>
|
|
319
319
|
std::pair<std::vector<T>, size_t> FirFilter<T>::getState() const
|
|
320
320
|
{
|
|
321
|
+
#if defined(__ARM_NEON) || defined(__aarch64__)
|
|
322
|
+
if (m_useNeon && m_neonFilter)
|
|
323
|
+
{
|
|
324
|
+
// Use NEON filter's linearization on ARM
|
|
325
|
+
auto [linearState, stateIndex] = m_neonFilter->exportLinearState();
|
|
326
|
+
std::vector<T> state(linearState.size());
|
|
327
|
+
for (size_t i = 0; i < linearState.size(); ++i)
|
|
328
|
+
{
|
|
329
|
+
state[i] = static_cast<T>(linearState[i]);
|
|
330
|
+
}
|
|
331
|
+
return {state, stateIndex};
|
|
332
|
+
}
|
|
333
|
+
#endif
|
|
321
334
|
return {m_state, m_stateIndex};
|
|
322
335
|
}
|
|
323
336
|
|
|
@@ -339,6 +352,19 @@ namespace dsp
|
|
|
339
352
|
throw std::invalid_argument("stateIndex out of range");
|
|
340
353
|
}
|
|
341
354
|
|
|
355
|
+
#if defined(__ARM_NEON) || defined(__aarch64__)
|
|
356
|
+
if (m_useNeon && m_neonFilter)
|
|
357
|
+
{
|
|
358
|
+
// Convert to float vector for NEON filter's linearization
|
|
359
|
+
std::vector<float> floatState(state.size());
|
|
360
|
+
for (size_t i = 0; i < state.size(); ++i)
|
|
361
|
+
{
|
|
362
|
+
floatState[i] = static_cast<float>(state[i]);
|
|
363
|
+
}
|
|
364
|
+
m_neonFilter->importLinearState(floatState, stateIndex);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
#endif
|
|
342
368
|
m_state = state;
|
|
343
369
|
m_stateIndex = stateIndex;
|
|
344
370
|
}
|
|
@@ -227,6 +227,66 @@ namespace dsp::core
|
|
|
227
227
|
m_samplesProcessed = 0;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* @brief Export state in linear format (oldest->newest) for serialization
|
|
232
|
+
* @return Pair of linear state vector and state index
|
|
233
|
+
*/
|
|
234
|
+
std::pair<std::vector<float>, size_t> exportLinearState() const
|
|
235
|
+
{
|
|
236
|
+
#if defined(__ARM_NEON) || defined(__aarch64__)
|
|
237
|
+
const float *state = getState();
|
|
238
|
+
std::vector<float> linearState(m_bufferSize, 0.0f);
|
|
239
|
+
|
|
240
|
+
// Calculate oldest sample position in circular buffer
|
|
241
|
+
size_t oldestPos = (m_head >= m_numTaps - 1)
|
|
242
|
+
? m_head - (m_numTaps - 1)
|
|
243
|
+
: m_head + m_bufferSize - (m_numTaps - 1);
|
|
244
|
+
|
|
245
|
+
// Un-rotate: copy from oldest->newest into linear array
|
|
246
|
+
for (size_t i = 0; i < m_bufferSize; ++i)
|
|
247
|
+
{
|
|
248
|
+
linearState[i] = state[(oldestPos + i) & m_headMask];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {linearState, m_numTaps - 1};
|
|
252
|
+
#else
|
|
253
|
+
// Scalar path: state is already linear
|
|
254
|
+
const float *state = getState();
|
|
255
|
+
std::vector<float> linearState(state, state + m_bufferSize);
|
|
256
|
+
return {linearState, m_head};
|
|
257
|
+
#endif
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @brief Import state from linear format (oldest->newest) after deserialization
|
|
262
|
+
* @param linearState Linear state vector (oldest->newest)
|
|
263
|
+
* @param stateIndex State index (number of valid samples - 1)
|
|
264
|
+
*/
|
|
265
|
+
void importLinearState(const std::vector<float> &linearState, size_t stateIndex)
|
|
266
|
+
{
|
|
267
|
+
#if defined(__ARM_NEON) || defined(__aarch64__)
|
|
268
|
+
float *state = getState();
|
|
269
|
+
|
|
270
|
+
// Copy linear state into circular buffer
|
|
271
|
+
for (size_t i = 0; i < m_bufferSize && i < linearState.size(); ++i)
|
|
272
|
+
{
|
|
273
|
+
state[i] = linearState[i];
|
|
274
|
+
state[i + m_bufferSize] = linearState[i]; // Guard zone
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Set head to point where newest sample will be written
|
|
278
|
+
m_head = stateIndex;
|
|
279
|
+
#else
|
|
280
|
+
// Scalar path: direct copy
|
|
281
|
+
float *state = getState();
|
|
282
|
+
for (size_t i = 0; i < m_bufferSize && i < linearState.size(); ++i)
|
|
283
|
+
{
|
|
284
|
+
state[i] = linearState[i];
|
|
285
|
+
}
|
|
286
|
+
m_head = stateIndex;
|
|
287
|
+
#endif
|
|
288
|
+
}
|
|
289
|
+
|
|
230
290
|
size_t getNumTaps() const { return m_numTaps; }
|
|
231
291
|
size_t getBufferSize() const { return m_bufferSize; }
|
|
232
292
|
|