dspx 1.3.7 → 1.4.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 +97 -27
- package/binding.gyp +2 -1
- package/dist/StateResilienceConfig.d.ts +17 -0
- package/dist/StateResilienceConfig.d.ts.map +1 -0
- package/dist/StateResilienceConfig.js +2 -0
- package/dist/StateResilienceConfig.js.map +1 -0
- package/dist/bindings.d.ts +119 -9
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +177 -12
- package/dist/bindings.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/src/native/DspPipeline.cc +385 -108
- package/src/native/adapters/TimeAlignmentStage.cc +743 -0
- package/src/native/adapters/TimeAlignmentStage.h +183 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeAlignmentStage.cc
|
|
3
|
+
*
|
|
4
|
+
* Production-grade irregular timestamp resampling implementation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#define _USE_MATH_DEFINES
|
|
8
|
+
#include <cmath>
|
|
9
|
+
|
|
10
|
+
#include "TimeAlignmentStage.h"
|
|
11
|
+
#include "../utils/Toon.h"
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <stdexcept>
|
|
14
|
+
#include <sstream>
|
|
15
|
+
#include <iostream>
|
|
16
|
+
#include <cstdlib>
|
|
17
|
+
#include <cassert>
|
|
18
|
+
|
|
19
|
+
// Debug assertion macro
|
|
20
|
+
#ifdef _DEBUG
|
|
21
|
+
#define ASSERT_BOUNDS(idx, maxSize, msg) \
|
|
22
|
+
if ((idx) >= (maxSize)) { \
|
|
23
|
+
std::cerr << "[BOUNDS ERROR] " << msg << ": idx=" << (idx) << ", max=" << (maxSize) << std::endl; \
|
|
24
|
+
throw std::out_of_range(msg); \
|
|
25
|
+
}
|
|
26
|
+
#else
|
|
27
|
+
#define ASSERT_BOUNDS(idx, maxSize, msg) ((void)0)
|
|
28
|
+
#endif
|
|
29
|
+
|
|
30
|
+
// Helper function to check debug flag
|
|
31
|
+
inline bool isDebugEnabled()
|
|
32
|
+
{
|
|
33
|
+
return std::getenv("DSPX_DEBUG") != nullptr;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#ifndef M_PI
|
|
37
|
+
#define M_PI 3.14159265358979323846
|
|
38
|
+
#endif
|
|
39
|
+
|
|
40
|
+
// SIMD intrinsics
|
|
41
|
+
#if defined(__AVX2__) || (defined(_MSC_VER) && defined(__AVX2__))
|
|
42
|
+
#include <immintrin.h>
|
|
43
|
+
#define HAS_AVX2 1
|
|
44
|
+
#elif defined(__SSE__) || defined(__SSE2__) || (defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)))
|
|
45
|
+
#include <emmintrin.h>
|
|
46
|
+
#define HAS_SSE 1
|
|
47
|
+
#elif defined(__ARM_NEON) || defined(__ARM_NEON__)
|
|
48
|
+
#include <arm_neon.h>
|
|
49
|
+
#define HAS_NEON 1
|
|
50
|
+
#endif
|
|
51
|
+
|
|
52
|
+
namespace dsp
|
|
53
|
+
{
|
|
54
|
+
namespace adapters
|
|
55
|
+
{
|
|
56
|
+
TimeAlignmentStage::TimeAlignmentStage(
|
|
57
|
+
float targetSampleRate,
|
|
58
|
+
InterpolationMethod interpMethod,
|
|
59
|
+
GapPolicy gapPolicy,
|
|
60
|
+
float gapThreshold,
|
|
61
|
+
DriftCompensation driftComp)
|
|
62
|
+
: m_targetSampleRate(targetSampleRate),
|
|
63
|
+
m_interpMethod(interpMethod),
|
|
64
|
+
m_gapPolicy(gapPolicy),
|
|
65
|
+
m_gapThreshold(gapThreshold),
|
|
66
|
+
m_driftComp(driftComp),
|
|
67
|
+
m_estimatedSampleRate(targetSampleRate),
|
|
68
|
+
m_driftWindowSize(100),
|
|
69
|
+
m_lastTimeScaleFactor(1.0),
|
|
70
|
+
m_lastStartTime(0.0f),
|
|
71
|
+
m_lastEndTime(0.0f)
|
|
72
|
+
{
|
|
73
|
+
if (targetSampleRate <= 0.0f)
|
|
74
|
+
{
|
|
75
|
+
throw std::invalid_argument("TimeAlignmentStage: targetSampleRate must be positive");
|
|
76
|
+
}
|
|
77
|
+
if (gapThreshold < 1.0f)
|
|
78
|
+
{
|
|
79
|
+
throw std::invalid_argument("TimeAlignmentStage: gapThreshold must be >= 1.0");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
reset();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void TimeAlignmentStage::reset()
|
|
86
|
+
{
|
|
87
|
+
m_stats = Statistics{};
|
|
88
|
+
m_estimatedSampleRate = m_targetSampleRate;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
bool TimeAlignmentStage::isResizing() const
|
|
92
|
+
{
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
size_t TimeAlignmentStage::calculateOutputSize(size_t inputSize) const
|
|
97
|
+
{
|
|
98
|
+
// Cannot determine exact output size without timestamps
|
|
99
|
+
// Worst case: sparse input with long time span resampled to high target rate
|
|
100
|
+
// Conservative: allocate 10x input size to handle extreme resampling ratios
|
|
101
|
+
// Examples that need this:
|
|
102
|
+
// - Input: 50 samples over 5 seconds → Output at 100Hz = 500 samples (10x)
|
|
103
|
+
// - Input: 10 samples over 10 seconds → Output at 1000Hz = 10000 samples (1000x)
|
|
104
|
+
// Using 10x as reasonable upper bound for typical use cases
|
|
105
|
+
// If actualOutputSize exceeds this, processResizing will corrupt memory
|
|
106
|
+
return inputSize * 10;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
double TimeAlignmentStage::getTimeScaleFactor() const
|
|
110
|
+
{
|
|
111
|
+
// Return the cached time scale factor from last processResizing call
|
|
112
|
+
// This tells the pipeline how to adjust timestamps
|
|
113
|
+
return m_lastTimeScaleFactor;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void TimeAlignmentStage::processResizing(
|
|
117
|
+
const float *inputBuffer,
|
|
118
|
+
size_t inputSize,
|
|
119
|
+
float *outputBuffer,
|
|
120
|
+
size_t &outputSize,
|
|
121
|
+
int channels,
|
|
122
|
+
const float *timestamps)
|
|
123
|
+
{
|
|
124
|
+
if (isDebugEnabled())
|
|
125
|
+
{
|
|
126
|
+
std::cout << "[TimeAlignment] processResizing START: inputSize=" << inputSize
|
|
127
|
+
<< ", channels=" << channels
|
|
128
|
+
<< ", inputBuffer=" << inputBuffer
|
|
129
|
+
<< ", outputBuffer=" << outputBuffer
|
|
130
|
+
<< ", timestamps=" << timestamps << std::endl;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (inputSize == 0)
|
|
134
|
+
{
|
|
135
|
+
outputSize = 0;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (timestamps == nullptr)
|
|
140
|
+
{
|
|
141
|
+
throw std::runtime_error("TimeAlignmentStage: timestamps are required");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Reset statistics
|
|
145
|
+
m_stats = Statistics{};
|
|
146
|
+
size_t numInputSamples = inputSize / channels;
|
|
147
|
+
m_stats.inputSamples = numInputSamples;
|
|
148
|
+
|
|
149
|
+
if (isDebugEnabled())
|
|
150
|
+
{
|
|
151
|
+
std::cout << "[TimeAlignment] numInputSamples=" << numInputSamples << std::endl;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Estimate sample rate from input timestamps (for drift compensation)
|
|
155
|
+
if (m_driftComp != DriftCompensation::NONE)
|
|
156
|
+
{
|
|
157
|
+
estimateSampleRate(timestamps, numInputSamples, channels);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Detect gaps in input data
|
|
161
|
+
std::vector<size_t> gapIndices;
|
|
162
|
+
detectGaps(timestamps, numInputSamples, channels, gapIndices);
|
|
163
|
+
m_stats.gapsDetected = gapIndices.size();
|
|
164
|
+
|
|
165
|
+
// Calculate time span
|
|
166
|
+
ASSERT_BOUNDS(0, inputSize, "startTime access");
|
|
167
|
+
ASSERT_BOUNDS((numInputSamples - 1) * channels, inputSize, "endTime access");
|
|
168
|
+
float startTime = timestamps[0];
|
|
169
|
+
float endTime = timestamps[(numInputSamples - 1) * channels]; // Correct: Last time step
|
|
170
|
+
m_stats.timeSpanMs = endTime - startTime;
|
|
171
|
+
|
|
172
|
+
// Determine output sample count based on time span and target rate
|
|
173
|
+
float targetIntervalMs = 1000.0f / m_targetSampleRate;
|
|
174
|
+
size_t numOutputSamples = static_cast<size_t>(std::ceil(m_stats.timeSpanMs / targetIntervalMs)) + 1;
|
|
175
|
+
|
|
176
|
+
m_stats.outputSamples = numOutputSamples;
|
|
177
|
+
outputSize = numOutputSamples * channels; // Total values (samples * channels)
|
|
178
|
+
|
|
179
|
+
// Cache time scale factor for getTimeScaleFactor()
|
|
180
|
+
// This tells the pipeline how to interpolate timestamps
|
|
181
|
+
m_lastStartTime = startTime;
|
|
182
|
+
m_lastEndTime = endTime;
|
|
183
|
+
float inputTimeSpan = endTime - startTime;
|
|
184
|
+
float outputTimeSpan = (numOutputSamples > 1) ? ((numOutputSamples - 1) * targetIntervalMs) : 0.0f;
|
|
185
|
+
m_lastTimeScaleFactor = (inputTimeSpan > 0.0f) ? (outputTimeSpan / inputTimeSpan) : 1.0;
|
|
186
|
+
|
|
187
|
+
// Two-pointer search: maintain search position across iterations
|
|
188
|
+
size_t searchStart = 0;
|
|
189
|
+
|
|
190
|
+
if (isDebugEnabled())
|
|
191
|
+
{
|
|
192
|
+
std::cout << "[TimeAlignment] Starting interpolation: numOutputSamples=" << numOutputSamples
|
|
193
|
+
<< ", outputSize=" << outputSize << std::endl;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Process each output sample using time-based interpolation
|
|
197
|
+
for (size_t outIdx = 0; outIdx < numOutputSamples; ++outIdx)
|
|
198
|
+
{
|
|
199
|
+
// Calculate target time on uniform grid
|
|
200
|
+
float targetTime = startTime + (outIdx * targetIntervalMs);
|
|
201
|
+
|
|
202
|
+
// Check if this falls in a gap
|
|
203
|
+
bool inGap = false;
|
|
204
|
+
size_t gapStart = 0, gapEnd = 0;
|
|
205
|
+
|
|
206
|
+
for (size_t gapIdx : gapIndices)
|
|
207
|
+
{
|
|
208
|
+
// gapIdx points to sample BEFORE the gap
|
|
209
|
+
// Make sure gapIdx + 1 is valid
|
|
210
|
+
if (gapIdx + 1 >= numInputSamples)
|
|
211
|
+
{
|
|
212
|
+
continue; // Skip invalid gap index
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
float gapStartTime = timestamps[gapIdx * channels];
|
|
216
|
+
float gapEndTime = timestamps[(gapIdx + 1) * channels];
|
|
217
|
+
|
|
218
|
+
if (targetTime > gapStartTime && targetTime < gapEndTime)
|
|
219
|
+
{
|
|
220
|
+
inGap = true;
|
|
221
|
+
gapStart = gapIdx;
|
|
222
|
+
gapEnd = gapIdx + 1;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Handle gaps according to policy
|
|
228
|
+
if (inGap)
|
|
229
|
+
{
|
|
230
|
+
switch (m_gapPolicy)
|
|
231
|
+
{
|
|
232
|
+
case GapPolicy::ERROR:
|
|
233
|
+
throw std::runtime_error("TimeAlignmentStage: Gap detected at output index " +
|
|
234
|
+
std::to_string(outIdx) + ", targetTime=" + std::to_string(targetTime));
|
|
235
|
+
|
|
236
|
+
case GapPolicy::ZERO_FILL:
|
|
237
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
238
|
+
{
|
|
239
|
+
size_t writeIdx = outIdx * channels + ch;
|
|
240
|
+
ASSERT_BOUNDS(writeIdx, outputSize, "ZERO_FILL output write");
|
|
241
|
+
outputBuffer[writeIdx] = 0.0f;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case GapPolicy::HOLD:
|
|
246
|
+
// Hold last valid value before gap
|
|
247
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
248
|
+
{
|
|
249
|
+
size_t readIdx = gapStart * channels + ch;
|
|
250
|
+
size_t writeIdx = outIdx * channels + ch;
|
|
251
|
+
ASSERT_BOUNDS(readIdx, inputSize, "HOLD input read");
|
|
252
|
+
ASSERT_BOUNDS(writeIdx, outputSize, "HOLD output write");
|
|
253
|
+
outputBuffer[writeIdx] = inputBuffer[readIdx];
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case GapPolicy::INTERPOLATE:
|
|
258
|
+
// Linear interpolation across gap
|
|
259
|
+
{
|
|
260
|
+
float t0 = timestamps[gapStart * channels];
|
|
261
|
+
float t1 = timestamps[gapEnd * channels];
|
|
262
|
+
float denominator = t1 - t0;
|
|
263
|
+
|
|
264
|
+
// Protection against division by zero
|
|
265
|
+
if (std::abs(denominator) < 1e-6f)
|
|
266
|
+
{
|
|
267
|
+
// Degenerate case: use start value
|
|
268
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
269
|
+
{
|
|
270
|
+
outputBuffer[outIdx * channels + ch] = inputBuffer[gapStart * channels + ch];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else
|
|
274
|
+
{
|
|
275
|
+
float alpha = (targetTime - t0) / denominator;
|
|
276
|
+
|
|
277
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
278
|
+
{
|
|
279
|
+
float v0 = inputBuffer[gapStart * channels + ch];
|
|
280
|
+
float v1 = inputBuffer[gapEnd * channels + ch];
|
|
281
|
+
outputBuffer[outIdx * channels + ch] = v0 + alpha * (v1 - v0);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
case GapPolicy::EXTRAPOLATE:
|
|
288
|
+
// Extrapolate from last two valid samples
|
|
289
|
+
if (gapStart > 0)
|
|
290
|
+
{
|
|
291
|
+
float t0 = timestamps[(gapStart - 1) * channels];
|
|
292
|
+
float t1 = timestamps[gapStart * channels];
|
|
293
|
+
float denominator = t1 - t0;
|
|
294
|
+
float slope = (std::abs(denominator) > 1e-6f) ? 1.0f / denominator : 0.0f;
|
|
295
|
+
|
|
296
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
297
|
+
{
|
|
298
|
+
float v0 = inputBuffer[(gapStart - 1) * channels + ch];
|
|
299
|
+
float v1 = inputBuffer[gapStart * channels + ch];
|
|
300
|
+
float delta = (targetTime - t1) * slope;
|
|
301
|
+
outputBuffer[outIdx * channels + ch] = v1 + delta * (v1 - v0);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else
|
|
305
|
+
{
|
|
306
|
+
// No samples before gap - use zero
|
|
307
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
308
|
+
{
|
|
309
|
+
outputBuffer[outIdx * channels + ch] = 0.0f;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else
|
|
316
|
+
{
|
|
317
|
+
// Not in gap - perform normal interpolation
|
|
318
|
+
for (int ch = 0; ch < channels; ++ch)
|
|
319
|
+
{
|
|
320
|
+
size_t writeIdx = outIdx * channels + ch;
|
|
321
|
+
ASSERT_BOUNDS(writeIdx, outputSize, "Normal interpolation output write");
|
|
322
|
+
outputBuffer[writeIdx] = interpolate(
|
|
323
|
+
targetTime, timestamps, inputBuffer, numInputSamples, channels, ch, searchStart);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
void TimeAlignmentStage::estimateSampleRate(const float *timestamps, size_t numSamples, int channels)
|
|
330
|
+
{
|
|
331
|
+
if (numSamples < 2)
|
|
332
|
+
{
|
|
333
|
+
m_estimatedSampleRate = m_targetSampleRate;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (m_driftComp == DriftCompensation::REGRESSION)
|
|
338
|
+
{
|
|
339
|
+
// Linear regression: fit line to (index, timestamp) points
|
|
340
|
+
float sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
341
|
+
size_t n = std::min(numSamples, m_driftWindowSize);
|
|
342
|
+
|
|
343
|
+
for (size_t i = 0; i < n; ++i)
|
|
344
|
+
{
|
|
345
|
+
float x = static_cast<float>(i);
|
|
346
|
+
float y = timestamps[i * channels]; // Stride-aware: use first channel's timestamp
|
|
347
|
+
sumX += x;
|
|
348
|
+
sumY += y;
|
|
349
|
+
sumXY += x * y;
|
|
350
|
+
sumX2 += x * x;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
float slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
354
|
+
m_estimatedSampleRate = 1000.0f / slope; // Convert ms/sample to Hz
|
|
355
|
+
}
|
|
356
|
+
else if (m_driftComp == DriftCompensation::PLL)
|
|
357
|
+
{
|
|
358
|
+
// Phase-locked loop: exponential moving average
|
|
359
|
+
float alpha = 0.1f; // PLL time constant
|
|
360
|
+
float avgInterval = 0.0f;
|
|
361
|
+
size_t n = std::min(numSamples - 1, m_driftWindowSize);
|
|
362
|
+
|
|
363
|
+
for (size_t i = 1; i <= n; ++i)
|
|
364
|
+
{
|
|
365
|
+
float interval = timestamps[i * channels] - timestamps[(i - 1) * channels]; // Stride-aware
|
|
366
|
+
avgInterval = alpha * interval + (1.0f - alpha) * avgInterval;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
m_estimatedSampleRate = 1000.0f / avgInterval;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Store in statistics
|
|
373
|
+
m_stats.estimatedSampleRate = m_estimatedSampleRate;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
void TimeAlignmentStage::detectGaps(const float *timestamps, size_t numSamples, int channels, std::vector<size_t> &gapIndices)
|
|
377
|
+
{
|
|
378
|
+
gapIndices.clear();
|
|
379
|
+
|
|
380
|
+
if (numSamples < 2)
|
|
381
|
+
return;
|
|
382
|
+
|
|
383
|
+
float expectedInterval = 1000.0f / m_estimatedSampleRate;
|
|
384
|
+
float gapMinDuration = expectedInterval * m_gapThreshold;
|
|
385
|
+
|
|
386
|
+
float minGap = std::numeric_limits<float>::max();
|
|
387
|
+
float maxGap = 0.0f;
|
|
388
|
+
float sumIntervals = 0.0f;
|
|
389
|
+
float sumSquaredIntervals = 0.0f;
|
|
390
|
+
|
|
391
|
+
for (size_t i = 1; i < numSamples; ++i)
|
|
392
|
+
{
|
|
393
|
+
float delta = timestamps[i * channels] - timestamps[(i - 1) * channels]; // Stride-aware
|
|
394
|
+
sumIntervals += delta;
|
|
395
|
+
sumSquaredIntervals += delta * delta;
|
|
396
|
+
|
|
397
|
+
if (delta > gapMinDuration)
|
|
398
|
+
{
|
|
399
|
+
gapIndices.push_back(i - 1);
|
|
400
|
+
minGap = std::min(minGap, delta);
|
|
401
|
+
maxGap = std::max(maxGap, delta);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Update statistics
|
|
406
|
+
m_stats.avgIntervalMs = sumIntervals / (numSamples - 1);
|
|
407
|
+
float variance = (sumSquaredIntervals / (numSamples - 1)) - (m_stats.avgIntervalMs * m_stats.avgIntervalMs);
|
|
408
|
+
m_stats.stdDevIntervalMs = std::sqrt(std::max(0.0f, variance));
|
|
409
|
+
|
|
410
|
+
if (!gapIndices.empty())
|
|
411
|
+
{
|
|
412
|
+
m_stats.minGapDurationMs = minGap;
|
|
413
|
+
m_stats.maxGapDurationMs = maxGap;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
float TimeAlignmentStage::interpolate(
|
|
418
|
+
float targetTime,
|
|
419
|
+
const float *timestamps,
|
|
420
|
+
const float *samples,
|
|
421
|
+
size_t numSamples,
|
|
422
|
+
int channels,
|
|
423
|
+
int channel,
|
|
424
|
+
size_t &searchStart)
|
|
425
|
+
{
|
|
426
|
+
float result = 0.0f;
|
|
427
|
+
|
|
428
|
+
switch (m_interpMethod)
|
|
429
|
+
{
|
|
430
|
+
case InterpolationMethod::LINEAR:
|
|
431
|
+
interpolateLinear(targetTime, timestamps, samples, numSamples, channels, channel, searchStart, result);
|
|
432
|
+
break;
|
|
433
|
+
case InterpolationMethod::CUBIC:
|
|
434
|
+
interpolateCubic(targetTime, timestamps, samples, numSamples, channels, channel, searchStart, result);
|
|
435
|
+
break;
|
|
436
|
+
case InterpolationMethod::SINC:
|
|
437
|
+
interpolateSinc(targetTime, timestamps, samples, numSamples, channels, channel, searchStart, result);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
void TimeAlignmentStage::interpolateLinear(
|
|
445
|
+
float targetTime,
|
|
446
|
+
const float *timestamps,
|
|
447
|
+
const float *samples,
|
|
448
|
+
size_t numSamples,
|
|
449
|
+
int channels,
|
|
450
|
+
int channel,
|
|
451
|
+
size_t &searchStart,
|
|
452
|
+
float &output)
|
|
453
|
+
{
|
|
454
|
+
if (isDebugEnabled())
|
|
455
|
+
{
|
|
456
|
+
std::cout << "[TimeAlignment] interpolateLinear: targetTime=" << targetTime
|
|
457
|
+
<< ", numSamples=" << numSamples
|
|
458
|
+
<< ", channels=" << channels
|
|
459
|
+
<< ", channel=" << channel
|
|
460
|
+
<< ", searchStart=" << searchStart << std::endl;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Find bracketing interval using two-pointer search
|
|
464
|
+
size_t idx = findBracketingInterval(targetTime, timestamps, numSamples, channels, searchStart);
|
|
465
|
+
|
|
466
|
+
if (isDebugEnabled())
|
|
467
|
+
{
|
|
468
|
+
std::cout << "[TimeAlignment] findBracketingInterval returned idx=" << idx << std::endl;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Handle edge cases
|
|
472
|
+
if (targetTime <= timestamps[0])
|
|
473
|
+
{
|
|
474
|
+
// Before first sample
|
|
475
|
+
if (m_gapPolicy == GapPolicy::EXTRAPOLATE && numSamples >= 2)
|
|
476
|
+
{
|
|
477
|
+
// Extrapolate backward
|
|
478
|
+
float t0 = timestamps[0];
|
|
479
|
+
float t1 = timestamps[channels];
|
|
480
|
+
float v0 = samples[channel];
|
|
481
|
+
float v1 = samples[channels + channel];
|
|
482
|
+
float denominator = t1 - t0;
|
|
483
|
+
if (std::abs(denominator) < 1e-6f)
|
|
484
|
+
{
|
|
485
|
+
output = v0; // Degenerate case
|
|
486
|
+
}
|
|
487
|
+
else
|
|
488
|
+
{
|
|
489
|
+
float alpha = (targetTime - t0) / denominator;
|
|
490
|
+
output = v0 + alpha * (v1 - v0);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else
|
|
494
|
+
{
|
|
495
|
+
output = samples[channel];
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (targetTime >= timestamps[(numSamples - 1) * channels])
|
|
501
|
+
{
|
|
502
|
+
// After last sample
|
|
503
|
+
if (m_gapPolicy == GapPolicy::EXTRAPOLATE && numSamples >= 2)
|
|
504
|
+
{
|
|
505
|
+
// Extrapolate forward
|
|
506
|
+
float t0 = timestamps[(numSamples - 2) * channels];
|
|
507
|
+
float t1 = timestamps[(numSamples - 1) * channels];
|
|
508
|
+
float v0 = samples[(numSamples - 2) * channels + channel];
|
|
509
|
+
float v1 = samples[(numSamples - 1) * channels + channel];
|
|
510
|
+
float denominator = t1 - t0;
|
|
511
|
+
if (std::abs(denominator) < 1e-6f)
|
|
512
|
+
{
|
|
513
|
+
output = v1; // Degenerate case
|
|
514
|
+
}
|
|
515
|
+
else
|
|
516
|
+
{
|
|
517
|
+
float alpha = (targetTime - t1) / denominator;
|
|
518
|
+
output = v1 + alpha * (v1 - v0);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else
|
|
522
|
+
{
|
|
523
|
+
output = samples[(numSamples - 1) * channels + channel];
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Normal case: interpolate between idx and idx+1
|
|
529
|
+
float t0 = timestamps[idx * channels];
|
|
530
|
+
float t1 = timestamps[(idx + 1) * channels];
|
|
531
|
+
float v0 = samples[idx * channels + channel];
|
|
532
|
+
float v1 = samples[(idx + 1) * channels + channel];
|
|
533
|
+
|
|
534
|
+
float denominator = t1 - t0;
|
|
535
|
+
if (std::abs(denominator) < 1e-6f)
|
|
536
|
+
{
|
|
537
|
+
// Degenerate case: timestamps are identical, just use v0
|
|
538
|
+
output = v0;
|
|
539
|
+
}
|
|
540
|
+
else
|
|
541
|
+
{
|
|
542
|
+
float alpha = (targetTime - t0) / denominator;
|
|
543
|
+
output = v0 + alpha * (v1 - v0);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Update search start for next iteration
|
|
547
|
+
searchStart = idx;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
void TimeAlignmentStage::interpolateCubic(
|
|
551
|
+
float targetTime,
|
|
552
|
+
const float *timestamps,
|
|
553
|
+
const float *samples,
|
|
554
|
+
size_t numSamples,
|
|
555
|
+
int channels,
|
|
556
|
+
int channel,
|
|
557
|
+
size_t &searchStart,
|
|
558
|
+
float &output)
|
|
559
|
+
{
|
|
560
|
+
// Cubic spline requires 4 points: [i-1, i, i+1, i+2]
|
|
561
|
+
size_t idx = findBracketingInterval(targetTime, timestamps, numSamples, channels, searchStart);
|
|
562
|
+
|
|
563
|
+
// Need at least 4 points
|
|
564
|
+
if (numSamples < 4)
|
|
565
|
+
{
|
|
566
|
+
interpolateLinear(targetTime, timestamps, samples, numSamples, channels, channel, searchStart, output);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Get 4 points for cubic interpolation
|
|
571
|
+
size_t i0 = (idx > 0) ? idx - 1 : 0;
|
|
572
|
+
size_t i1 = idx;
|
|
573
|
+
size_t i2 = (idx + 1 < numSamples) ? idx + 1 : numSamples - 1;
|
|
574
|
+
size_t i3 = (idx + 2 < numSamples) ? idx + 2 : numSamples - 1;
|
|
575
|
+
|
|
576
|
+
size_t totalSamples = numSamples * channels;
|
|
577
|
+
ASSERT_BOUNDS(i0 * channels, totalSamples, "cubic t0 access");
|
|
578
|
+
ASSERT_BOUNDS(i1 * channels, totalSamples, "cubic t1 access");
|
|
579
|
+
ASSERT_BOUNDS(i2 * channels, totalSamples, "cubic t2 access");
|
|
580
|
+
ASSERT_BOUNDS(i3 * channels, totalSamples, "cubic t3 access");
|
|
581
|
+
ASSERT_BOUNDS(i0 * channels + channel, totalSamples, "cubic v0 access");
|
|
582
|
+
ASSERT_BOUNDS(i1 * channels + channel, totalSamples, "cubic v1 access");
|
|
583
|
+
ASSERT_BOUNDS(i2 * channels + channel, totalSamples, "cubic v2 access");
|
|
584
|
+
ASSERT_BOUNDS(i3 * channels + channel, totalSamples, "cubic v3 access");
|
|
585
|
+
|
|
586
|
+
float t0 = timestamps[i0 * channels];
|
|
587
|
+
float t1 = timestamps[i1 * channels];
|
|
588
|
+
float t2 = timestamps[i2 * channels];
|
|
589
|
+
float t3 = timestamps[i3 * channels];
|
|
590
|
+
|
|
591
|
+
float v0 = samples[i0 * channels + channel];
|
|
592
|
+
float v1 = samples[i1 * channels + channel];
|
|
593
|
+
float v2 = samples[i2 * channels + channel];
|
|
594
|
+
float v3 = samples[i3 * channels + channel];
|
|
595
|
+
|
|
596
|
+
// CRITICAL FIX: Check for degenerate case where t2 == t1
|
|
597
|
+
// This can happen at array boundaries when clamping produces duplicate indices
|
|
598
|
+
float denominator = t2 - t1;
|
|
599
|
+
if (std::abs(denominator) < 1e-6f)
|
|
600
|
+
{
|
|
601
|
+
// Degenerate case: fall back to linear or just return v1
|
|
602
|
+
interpolateLinear(targetTime, timestamps, samples, numSamples, channels, channel, searchStart, output);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Catmull-Rom spline coefficients
|
|
607
|
+
float alpha = (targetTime - t1) / denominator;
|
|
608
|
+
float alpha2 = alpha * alpha;
|
|
609
|
+
float alpha3 = alpha2 * alpha;
|
|
610
|
+
|
|
611
|
+
output = 0.5f * ((2.0f * v1) +
|
|
612
|
+
(-v0 + v2) * alpha +
|
|
613
|
+
(2.0f * v0 - 5.0f * v1 + 4.0f * v2 - v3) * alpha2 +
|
|
614
|
+
(-v0 + 3.0f * v1 - 3.0f * v2 + v3) * alpha3);
|
|
615
|
+
|
|
616
|
+
searchStart = idx;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
void TimeAlignmentStage::interpolateSinc(
|
|
620
|
+
float targetTime,
|
|
621
|
+
const float *timestamps,
|
|
622
|
+
const float *samples,
|
|
623
|
+
size_t numSamples,
|
|
624
|
+
int channels,
|
|
625
|
+
int channel,
|
|
626
|
+
size_t &searchStart,
|
|
627
|
+
float &output)
|
|
628
|
+
{
|
|
629
|
+
// Windowed sinc interpolation (ideal lowpass filter)
|
|
630
|
+
// Window size: 8 samples (±4 from center)
|
|
631
|
+
constexpr int windowSize = 8;
|
|
632
|
+
|
|
633
|
+
size_t centerIdx = findBracketingInterval(targetTime, timestamps, numSamples, channels, searchStart);
|
|
634
|
+
|
|
635
|
+
float sum = 0.0f;
|
|
636
|
+
float weightSum = 0.0f;
|
|
637
|
+
|
|
638
|
+
for (int offset = -windowSize / 2; offset < windowSize / 2; ++offset)
|
|
639
|
+
{
|
|
640
|
+
int sampleIdx = static_cast<int>(centerIdx) + offset;
|
|
641
|
+
if (sampleIdx < 0 || sampleIdx >= static_cast<int>(numSamples))
|
|
642
|
+
continue;
|
|
643
|
+
|
|
644
|
+
float t = timestamps[sampleIdx * channels];
|
|
645
|
+
float v = samples[sampleIdx * channels + channel];
|
|
646
|
+
|
|
647
|
+
// Sinc function: sin(π*x) / (π*x)
|
|
648
|
+
float x = (targetTime - t) * m_estimatedSampleRate / 1000.0f; // Normalize by sample rate
|
|
649
|
+
float sinc = (std::abs(x) < 1e-6f) ? 1.0f : std::sin(M_PI * x) / (M_PI * x);
|
|
650
|
+
|
|
651
|
+
// Hamming window
|
|
652
|
+
float window = 0.54f - 0.46f * std::cos(2.0f * M_PI * (offset + windowSize / 2.0f) / windowSize);
|
|
653
|
+
|
|
654
|
+
float weight = sinc * window;
|
|
655
|
+
sum += v * weight;
|
|
656
|
+
weightSum += weight;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
output = (weightSum > 0.0f) ? (sum / weightSum) : 0.0f;
|
|
660
|
+
searchStart = centerIdx;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
size_t TimeAlignmentStage::findBracketingInterval(
|
|
664
|
+
float targetTime,
|
|
665
|
+
const float *timestamps,
|
|
666
|
+
size_t numSamples,
|
|
667
|
+
int channels,
|
|
668
|
+
size_t searchStart)
|
|
669
|
+
{
|
|
670
|
+
if (isDebugEnabled())
|
|
671
|
+
{
|
|
672
|
+
std::cout << "[TimeAlignment] findBracketingInterval: targetTime=" << targetTime
|
|
673
|
+
<< ", numSamples=" << numSamples
|
|
674
|
+
<< ", channels=" << channels
|
|
675
|
+
<< ", searchStart=" << searchStart << std::endl;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Two-pointer search: start from last known position
|
|
679
|
+
// This is O(1) amortized for monotonically increasing target times
|
|
680
|
+
|
|
681
|
+
// Clamp search start
|
|
682
|
+
if (searchStart >= numSamples - 1)
|
|
683
|
+
searchStart = 0;
|
|
684
|
+
|
|
685
|
+
// Forward search (most common case)
|
|
686
|
+
while (searchStart < numSamples - 1 && timestamps[(searchStart + 1) * channels] < targetTime)
|
|
687
|
+
{
|
|
688
|
+
searchStart++;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Backward search (rare, but handles non-monotonic targets)
|
|
692
|
+
while (searchStart > 0 && timestamps[searchStart * channels] > targetTime)
|
|
693
|
+
{
|
|
694
|
+
searchStart--;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return searchStart;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ========================================
|
|
701
|
+
// Serialization
|
|
702
|
+
// ========================================
|
|
703
|
+
|
|
704
|
+
Napi::Object TimeAlignmentStage::serializeState(Napi::Env env) const
|
|
705
|
+
{
|
|
706
|
+
Napi::Object state = Napi::Object::New(env);
|
|
707
|
+
state.Set("targetSampleRate", Napi::Number::New(env, m_targetSampleRate));
|
|
708
|
+
state.Set("interpMethod", Napi::Number::New(env, static_cast<int>(m_interpMethod)));
|
|
709
|
+
state.Set("gapPolicy", Napi::Number::New(env, static_cast<int>(m_gapPolicy)));
|
|
710
|
+
state.Set("gapThreshold", Napi::Number::New(env, m_gapThreshold));
|
|
711
|
+
state.Set("driftComp", Napi::Number::New(env, static_cast<int>(m_driftComp)));
|
|
712
|
+
return state;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
void TimeAlignmentStage::deserializeState(const Napi::Object &state)
|
|
716
|
+
{
|
|
717
|
+
m_targetSampleRate = state.Get("targetSampleRate").As<Napi::Number>().FloatValue();
|
|
718
|
+
m_interpMethod = static_cast<InterpolationMethod>(state.Get("interpMethod").As<Napi::Number>().Int32Value());
|
|
719
|
+
m_gapPolicy = static_cast<GapPolicy>(state.Get("gapPolicy").As<Napi::Number>().Int32Value());
|
|
720
|
+
m_gapThreshold = state.Get("gapThreshold").As<Napi::Number>().FloatValue();
|
|
721
|
+
m_driftComp = static_cast<DriftCompensation>(state.Get("driftComp").As<Napi::Number>().Int32Value());
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
void TimeAlignmentStage::serializeToon(toon::Serializer &serializer) const
|
|
725
|
+
{
|
|
726
|
+
serializer.writeFloat(m_targetSampleRate);
|
|
727
|
+
serializer.writeInt32(static_cast<int>(m_interpMethod));
|
|
728
|
+
serializer.writeInt32(static_cast<int>(m_gapPolicy));
|
|
729
|
+
serializer.writeFloat(m_gapThreshold);
|
|
730
|
+
serializer.writeInt32(static_cast<int>(m_driftComp));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
void TimeAlignmentStage::deserializeToon(toon::Deserializer &deserializer)
|
|
734
|
+
{
|
|
735
|
+
m_targetSampleRate = deserializer.readFloat();
|
|
736
|
+
m_interpMethod = static_cast<InterpolationMethod>(deserializer.readInt32());
|
|
737
|
+
m_gapPolicy = static_cast<GapPolicy>(deserializer.readInt32());
|
|
738
|
+
m_gapThreshold = deserializer.readFloat();
|
|
739
|
+
m_driftComp = static_cast<DriftCompensation>(deserializer.readInt32());
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
} // namespace adapters
|
|
743
|
+
} // namespace dsp
|