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.
@@ -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