dspx 0.1.1-alpha.0

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.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. package/tsconfig.json +15 -0
@@ -0,0 +1,140 @@
1
+ #pragma once
2
+ #include <deque>
3
+ #include <utility>
4
+ #include <vector>
5
+ #include <cstdint>
6
+ #include <stdexcept>
7
+
8
+ namespace dsp::utils
9
+ {
10
+ /**
11
+ * @brief A time-series buffer that stores timestamped samples.
12
+ *
13
+ * This buffer replaces CircularBuffer for time-aware processing.
14
+ * It stores pairs of (timestamp, value) and supports both:
15
+ * - Sample-based windows (fixed number of samples)
16
+ * - Time-based windows (fixed duration in milliseconds)
17
+ *
18
+ * Uses std::deque for efficient front/back operations.
19
+ *
20
+ * @tparam T The numeric type (e.g., float, double).
21
+ */
22
+ template <typename T>
23
+ class TimeSeriesBuffer
24
+ {
25
+ public:
26
+ using TimestampType = uint64_t;
27
+ using Sample = std::pair<TimestampType, T>;
28
+
29
+ /**
30
+ * @brief Constructs a time-series buffer.
31
+ * @param max_samples Maximum number of samples (for sample-based mode, 0 = unlimited)
32
+ * @param window_duration_ms Maximum time window in milliseconds (for time-based mode, 0 = disabled)
33
+ */
34
+ explicit TimeSeriesBuffer(size_t max_samples = 0, uint64_t window_duration_ms = 0);
35
+
36
+ /**
37
+ * @brief Adds a new timestamped sample.
38
+ *
39
+ * Automatically removes old samples based on window constraints:
40
+ * - If max_samples > 0: removes oldest samples beyond the limit
41
+ * - If window_duration_ms > 0: removes samples older than (newest_timestamp - window_duration_ms)
42
+ *
43
+ * @param timestamp The timestamp in milliseconds (or sample index)
44
+ * @param value The sample value
45
+ */
46
+ void push(TimestampType timestamp, T value);
47
+
48
+ /**
49
+ * @brief Removes samples older than the specified timestamp.
50
+ * @param cutoff_timestamp Samples with timestamp < cutoff_timestamp will be removed
51
+ * @return size_t Number of samples removed
52
+ */
53
+ size_t removeOlderThan(TimestampType cutoff_timestamp);
54
+
55
+ /**
56
+ * @brief Gets the oldest sample without removing it.
57
+ * @return const Sample& Reference to the oldest (front) sample
58
+ * @throws std::out_of_range if buffer is empty
59
+ */
60
+ const Sample &front() const;
61
+
62
+ /**
63
+ * @brief Gets the newest sample without removing it.
64
+ * @return const Sample& Reference to the newest (back) sample
65
+ * @throws std::out_of_range if buffer is empty
66
+ */
67
+ const Sample &back() const;
68
+
69
+ /**
70
+ * @brief Removes the oldest sample.
71
+ * @throws std::out_of_range if buffer is empty
72
+ */
73
+ void popFront();
74
+
75
+ /**
76
+ * @brief Gets the current number of samples in the buffer.
77
+ * @return size_t The number of samples
78
+ */
79
+ size_t size() const noexcept;
80
+
81
+ /**
82
+ * @brief Checks if the buffer is empty.
83
+ * @return bool True if empty
84
+ */
85
+ bool empty() const noexcept;
86
+
87
+ /**
88
+ * @brief Clears all samples from the buffer.
89
+ */
90
+ void clear() noexcept;
91
+
92
+ /**
93
+ * @brief Exports all samples as a vector.
94
+ * @return std::vector<Sample> All timestamp-value pairs
95
+ */
96
+ std::vector<Sample> toVector() const;
97
+
98
+ /**
99
+ * @brief Restores buffer from a vector of samples.
100
+ * @param samples Vector of timestamp-value pairs to restore
101
+ */
102
+ void fromVector(const std::vector<Sample> &samples);
103
+
104
+ /**
105
+ * @brief Gets the time span of the buffer (newest - oldest timestamp).
106
+ * @return uint64_t Time span in milliseconds (0 if empty or single sample)
107
+ */
108
+ uint64_t getTimeSpan() const noexcept;
109
+
110
+ /**
111
+ * @brief Provides iterator access to samples (const).
112
+ */
113
+ typename std::deque<Sample>::const_iterator begin() const noexcept;
114
+ typename std::deque<Sample>::const_iterator end() const noexcept;
115
+
116
+ /**
117
+ * @brief Gets the maximum sample count constraint.
118
+ * @return size_t Maximum samples (0 = unlimited)
119
+ */
120
+ size_t getMaxSamples() const noexcept;
121
+
122
+ /**
123
+ * @brief Gets the time window duration constraint.
124
+ * @return uint64_t Window duration in milliseconds (0 = disabled)
125
+ */
126
+ uint64_t getWindowDuration() const noexcept;
127
+
128
+ private:
129
+ std::deque<Sample> m_samples;
130
+ size_t m_max_samples;
131
+ uint64_t m_window_duration_ms;
132
+
133
+ /**
134
+ * @brief Enforces window constraints by removing old samples.
135
+ * Called automatically after push().
136
+ */
137
+ void enforceWindowConstraints();
138
+ };
139
+
140
+ } // namespace dsp::utils
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Circular buffer for efficient log pooling
3
+ * Avoids array reallocations and maintains fixed memory footprint
4
+ */
5
+
6
+ import type { LogEntry } from "./types.js";
7
+
8
+ export class CircularLogBuffer {
9
+ private buffer: LogEntry[];
10
+ private writeIndex = 0;
11
+ private count = 0;
12
+ private readonly capacity: number;
13
+
14
+ /**
15
+ * Create a circular log buffer
16
+ * @param capacity - Maximum number of log entries to store (default: 32)
17
+ */
18
+ constructor(capacity: number = 32) {
19
+ this.capacity = capacity;
20
+ this.buffer = new Array(capacity);
21
+ }
22
+
23
+ /**
24
+ * Add a log entry to the buffer
25
+ * If buffer is full, oldest entry is overwritten
26
+ */
27
+ push(entry: LogEntry): void {
28
+ this.buffer[this.writeIndex] = entry;
29
+ this.writeIndex = (this.writeIndex + 1) % this.capacity;
30
+ if (this.count < this.capacity) {
31
+ this.count++;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get all log entries in chronological order and clear the buffer
37
+ * @returns Array of log entries (oldest to newest)
38
+ */
39
+ flush(): LogEntry[] {
40
+ if (this.count === 0) {
41
+ return [];
42
+ }
43
+
44
+ const result: LogEntry[] = new Array(this.count);
45
+
46
+ if (this.count < this.capacity) {
47
+ // Buffer not full - entries are at start
48
+ for (let i = 0; i < this.count; i++) {
49
+ result[i] = this.buffer[i];
50
+ }
51
+ } else {
52
+ // Buffer full - need to unwrap circular order
53
+ const oldestIndex = this.writeIndex; // Points to oldest (next to be overwritten)
54
+ for (let i = 0; i < this.capacity; i++) {
55
+ result[i] = this.buffer[(oldestIndex + i) % this.capacity];
56
+ }
57
+ }
58
+
59
+ // Reset buffer state (reuse memory)
60
+ this.count = 0;
61
+ this.writeIndex = 0;
62
+
63
+ return result;
64
+ }
65
+
66
+ /**
67
+ * Check if buffer has any entries
68
+ */
69
+ hasEntries(): boolean {
70
+ return this.count > 0;
71
+ }
72
+
73
+ /**
74
+ * Get current number of entries in buffer
75
+ */
76
+ size(): number {
77
+ return this.count;
78
+ }
79
+
80
+ /**
81
+ * Clear all entries without allocating new memory
82
+ */
83
+ clear(): void {
84
+ this.count = 0;
85
+ this.writeIndex = 0;
86
+ }
87
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Drift Detection for Time-Series Processing
3
+ *
4
+ * Monitors sample timing and detects drift/irregularities in real-time data streams.
5
+ * Useful for debugging hardware issues, network problems, and ensuring data quality.
6
+ */
7
+
8
+ export interface DriftStatistics {
9
+ /** Current time delta between samples (ms) */
10
+ deltaMs: number;
11
+ /** Expected time delta based on sample rate (ms) */
12
+ expectedMs: number;
13
+ /** Absolute drift from expected (ms) */
14
+ absoluteDrift: number;
15
+ /** Relative drift as percentage (%) */
16
+ relativeDrift: number;
17
+ /** Sample index where drift was detected */
18
+ sampleIndex: number;
19
+ /** Timestamp of current sample */
20
+ currentTimestamp: number;
21
+ /** Timestamp of previous sample */
22
+ previousTimestamp: number;
23
+ }
24
+
25
+ export interface DriftDetectorOptions {
26
+ /** Expected sample rate in Hz (e.g., 1000 for 1kHz) */
27
+ expectedSampleRate?: number;
28
+ /** Drift threshold percentage (0-100, default: 10%) */
29
+ driftThreshold?: number;
30
+ /** Callback when drift is detected */
31
+ onDriftDetected?: (stats: DriftStatistics) => void;
32
+ /** Callback for all timing measurements (high volume!) */
33
+ onTimingMeasured?: (stats: DriftStatistics) => void;
34
+ }
35
+
36
+ export interface TimingMetrics {
37
+ /** Total samples processed */
38
+ samplesProcessed: number;
39
+ /** Number of drift events detected */
40
+ driftEventsCount: number;
41
+ /** Minimum delta observed (ms) */
42
+ minDelta: number;
43
+ /** Maximum delta observed (ms) */
44
+ maxDelta: number;
45
+ /** Average delta (ms) */
46
+ averageDelta: number;
47
+ /** Standard deviation of deltas (ms) */
48
+ stdDevDelta: number;
49
+ /** Largest drift detected (ms) */
50
+ maxDriftObserved: number;
51
+ }
52
+
53
+ /**
54
+ * DriftDetector monitors timing between samples and detects anomalies
55
+ */
56
+ export class DriftDetector {
57
+ private options: Required<DriftDetectorOptions>;
58
+ private previousTimestamp: number | null = null;
59
+ private sampleIndex: number = 0;
60
+
61
+ // Metrics tracking
62
+ private samplesProcessed: number = 0;
63
+ private driftEventsCount: number = 0;
64
+ private minDelta: number = Infinity;
65
+ private maxDelta: number = -Infinity;
66
+ private deltasSum: number = 0;
67
+ private deltasSumSquared: number = 0;
68
+ private maxDriftObserved: number = 0;
69
+
70
+ constructor(options: DriftDetectorOptions = {}) {
71
+ this.options = {
72
+ expectedSampleRate: options.expectedSampleRate ?? 1000,
73
+ driftThreshold: options.driftThreshold ?? 10,
74
+ onDriftDetected: options.onDriftDetected ?? (() => {}),
75
+ onTimingMeasured: options.onTimingMeasured ?? (() => {}),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Process a single timestamp and detect drift
81
+ */
82
+ processSample(timestamp: number): void {
83
+ if (this.previousTimestamp !== null) {
84
+ const deltaMs = timestamp - this.previousTimestamp;
85
+ const expectedMs = 1000 / this.options.expectedSampleRate;
86
+ const absoluteDrift = Math.abs(deltaMs - expectedMs);
87
+ const relativeDrift = (absoluteDrift / expectedMs) * 100;
88
+
89
+ const stats: DriftStatistics = {
90
+ deltaMs,
91
+ expectedMs,
92
+ absoluteDrift,
93
+ relativeDrift,
94
+ sampleIndex: this.sampleIndex,
95
+ currentTimestamp: timestamp,
96
+ previousTimestamp: this.previousTimestamp,
97
+ };
98
+
99
+ // Update metrics
100
+ this.updateMetrics(deltaMs, absoluteDrift);
101
+
102
+ // Call timing callback (every sample)
103
+ this.options.onTimingMeasured(stats);
104
+
105
+ // Detect drift
106
+ if (relativeDrift > this.options.driftThreshold) {
107
+ this.driftEventsCount++;
108
+ this.options.onDriftDetected(stats);
109
+ }
110
+ }
111
+
112
+ this.previousTimestamp = timestamp;
113
+ this.sampleIndex++;
114
+ this.samplesProcessed++;
115
+ }
116
+
117
+ /**
118
+ * Process a batch of timestamps
119
+ */
120
+ processBatch(timestamps: Float32Array): void {
121
+ for (let i = 0; i < timestamps.length; i++) {
122
+ this.processSample(timestamps[i]);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get current timing metrics
128
+ */
129
+ getMetrics(): TimingMetrics {
130
+ const avgDelta =
131
+ this.samplesProcessed > 1
132
+ ? this.deltasSum / (this.samplesProcessed - 1)
133
+ : 0;
134
+
135
+ const variance =
136
+ this.samplesProcessed > 1
137
+ ? this.deltasSumSquared / (this.samplesProcessed - 1) -
138
+ avgDelta * avgDelta
139
+ : 0;
140
+
141
+ const stdDev = Math.sqrt(Math.max(0, variance));
142
+
143
+ return {
144
+ samplesProcessed: this.samplesProcessed,
145
+ driftEventsCount: this.driftEventsCount,
146
+ minDelta: this.minDelta === Infinity ? 0 : this.minDelta,
147
+ maxDelta: this.maxDelta === -Infinity ? 0 : this.maxDelta,
148
+ averageDelta: avgDelta,
149
+ stdDevDelta: stdDev,
150
+ maxDriftObserved: this.maxDriftObserved,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Reset all metrics and state
156
+ */
157
+ reset(): void {
158
+ this.previousTimestamp = null;
159
+ this.sampleIndex = 0;
160
+ this.samplesProcessed = 0;
161
+ this.driftEventsCount = 0;
162
+ this.minDelta = Infinity;
163
+ this.maxDelta = -Infinity;
164
+ this.deltasSum = 0;
165
+ this.deltasSumSquared = 0;
166
+ this.maxDriftObserved = 0;
167
+ }
168
+
169
+ /**
170
+ * Get the expected sample rate configured for this detector
171
+ */
172
+ getExpectedSampleRate(): number {
173
+ return this.options.expectedSampleRate;
174
+ }
175
+
176
+ /**
177
+ * Update internal metrics
178
+ */
179
+ private updateMetrics(deltaMs: number, absoluteDrift: number): void {
180
+ this.minDelta = Math.min(this.minDelta, deltaMs);
181
+ this.maxDelta = Math.max(this.maxDelta, deltaMs);
182
+ this.deltasSum += deltaMs;
183
+ this.deltasSumSquared += deltaMs * deltaMs;
184
+ this.maxDriftObserved = Math.max(this.maxDriftObserved, absoluteDrift);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Utility: Detect gaps in timestamps (missing samples)
190
+ */
191
+ export interface GapDetection {
192
+ /** Index where gap starts */
193
+ startIndex: number;
194
+ /** Index where gap ends */
195
+ endIndex: number;
196
+ /** Gap duration in ms */
197
+ durationMs: number;
198
+ /** Expected number of samples in gap */
199
+ expectedSamples: number;
200
+ /** Timestamp before gap */
201
+ timestampBefore: number;
202
+ /** Timestamp after gap */
203
+ timestampAfter: number;
204
+ }
205
+
206
+ export function detectGaps(
207
+ timestamps: Float32Array,
208
+ expectedSampleRate: number,
209
+ gapThreshold: number = 2.0 // Multiplier of expected interval
210
+ ): GapDetection[] {
211
+ const gaps: GapDetection[] = [];
212
+ const expectedInterval = 1000 / expectedSampleRate;
213
+ const gapMinDuration = expectedInterval * gapThreshold;
214
+
215
+ for (let i = 1; i < timestamps.length; i++) {
216
+ const delta = timestamps[i] - timestamps[i - 1];
217
+ if (delta > gapMinDuration) {
218
+ const expectedSamples = Math.floor(delta / expectedInterval) - 1;
219
+ gaps.push({
220
+ startIndex: i - 1,
221
+ endIndex: i,
222
+ durationMs: delta,
223
+ expectedSamples,
224
+ timestampBefore: timestamps[i - 1],
225
+ timestampAfter: timestamps[i],
226
+ });
227
+ }
228
+ }
229
+
230
+ return gaps;
231
+ }
232
+
233
+ /**
234
+ * Utility: Validate timestamp monotonicity
235
+ */
236
+ export interface MonotonicityViolation {
237
+ index: number;
238
+ currentTimestamp: number;
239
+ previousTimestamp: number;
240
+ violation: "backwards" | "duplicate";
241
+ }
242
+
243
+ export function validateMonotonicity(
244
+ timestamps: Float32Array
245
+ ): MonotonicityViolation[] {
246
+ const violations: MonotonicityViolation[] = [];
247
+
248
+ for (let i = 1; i < timestamps.length; i++) {
249
+ const current = timestamps[i];
250
+ const previous = timestamps[i - 1];
251
+
252
+ if (current < previous) {
253
+ violations.push({
254
+ index: i,
255
+ currentTimestamp: current,
256
+ previousTimestamp: previous,
257
+ violation: "backwards",
258
+ });
259
+ } else if (current === previous) {
260
+ violations.push({
261
+ index: i,
262
+ currentTimestamp: current,
263
+ previousTimestamp: previous,
264
+ violation: "duplicate",
265
+ });
266
+ }
267
+ }
268
+
269
+ return violations;
270
+ }
271
+
272
+ /**
273
+ * Utility: Calculate sample rate from timestamps
274
+ */
275
+ export interface SampleRateEstimate {
276
+ /** Estimated sample rate in Hz */
277
+ estimatedRate: number;
278
+ /** Average interval in ms */
279
+ averageInterval: number;
280
+ /** Standard deviation of intervals */
281
+ stdDevInterval: number;
282
+ /** Coefficient of variation (stdDev / mean) */
283
+ coefficientOfVariation: number;
284
+ /** Regularity assessment */
285
+ regularity: "excellent" | "good" | "fair" | "poor" | "irregular";
286
+ }
287
+
288
+ export function estimateSampleRate(
289
+ timestamps: Float32Array
290
+ ): SampleRateEstimate {
291
+ if (timestamps.length < 2) {
292
+ return {
293
+ estimatedRate: 0,
294
+ averageInterval: 0,
295
+ stdDevInterval: 0,
296
+ coefficientOfVariation: 0,
297
+ regularity: "irregular",
298
+ };
299
+ }
300
+
301
+ // Calculate intervals
302
+ let sumIntervals = 0;
303
+ let sumSquaredIntervals = 0;
304
+ for (let i = 1; i < timestamps.length; i++) {
305
+ const interval = timestamps[i] - timestamps[i - 1];
306
+ sumIntervals += interval;
307
+ sumSquaredIntervals += interval * interval;
308
+ }
309
+
310
+ const n = timestamps.length - 1;
311
+ const avgInterval = sumIntervals / n;
312
+ const variance = sumSquaredIntervals / n - avgInterval * avgInterval;
313
+ const stdDev = Math.sqrt(Math.max(0, variance));
314
+ const cv = avgInterval > 0 ? stdDev / avgInterval : Infinity;
315
+
316
+ // Assess regularity
317
+ let regularity: "excellent" | "good" | "fair" | "poor" | "irregular";
318
+ if (cv < 0.01) regularity = "excellent"; // <1% variation
319
+ else if (cv < 0.05) regularity = "good"; // <5% variation
320
+ else if (cv < 0.15) regularity = "fair"; // <15% variation
321
+ else if (cv < 0.3) regularity = "poor"; // <30% variation
322
+ else regularity = "irregular"; // >30% variation
323
+
324
+ return {
325
+ estimatedRate: 1000 / avgInterval,
326
+ averageInterval: avgInterval,
327
+ stdDevInterval: stdDev,
328
+ coefficientOfVariation: cv,
329
+ regularity,
330
+ };
331
+ }