dspx 1.4.8 → 1.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dspx",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
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",
Binary file
@@ -342,19 +342,19 @@ namespace dsp
342
342
  throw std::runtime_error("setState() requires stateful mode");
343
343
  }
344
344
 
345
- // Validate size - must match the power-of-2 buffer size, not coefficient count
346
- if (state.size() != m_state.size())
347
- {
348
- throw std::invalid_argument("state size must match internal buffer size");
349
- }
350
- if (stateIndex >= state.size() && !state.empty())
351
- {
352
- throw std::invalid_argument("stateIndex out of range");
353
- }
354
-
355
345
  #if defined(__ARM_NEON) || defined(__aarch64__)
356
346
  if (m_useNeon && m_neonFilter)
357
347
  {
348
+ // Validate against NEON buffer size on ARM
349
+ if (state.size() != m_neonFilter->getBufferSize())
350
+ {
351
+ throw std::invalid_argument("state size must match internal buffer size");
352
+ }
353
+ if (stateIndex >= state.size() && !state.empty())
354
+ {
355
+ throw std::invalid_argument("stateIndex out of range");
356
+ }
357
+
358
358
  // Convert to float vector for NEON filter's linearization
359
359
  std::vector<float> floatState(state.size());
360
360
  for (size_t i = 0; i < state.size(); ++i)
@@ -365,6 +365,17 @@ namespace dsp
365
365
  return;
366
366
  }
367
367
  #endif
368
+
369
+ // Validate size - must match the power-of-2 buffer size, not coefficient count
370
+ if (state.size() != m_state.size())
371
+ {
372
+ throw std::invalid_argument("state size must match internal buffer size");
373
+ }
374
+ if (stateIndex >= state.size() && !state.empty())
375
+ {
376
+ throw std::invalid_argument("stateIndex out of range");
377
+ }
378
+
368
379
  m_state = state;
369
380
  m_stateIndex = stateIndex;
370
381
  }
@@ -233,58 +233,52 @@ namespace dsp::core
233
233
  */
234
234
  std::pair<std::vector<float>, size_t> exportLinearState() const
235
235
  {
236
- #if defined(__ARM_NEON) || defined(__aarch64__)
237
236
  const float *state = getState();
238
237
  std::vector<float> linearState(m_bufferSize, 0.0f);
239
238
 
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);
239
+ // Extract the circular buffer into linear format (oldest->newest)
240
+ // m_head points to the position of the most recent sample
241
+ // The convolution reads from (m_head - numTaps + 1), which is the oldest sample position
242
+
243
+ // Calculate the read start position (oldest valid sample)
244
+ size_t readStart = (m_head + m_bufferSize - m_numTaps + 1) & m_headMask;
244
245
 
245
- // Un-rotate: copy from oldest->newest into linear array
246
+ // Copy samples in order: oldest->newest into linear positions [0..bufferSize-1]
246
247
  for (size_t i = 0; i < m_bufferSize; ++i)
247
248
  {
248
- linearState[i] = state[(oldestPos + i) & m_headMask];
249
+ linearState[i] = state[(readStart + i) & m_headMask];
249
250
  }
250
251
 
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
252
+ // Return 0 as stateIndex to indicate the next write should go to position 0
253
+ // (overwriting the oldest sample in the linear layout)
254
+ return {linearState, 0};
258
255
  }
259
256
 
260
257
  /**
261
258
  * @brief Import state from linear format (oldest->newest) after deserialization
262
259
  * @param linearState Linear state vector (oldest->newest)
263
- * @param stateIndex State index (number of valid samples - 1)
260
+ * @param stateIndex State index (must be 0 for linear layout)
264
261
  */
265
262
  void importLinearState(const std::vector<float> &linearState, size_t stateIndex)
266
263
  {
267
- #if defined(__ARM_NEON) || defined(__aarch64__)
268
264
  float *state = getState();
269
265
 
270
- // Copy linear state into circular buffer
266
+ // Copy the linear state directly into the circular buffer starting at position 0
267
+ // This creates a "linearized" layout where oldest is at 0, newest at numTaps-1
271
268
  for (size_t i = 0; i < m_bufferSize && i < linearState.size(); ++i)
272
269
  {
273
270
  state[i] = linearState[i];
274
271
  state[i + m_bufferSize] = linearState[i]; // Guard zone
275
272
  }
276
273
 
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
274
+ // Set m_head to point to the newest sample (last valid position)
275
+ // After import: oldest=0, newest=(numTaps-1)
276
+ // m_head should point to position (numTaps - 1)
277
+ // Next write will go to position numTaps, which is correct
278
+ m_head = (m_numTaps > 0) ? (m_numTaps - 1) : 0;
279
+
280
+ // Mark buffer as filled so we don't return zeros during transient phase
281
+ m_samplesProcessed = m_numTaps;
288
282
  }
289
283
 
290
284
  size_t getNumTaps() const { return m_numTaps; }