@codexo/exojs 0.6.12 → 0.7.11

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 (186) hide show
  1. package/CHANGELOG.md +1316 -0
  2. package/dist/esm/audio/AbstractMedia.d.ts +18 -0
  3. package/dist/esm/audio/AbstractMedia.js +66 -0
  4. package/dist/esm/audio/AbstractMedia.js.map +1 -1
  5. package/dist/esm/audio/AudioAnalyser.d.ts +62 -23
  6. package/dist/esm/audio/AudioAnalyser.js +261 -57
  7. package/dist/esm/audio/AudioAnalyser.js.map +1 -1
  8. package/dist/esm/audio/AudioBus.d.ts +45 -0
  9. package/dist/esm/audio/AudioBus.js +219 -0
  10. package/dist/esm/audio/AudioBus.js.map +1 -0
  11. package/dist/esm/audio/AudioFilter.d.ts +9 -0
  12. package/dist/esm/audio/AudioFilter.js +7 -0
  13. package/dist/esm/audio/AudioFilter.js.map +1 -0
  14. package/dist/esm/audio/AudioListener.d.ts +20 -0
  15. package/dist/esm/audio/AudioListener.js +86 -0
  16. package/dist/esm/audio/AudioListener.js.map +1 -0
  17. package/dist/esm/audio/AudioManager.d.ts +31 -0
  18. package/dist/esm/audio/AudioManager.js +102 -0
  19. package/dist/esm/audio/AudioManager.js.map +1 -0
  20. package/dist/esm/audio/BeatDetector.d.ts +121 -0
  21. package/dist/esm/audio/BeatDetector.js +936 -0
  22. package/dist/esm/audio/BeatDetector.js.map +1 -0
  23. package/dist/esm/audio/Envelope.d.ts +44 -0
  24. package/dist/esm/audio/Envelope.js +60 -0
  25. package/dist/esm/audio/Envelope.js.map +1 -0
  26. package/dist/esm/audio/Music.d.ts +8 -0
  27. package/dist/esm/audio/Music.js +33 -4
  28. package/dist/esm/audio/Music.js.map +1 -1
  29. package/dist/esm/audio/OscillatorSound.d.ts +98 -0
  30. package/dist/esm/audio/OscillatorSound.js +342 -0
  31. package/dist/esm/audio/OscillatorSound.js.map +1 -0
  32. package/dist/esm/audio/Sound.d.ts +94 -9
  33. package/dist/esm/audio/Sound.js +283 -117
  34. package/dist/esm/audio/Sound.js.map +1 -1
  35. package/dist/esm/audio/crossFade.d.ts +19 -0
  36. package/dist/esm/audio/crossFade.js +26 -0
  37. package/dist/esm/audio/crossFade.js.map +1 -0
  38. package/dist/esm/audio/dsp/fft.d.ts +22 -0
  39. package/dist/esm/audio/dsp/mel.d.ts +43 -0
  40. package/dist/esm/audio/dsp/tempogram.d.ts +51 -0
  41. package/dist/esm/audio/filters/ChorusFilter.d.ts +47 -0
  42. package/dist/esm/audio/filters/ChorusFilter.js +139 -0
  43. package/dist/esm/audio/filters/ChorusFilter.js.map +1 -0
  44. package/dist/esm/audio/filters/CompressorFilter.d.ts +31 -0
  45. package/dist/esm/audio/filters/CompressorFilter.js +97 -0
  46. package/dist/esm/audio/filters/CompressorFilter.js.map +1 -0
  47. package/dist/esm/audio/filters/DelayFilter.d.ts +23 -0
  48. package/dist/esm/audio/filters/DelayFilter.js +100 -0
  49. package/dist/esm/audio/filters/DelayFilter.js.map +1 -0
  50. package/dist/esm/audio/filters/DuckingFilter.d.ts +31 -0
  51. package/dist/esm/audio/filters/DuckingFilter.js +152 -0
  52. package/dist/esm/audio/filters/DuckingFilter.js.map +1 -0
  53. package/dist/esm/audio/filters/EqualizerFilter.d.ts +29 -0
  54. package/dist/esm/audio/filters/EqualizerFilter.js +94 -0
  55. package/dist/esm/audio/filters/EqualizerFilter.js.map +1 -0
  56. package/dist/esm/audio/filters/GranularFilter.d.ts +56 -0
  57. package/dist/esm/audio/filters/GranularFilter.js +170 -0
  58. package/dist/esm/audio/filters/GranularFilter.js.map +1 -0
  59. package/dist/esm/audio/filters/HighpassFilter.d.ts +19 -0
  60. package/dist/esm/audio/filters/HighpassFilter.js +62 -0
  61. package/dist/esm/audio/filters/HighpassFilter.js.map +1 -0
  62. package/dist/esm/audio/filters/LowpassFilter.d.ts +19 -0
  63. package/dist/esm/audio/filters/LowpassFilter.js +62 -0
  64. package/dist/esm/audio/filters/LowpassFilter.js.map +1 -0
  65. package/dist/esm/audio/filters/PitchShiftFilter.d.ts +42 -0
  66. package/dist/esm/audio/filters/PitchShiftFilter.js +130 -0
  67. package/dist/esm/audio/filters/PitchShiftFilter.js.map +1 -0
  68. package/dist/esm/audio/filters/ReverbFilter.d.ts +24 -0
  69. package/dist/esm/audio/filters/ReverbFilter.js +107 -0
  70. package/dist/esm/audio/filters/ReverbFilter.js.map +1 -0
  71. package/dist/esm/audio/filters/VocoderFilter.d.ts +38 -0
  72. package/dist/esm/audio/filters/VocoderFilter.js +163 -0
  73. package/dist/esm/audio/filters/VocoderFilter.js.map +1 -0
  74. package/dist/esm/audio/filters/WorkletFilter.d.ts +46 -0
  75. package/dist/esm/audio/filters/WorkletFilter.js +101 -0
  76. package/dist/esm/audio/filters/WorkletFilter.js.map +1 -0
  77. package/dist/esm/audio/filters/index.d.ts +12 -0
  78. package/dist/esm/audio/index.d.ts +15 -1
  79. package/dist/esm/audio/worklet/registerWorklet.d.ts +10 -0
  80. package/dist/esm/audio/worklet/registerWorklet.js +44 -0
  81. package/dist/esm/audio/worklet/registerWorklet.js.map +1 -0
  82. package/dist/esm/core/Application.d.ts +19 -0
  83. package/dist/esm/core/Application.js +76 -2
  84. package/dist/esm/core/Application.js.map +1 -1
  85. package/dist/esm/core/SceneNode.d.ts +9 -1
  86. package/dist/esm/core/SceneNode.js +44 -6
  87. package/dist/esm/core/SceneNode.js.map +1 -1
  88. package/dist/esm/core/Time.js +1 -1
  89. package/dist/esm/core/index.d.ts +0 -1
  90. package/dist/esm/debug/BoundingBoxesLayer.d.ts +18 -0
  91. package/dist/esm/debug/BoundingBoxesLayer.js +128 -0
  92. package/dist/esm/debug/BoundingBoxesLayer.js.map +1 -0
  93. package/dist/esm/debug/DebugLayer.d.ts +29 -0
  94. package/dist/esm/debug/DebugLayer.js +26 -0
  95. package/dist/esm/debug/DebugLayer.js.map +1 -0
  96. package/dist/esm/debug/DebugOverlay.d.ts +48 -0
  97. package/dist/esm/debug/DebugOverlay.js +117 -0
  98. package/dist/esm/debug/DebugOverlay.js.map +1 -0
  99. package/dist/esm/debug/HitTestLayer.d.ts +23 -0
  100. package/dist/esm/debug/HitTestLayer.js +109 -0
  101. package/dist/esm/debug/HitTestLayer.js.map +1 -0
  102. package/dist/esm/debug/PerformanceLayer.d.ts +21 -0
  103. package/dist/esm/debug/PerformanceLayer.js +175 -0
  104. package/dist/esm/debug/PerformanceLayer.js.map +1 -0
  105. package/dist/esm/debug/PointerStackLayer.d.ts +23 -0
  106. package/dist/esm/debug/PointerStackLayer.js +152 -0
  107. package/dist/esm/debug/PointerStackLayer.js.map +1 -0
  108. package/dist/esm/debug/index.d.ts +6 -0
  109. package/dist/esm/debug/index.js +7 -0
  110. package/dist/esm/debug/index.js.map +1 -0
  111. package/dist/esm/index.js +28 -2
  112. package/dist/esm/index.js.map +1 -1
  113. package/dist/esm/input/InputManager.d.ts +10 -0
  114. package/dist/esm/input/InputManager.js +35 -5
  115. package/dist/esm/input/InputManager.js.map +1 -1
  116. package/dist/esm/input/InteractionEvent.d.ts +18 -0
  117. package/dist/esm/input/InteractionEvent.js +29 -0
  118. package/dist/esm/input/InteractionEvent.js.map +1 -0
  119. package/dist/esm/input/InteractionManager.d.ts +134 -0
  120. package/dist/esm/input/InteractionManager.js +546 -0
  121. package/dist/esm/input/InteractionManager.js.map +1 -0
  122. package/dist/esm/input/index.d.ts +2 -0
  123. package/dist/esm/input/interaction-hooks.d.ts +34 -0
  124. package/dist/esm/input/interaction-hooks.js +35 -0
  125. package/dist/esm/input/interaction-hooks.js.map +1 -0
  126. package/dist/esm/math/Circle.d.ts +12 -2
  127. package/dist/esm/math/Circle.js +82 -14
  128. package/dist/esm/math/Circle.js.map +1 -1
  129. package/dist/esm/math/Interval.js +1 -1
  130. package/dist/esm/math/ObservableVector.d.ts +2 -2
  131. package/dist/esm/math/ObservableVector.js +4 -2
  132. package/dist/esm/math/ObservableVector.js.map +1 -1
  133. package/dist/esm/math/Polygon.d.ts +15 -1
  134. package/dist/esm/math/Polygon.js +58 -6
  135. package/dist/esm/math/Polygon.js.map +1 -1
  136. package/dist/esm/math/Quadtree.d.ts +47 -0
  137. package/dist/esm/math/Quadtree.js +168 -0
  138. package/dist/esm/math/Quadtree.js.map +1 -0
  139. package/dist/esm/math/Random.js +1 -1
  140. package/dist/esm/math/Size.js +1 -1
  141. package/dist/esm/math/Vector.js +1 -1
  142. package/dist/esm/math/collision-detection.js +4 -1
  143. package/dist/esm/math/collision-detection.js.map +1 -1
  144. package/dist/esm/math/index.d.ts +1 -0
  145. package/dist/esm/particles/ParticleSystem.js +1 -0
  146. package/dist/esm/particles/ParticleSystem.js.map +1 -1
  147. package/dist/esm/particles/affectors/TorqueAffector.js +1 -1
  148. package/dist/esm/rendering/Container.d.ts +1 -0
  149. package/dist/esm/rendering/Container.js +19 -0
  150. package/dist/esm/rendering/Container.js.map +1 -1
  151. package/dist/esm/rendering/RenderNode.d.ts +27 -0
  152. package/dist/esm/rendering/RenderNode.js +44 -0
  153. package/dist/esm/rendering/RenderNode.js.map +1 -1
  154. package/dist/esm/rendering/View.d.ts +6 -4
  155. package/dist/esm/rendering/View.js +12 -2
  156. package/dist/esm/rendering/View.js.map +1 -1
  157. package/dist/esm/rendering/filters/WebGl2ShaderFilter.d.ts +109 -0
  158. package/dist/esm/rendering/filters/WebGl2ShaderFilter.js +268 -0
  159. package/dist/esm/rendering/filters/WebGl2ShaderFilter.js.map +1 -0
  160. package/dist/esm/rendering/filters/WebGpuShaderFilter.d.ts +111 -0
  161. package/dist/esm/rendering/filters/WebGpuShaderFilter.js +397 -0
  162. package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -0
  163. package/dist/esm/rendering/index.d.ts +3 -0
  164. package/dist/esm/rendering/mesh/Mesh.js +1 -0
  165. package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
  166. package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.d.ts +34 -0
  167. package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.js +60 -0
  168. package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.js.map +1 -0
  169. package/dist/esm/rendering/sprite/Sprite.d.ts +6 -1
  170. package/dist/esm/rendering/sprite/Sprite.js +41 -19
  171. package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
  172. package/dist/esm/rendering/video/Video.d.ts +4 -0
  173. package/dist/esm/rendering/video/Video.js +32 -4
  174. package/dist/esm/rendering/video/Video.js.map +1 -1
  175. package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +4 -4
  176. package/dist/esm/rendering/webgl2/WebGl2Backend.js +7 -16
  177. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
  178. package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +10 -8
  179. package/dist/esm/rendering/webgpu/WebGpuBackend.js +30 -40
  180. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  181. package/dist/exo.esm.js +7764 -2453
  182. package/dist/exo.esm.js.map +1 -1
  183. package/package.json +14 -2
  184. package/dist/esm/core/Quadtree.d.ts +0 -20
  185. package/dist/esm/core/Quadtree.js +0 -86
  186. package/dist/esm/core/Quadtree.js.map +0 -1
@@ -0,0 +1,936 @@
1
+ import { Signal } from '../core/Signal.js';
2
+ import { isAudioContextReady, getAudioContext, onAudioContextReady } from './audio-context.js';
3
+ import { registerWorkletProcessor } from './worklet/registerWorklet.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Worklet source (self-contained JavaScript, no imports)
7
+ // ---------------------------------------------------------------------------
8
+ const workletName = 'exojs-beat-detector';
9
+ const beatDetectorWorkletSource = `
10
+ // ---- Hann window + FFT (radix-2 Cooley-Tukey) ----
11
+ function applyHannWindow(real, imag) {
12
+ var n = real.length;
13
+ for (var i = 0; i < n; i++) {
14
+ var w = 0.5 * (1 - Math.cos(2 * Math.PI * i / (n - 1)));
15
+ real[i] *= w;
16
+ imag[i] = 0;
17
+ }
18
+ }
19
+
20
+ function bitReversePermute(real, imag) {
21
+ var n = real.length;
22
+ var j = 0;
23
+ for (var i = 1; i < n; i++) {
24
+ var bit = n >> 1;
25
+ for (; j & bit; bit >>= 1) { j ^= bit; }
26
+ j ^= bit;
27
+ if (i < j) {
28
+ var t = real[i]; real[i] = real[j]; real[j] = t;
29
+ t = imag[i]; imag[i] = imag[j]; imag[j] = t;
30
+ }
31
+ }
32
+ }
33
+
34
+ function fftInPlace(real, imag) {
35
+ applyHannWindow(real, imag);
36
+ bitReversePermute(real, imag);
37
+ var n = real.length;
38
+ for (var len = 2; len <= n; len <<= 1) {
39
+ var halfLen = len >> 1;
40
+ var step = -2 * Math.PI / len;
41
+ for (var i = 0; i < n; i += len) {
42
+ for (var k = 0; k < halfLen; k++) {
43
+ var angle = step * k;
44
+ var cos = Math.cos(angle);
45
+ var sin = Math.sin(angle);
46
+ var re = real[i+k+halfLen]*cos - imag[i+k+halfLen]*sin;
47
+ var im = real[i+k+halfLen]*sin + imag[i+k+halfLen]*cos;
48
+ real[i+k+halfLen] = real[i+k] - re;
49
+ imag[i+k+halfLen] = imag[i+k] - im;
50
+ real[i+k] += re;
51
+ imag[i+k] += im;
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // ---- Mel filterbank ----
58
+ function buildMelFilterbank(numBands, fMin, fMax, fftSize, sampleRate) {
59
+ var numBins = fftSize >> 1;
60
+ var nyquist = sampleRate / 2;
61
+ var melMin = 2595 * Math.log10(1 + fMin / 700);
62
+ var melMax = 2595 * Math.log10(1 + fMax / 700);
63
+ var melPoints = new Float32Array(numBands + 2);
64
+ for (var i = 0; i < numBands + 2; i++) {
65
+ melPoints[i] = melMin + (melMax - melMin) * i / (numBands + 1);
66
+ }
67
+ var binPoints = new Float32Array(numBands + 2);
68
+ for (var i = 0; i < numBands + 2; i++) {
69
+ var hz = 700 * (Math.pow(10, melPoints[i] / 2595) - 1);
70
+ binPoints[i] = Math.round(hz / nyquist * (numBins - 1));
71
+ }
72
+ var bands = [];
73
+ for (var b = 0; b < numBands; b++) {
74
+ var startBin = Math.max(0, Math.min(numBins - 1, binPoints[b]));
75
+ var peakBin = Math.max(0, Math.min(numBins - 1, binPoints[b+1]));
76
+ var endBin = Math.max(0, Math.min(numBins - 1, binPoints[b+2]));
77
+ var len = endBin - startBin + 1;
78
+ var weights = new Float32Array(len);
79
+ for (var i = 0; i < len; i++) {
80
+ var bin = startBin + i;
81
+ if (bin <= peakBin && peakBin > startBin) {
82
+ weights[i] = (bin - startBin) / (peakBin - startBin);
83
+ } else if (bin > peakBin && endBin > peakBin) {
84
+ weights[i] = (endBin - bin) / (endBin - peakBin);
85
+ } else {
86
+ weights[i] = 1;
87
+ }
88
+ }
89
+ bands.push({ startBin: startBin, peakBin: peakBin, endBin: endBin, weights: weights });
90
+ }
91
+ return bands;
92
+ }
93
+
94
+ function computeMelBands(mag, bands, out) {
95
+ for (var b = 0; b < bands.length; b++) {
96
+ var band = bands[b];
97
+ var energy = 0;
98
+ for (var i = 0; i < band.weights.length; i++) {
99
+ energy += mag[band.startBin + i] * band.weights[i];
100
+ }
101
+ out[b] = Math.log(1 + energy);
102
+ }
103
+ }
104
+
105
+ // ---- Processor ----
106
+ class BeatDetectorProcessor extends AudioWorkletProcessor {
107
+ constructor(options) {
108
+ super();
109
+ var opts = (options && options.processorOptions) || {};
110
+ this._sampleRate = sampleRate;
111
+ this._fftSize = opts.fftSize || 2048;
112
+ this._hopSize = opts.hopSize || 512;
113
+ this._minBpm = opts.minBpm || 50;
114
+ this._maxBpm = opts.maxBpm || 250;
115
+ this._melBands = opts.melBands || 24;
116
+ this._settlingMs = opts.settlingMs !== undefined ? opts.settlingMs : 1500;
117
+ this._tempoWindowSec = opts.tempoWindowSec || 6;
118
+ this._enableTsDetection = opts.enableTimeSignatureDetection !== false;
119
+
120
+ var numBins = this._fftSize >> 1;
121
+ this._real = new Float32Array(this._fftSize);
122
+ this._imag = new Float32Array(this._fftSize);
123
+ this._mag = new Float32Array(numBins);
124
+ this._ringBuffer = new Float32Array(this._fftSize);
125
+ this._ringWritePos = 0;
126
+ this._sampleCount = 0;
127
+ this._hopAccum = 0;
128
+
129
+ this._melBandFilters = buildMelFilterbank(this._melBands, 80, 8000, this._fftSize, this._sampleRate);
130
+ this._melOut = new Float32Array(this._melBands);
131
+ var fluxWindowLen = Math.ceil(this._tempoWindowSec * this._sampleRate / this._hopSize);
132
+ this._fluxWindow = new Float32Array(fluxWindowLen);
133
+ this._fluxWritePos = 0;
134
+ this._fluxCount = 0;
135
+ var LAG_K = 3;
136
+ this._prevMelFrames = [];
137
+ for (var i = 0; i < LAG_K; i++) {
138
+ this._prevMelFrames.push(new Float32Array(this._melBands));
139
+ }
140
+ this._prevMelFrameIdx = 0;
141
+
142
+ // Lag range in hops for BPM range
143
+ this._minLag = Math.max(1, Math.round(60 / this._maxBpm * this._sampleRate / this._hopSize));
144
+ this._maxLag = Math.round(60 / this._minBpm * this._sampleRate / this._hopSize);
145
+
146
+ this._hopsSinceACF = 0;
147
+ var ACF_INTERVAL = 15;
148
+ this._acfInterval = ACF_INTERVAL;
149
+
150
+ // Tempo state
151
+ this._bestBpm = 0;
152
+ this._bestScore = 0;
153
+ this._candidates = [];
154
+
155
+ // Phase state
156
+ this._lastBeatSample = -1;
157
+ this._ibiHistory = new Float32Array(4); // last 4 inter-beat intervals
158
+ this._ibiIdx = 0;
159
+
160
+ // Bar position state — parallel posteriors for 4/4 and 3/4
161
+ this._posterior4 = new Float32Array([0.25, 0.25, 0.25, 0.25]);
162
+ this._posterior3 = new Float32Array([1/3, 1/3, 1/3]);
163
+ this._ts4Confidence = 0.5;
164
+ this._ts3Confidence = 0.5;
165
+ this._activeTs = '4/4';
166
+ this._sustainCounter = 0;
167
+ this._barPosition = 1; // 1-indexed
168
+ this._barNumber = 0;
169
+ this._beatsSinceStart = 0;
170
+
171
+ // Confidence
172
+ this._confidence = 0;
173
+
174
+ // State snapshot cadence (~20 Hz)
175
+ var STATE_INTERVAL_HOPS = Math.round(this._sampleRate / this._hopSize / 20);
176
+ this._stateInterval = Math.max(1, STATE_INTERVAL_HOPS);
177
+ this._hopsSinceState = 0;
178
+
179
+ // Settling
180
+ this._startSample = currentFrame;
181
+ this._settledSamples = Math.round(this._settlingMs * 0.001 * this._sampleRate);
182
+
183
+ // Lookahead
184
+ this._lookahead = [];
185
+
186
+ // RMS / onset state
187
+ this._rms = 0;
188
+ this._onsetStrength = 0;
189
+
190
+ // Band energy (for state messages)
191
+ var LOW_BANDS = Math.round(this._melBands * 0.25);
192
+ var MID_BANDS = Math.round(this._melBands * 0.6);
193
+ this._lowBandEnd = LOW_BANDS;
194
+ this._midBandEnd = MID_BANDS;
195
+ }
196
+
197
+ process(inputs, _outputs, _parameters) {
198
+ var input = inputs[0];
199
+ if (!input || input.length === 0) return true;
200
+
201
+ var left = input[0] || [];
202
+ var right = input[1] || left;
203
+ var blockLen = left.length;
204
+
205
+ for (var s = 0; s < blockLen; s++) {
206
+ // Mono downmix
207
+ var mono = (left[s] + right[s]) * 0.5;
208
+
209
+ // Accumulate RMS
210
+ this._rms += mono * mono;
211
+
212
+ // Fill ring buffer
213
+ this._ringBuffer[this._ringWritePos] = mono;
214
+ this._ringWritePos = (this._ringWritePos + 1) & (this._fftSize - 1);
215
+
216
+ this._hopAccum++;
217
+ this._sampleCount++;
218
+
219
+ if (this._hopAccum >= this._hopSize) {
220
+ this._hopAccum = 0;
221
+ this._processHop();
222
+ }
223
+ }
224
+
225
+ return true;
226
+ }
227
+
228
+ _processHop() {
229
+ // Read ring buffer into real[] (oldest first)
230
+ var rb = this._ringBuffer;
231
+ var wp = this._ringWritePos;
232
+ var n = this._fftSize;
233
+ for (var i = 0; i < n; i++) {
234
+ this._real[i] = rb[(wp + i) & (n - 1)];
235
+ }
236
+
237
+ // FFT
238
+ fftInPlace(this._real, this._imag);
239
+
240
+ // Magnitude spectrum
241
+ var bins = n >> 1;
242
+ for (var i = 0; i < bins; i++) {
243
+ this._mag[i] = Math.sqrt(this._real[i]*this._real[i] + this._imag[i]*this._imag[i]);
244
+ }
245
+
246
+ // RMS (from time domain, using ring buffer)
247
+ var rmsAccum = 0;
248
+ for (var i = 0; i < n; i++) {
249
+ rmsAccum += rb[(wp + i) & (n - 1)] * rb[(wp + i) & (n - 1)];
250
+ }
251
+ this._rms = Math.sqrt(rmsAccum / n);
252
+
253
+ // Mel bands
254
+ computeMelBands(this._mag, this._melBandFilters, this._melOut);
255
+
256
+ // Spectral flux (SuperFlux-lite, lag k=3)
257
+ var K = this._prevMelFrames.length;
258
+ var flux = 0;
259
+ for (var b = 0; b < this._melBands; b++) {
260
+ var localMax = -Infinity;
261
+ for (var k = 0; k < K; k++) {
262
+ var prevVal = this._prevMelFrames[k][b];
263
+ if (prevVal > localMax) localMax = prevVal;
264
+ }
265
+ var diff = this._melOut[b] - localMax;
266
+ if (diff > 0) flux += diff;
267
+ }
268
+ this._onsetStrength = flux;
269
+
270
+ // Store current mel frame in circular buffer
271
+ var prevFrame = this._prevMelFrames[this._prevMelFrameIdx];
272
+ for (var b = 0; b < this._melBands; b++) {
273
+ prevFrame[b] = this._melOut[b];
274
+ }
275
+ this._prevMelFrameIdx = (this._prevMelFrameIdx + 1) % K;
276
+
277
+ // Add flux to sliding window
278
+ this._fluxWindow[this._fluxWritePos] = flux;
279
+ this._fluxWritePos = (this._fluxWritePos + 1) % this._fluxWindow.length;
280
+ if (this._fluxCount < this._fluxWindow.length) this._fluxCount++;
281
+
282
+ // Tempogram: compute ACF periodically
283
+ this._hopsSinceACF++;
284
+ if (this._hopsSinceACF >= this._acfInterval && this._fluxCount >= this._maxLag + 1) {
285
+ this._hopsSinceACF = 0;
286
+ this._computeACFAndCandidates();
287
+ }
288
+
289
+ // Phase tracker
290
+ var settled = (this._sampleCount - this._settledSamples) > 0;
291
+ if (this._bestBpm > 0 && settled) {
292
+ this._tickPhase(flux);
293
+ }
294
+
295
+ // State snapshot
296
+ this._hopsSinceState++;
297
+ if (this._hopsSinceState >= this._stateInterval) {
298
+ this._hopsSinceState = 0;
299
+ this._sendStateMessage();
300
+ }
301
+ }
302
+
303
+ _computeACFAndCandidates() {
304
+ var n = this._fluxCount;
305
+ var buf = this._fluxWindow;
306
+ var wp = this._fluxWritePos;
307
+ var len = buf.length;
308
+
309
+ // Compute ACF for lags in [minLag, maxLag]
310
+ var numLags = this._maxLag - this._minLag + 1;
311
+ var acf = new Float32Array(numLags);
312
+ for (var lagIdx = 0; lagIdx < numLags; lagIdx++) {
313
+ var lag = this._minLag + lagIdx;
314
+ var sum = 0, count = 0;
315
+ for (var t = lag; t < n; t++) {
316
+ var idxT = (wp - 1 - (n - 1 - t) + len) % len;
317
+ var idxTL = (wp - 1 - (n - 1 - (t - lag)) + len) % len;
318
+ sum += buf[idxT] * buf[idxTL];
319
+ count++;
320
+ }
321
+ acf[lagIdx] = count > 0 ? sum / count : 0;
322
+ }
323
+
324
+ // Normalise
325
+ var norm = 0;
326
+ for (var i = 0; i < numLags; i++) { if (acf[i] > norm) norm = acf[i]; }
327
+ if (norm > 0) {
328
+ for (var i = 0; i < numLags; i++) acf[i] /= norm;
329
+ }
330
+
331
+ // Find top peaks
332
+ var peaks = [];
333
+ for (var i = 1; i < numLags - 1; i++) {
334
+ if (acf[i] > acf[i-1] && acf[i] > acf[i+1] && acf[i] > 0) {
335
+ var lag2 = this._minLag + i;
336
+ var bpm = 60 * this._sampleRate / (lag2 * this._hopSize);
337
+ if (bpm >= this._minBpm && bpm <= this._maxBpm) {
338
+ peaks.push({ bpm: bpm, score: acf[i], lag: lag2 });
339
+ }
340
+ }
341
+ }
342
+ peaks.sort(function(a, b) { return b.score - a.score; });
343
+ this._candidates = peaks.slice(0, 3);
344
+
345
+ if (this._candidates.length === 0) return;
346
+
347
+ var top = this._candidates[0];
348
+
349
+ // Hysteresis
350
+ if (this._bestBpm <= 0) {
351
+ // First lock
352
+ this._bestBpm = top.bpm;
353
+ this._bestScore = top.score;
354
+ } else {
355
+ var isOctave = (Math.abs(top.bpm / this._bestBpm - 2) < 0.1) ||
356
+ (Math.abs(top.bpm / this._bestBpm - 0.5) < 0.05);
357
+ var margin = isOctave ? 1.5 : 1.15;
358
+ if (top.score > this._bestScore * margin) {
359
+ var oldBpm = this._bestBpm;
360
+ this._bestBpm = top.bpm;
361
+ this._bestScore = top.score;
362
+ // Only fire tempoChange if > 5% different
363
+ if (Math.abs(this._bestBpm - oldBpm) / oldBpm > 0.05) {
364
+ this.port.postMessage({ type: 'tempoChange', newTempo: this._bestBpm, oldTempo: oldBpm });
365
+ }
366
+ } else {
367
+ this._bestScore = this._bestScore * 0.99 + top.score * 0.01; // slowly decay
368
+ }
369
+ }
370
+
371
+ this._updateConfidence();
372
+ }
373
+
374
+ _tickPhase(flux) {
375
+ if (this._lastBeatSample < 0) {
376
+ // Bootstrap: set first beat at current sample
377
+ this._lastBeatSample = this._sampleCount;
378
+ return;
379
+ }
380
+
381
+ var beatIntervalSamples = 60 / this._bestBpm * this._sampleRate;
382
+ var phase = (this._sampleCount - this._lastBeatSample) / beatIntervalSamples;
383
+
384
+ // Snap correction: if novelty peak is strong and phase is 0.7..1.3, snap
385
+ if (phase >= 0.7 && phase <= 1.3 && flux > 0) {
386
+ // Compute mean recent flux
387
+ var recentMean = 0;
388
+ var count = Math.min(this._fluxCount, 16);
389
+ var wp = this._fluxWritePos;
390
+ var len = this._fluxWindow.length;
391
+ for (var i = 0; i < count; i++) {
392
+ recentMean += this._fluxWindow[(wp - 1 - i + len) % len];
393
+ }
394
+ recentMean /= count || 1;
395
+ if (flux > 1.5 * recentMean && recentMean > 0) {
396
+ // Snap to this sample
397
+ this._lastBeatSample = this._sampleCount;
398
+ phase = 1.0;
399
+ }
400
+ }
401
+
402
+ if (phase >= 1.0) {
403
+ var beatTime = (this._lastBeatSample + beatIntervalSamples) / this._sampleRate;
404
+ this._lastBeatSample += beatIntervalSamples;
405
+
406
+ // Update IBI history
407
+ this._ibiHistory[this._ibiIdx] = beatIntervalSamples;
408
+ this._ibiIdx = (this._ibiIdx + 1) & 3;
409
+
410
+ this._beatsSinceStart++;
411
+
412
+ // Update bar position
413
+ this._updateBarPosition(flux);
414
+
415
+ // Compute confidence
416
+ this._updateConfidence();
417
+
418
+ // Lookahead
419
+ this._updateLookahead(beatTime);
420
+
421
+ var isDownbeat = this._barPosition === 1;
422
+ var beatInfo = {
423
+ type: 'beat',
424
+ audioTime: beatTime,
425
+ tempo: this._bestBpm,
426
+ confidence: this._confidence,
427
+ beatPhase: 0,
428
+ energy: flux,
429
+ isDownbeat: isDownbeat,
430
+ beatInBar: this._barPosition,
431
+ };
432
+ this.port.postMessage(beatInfo);
433
+
434
+ if (isDownbeat) {
435
+ this.port.postMessage({
436
+ type: 'barStart',
437
+ audioTime: beatTime,
438
+ tempo: this._bestBpm,
439
+ confidence: this._confidence,
440
+ barNumber: this._barNumber,
441
+ });
442
+ }
443
+ }
444
+ }
445
+
446
+ _computeBeatLikelihood(flux) {
447
+ var count = Math.min(this._fluxCount, 32);
448
+ var wp = this._fluxWritePos;
449
+ var len = this._fluxWindow.length;
450
+ var totalFlux = 0;
451
+ for (var i = 0; i < count; i++) {
452
+ totalFlux += this._fluxWindow[(wp - 1 - i + len) % len];
453
+ }
454
+ var mean = count > 0 ? totalFlux / count : 1;
455
+ return Math.max(0.5, Math.min(1.5, mean > 0 ? flux / mean : 1));
456
+ }
457
+
458
+ _updateBarPosition(flux) {
459
+ var likelihood = this._computeBeatLikelihood(flux);
460
+
461
+ // --- 4/4 posterior ---
462
+ var p4 = this._posterior4;
463
+ var s4 = new Float32Array(4);
464
+ s4[0] = p4[3]; s4[1] = p4[0]; s4[2] = p4[1]; s4[3] = p4[2];
465
+ var sum4 = 0;
466
+ for (var i = 0; i < 4; i++) {
467
+ p4[i] = s4[i] * (likelihood + (i === 0 ? 0.3 : 0));
468
+ sum4 += p4[i];
469
+ }
470
+ if (sum4 > 0) { for (var i = 0; i < 4; i++) p4[i] /= sum4; }
471
+
472
+ // --- 3/4 posterior ---
473
+ var p3 = this._posterior3;
474
+ var s3 = new Float32Array(3);
475
+ s3[0] = p3[2]; s3[1] = p3[0]; s3[2] = p3[1];
476
+ var sum3 = 0;
477
+ for (var i = 0; i < 3; i++) {
478
+ p3[i] = s3[i] * (likelihood + (i === 0 ? 0.3 : 0));
479
+ sum3 += p3[i];
480
+ }
481
+ if (sum3 > 0) { for (var i = 0; i < 3; i++) p3[i] /= sum3; }
482
+
483
+ // --- Update TS confidences (EMA) ---
484
+ var max4 = 0;
485
+ for (var i = 0; i < 4; i++) { if (p4[i] > max4) max4 = p4[i]; }
486
+ var max3 = 0;
487
+ for (var i = 0; i < 3; i++) { if (p3[i] > max3) max3 = p3[i]; }
488
+ var alpha = 0.1;
489
+ this._ts4Confidence = (1 - alpha) * this._ts4Confidence + alpha * max4;
490
+ this._ts3Confidence = (1 - alpha) * this._ts3Confidence + alpha * max3;
491
+
492
+ // --- Hysteresis switching ---
493
+ if (this._enableTsDetection && this._beatsSinceStart > 8) {
494
+ var minSwitchMargin = 1.4;
495
+ var minSustainBeats = 12; // ~4 bars * 3 beats
496
+ var threeFavored = this._ts3Confidence > this._ts4Confidence * minSwitchMargin;
497
+ var fourFavored = this._ts4Confidence > this._ts3Confidence * minSwitchMargin;
498
+
499
+ if (this._activeTs === '4/4' && threeFavored) {
500
+ this._sustainCounter++;
501
+ if (this._sustainCounter >= minSustainBeats) {
502
+ this._activeTs = '3/4';
503
+ this._sustainCounter = 0;
504
+ }
505
+ } else if (this._activeTs === '3/4' && fourFavored) {
506
+ this._sustainCounter++;
507
+ if (this._sustainCounter >= minSustainBeats + 4) { // 16 beats for 4/4
508
+ this._activeTs = '4/4';
509
+ this._sustainCounter = 0;
510
+ }
511
+ } else {
512
+ this._sustainCounter = 0;
513
+ }
514
+ }
515
+
516
+ // --- Determine bar position from active TS ---
517
+ var barLen = this._activeTs === '3/4' ? 3 : 4;
518
+ var posterior = this._activeTs === '3/4' ? p3 : p4;
519
+
520
+ if (this._beatsSinceStart >= barLen) {
521
+ var maxP = -1, maxI = 0;
522
+ for (var i = 0; i < barLen; i++) {
523
+ if (posterior[i] > maxP) { maxP = posterior[i]; maxI = i; }
524
+ }
525
+ var newPos = maxI + 1; // 1-indexed
526
+ if (newPos === 1 && this._barPosition !== 1) {
527
+ this._barNumber++;
528
+ }
529
+ this._barPosition = newPos;
530
+ } else {
531
+ // Just advance sequentially
532
+ this._barPosition = ((this._barPosition) % barLen) + 1;
533
+ if (this._barPosition === 1) this._barNumber++;
534
+ }
535
+ }
536
+
537
+ _updateConfidence() {
538
+ if (this._candidates.length === 0) {
539
+ this._confidence = 0;
540
+ return;
541
+ }
542
+
543
+ // Peak contrast
544
+ var top1 = this._candidates[0] ? this._candidates[0].score : 0;
545
+ var top2 = this._candidates[1] ? this._candidates[1].score : top1;
546
+ var top3 = this._candidates[2] ? this._candidates[2].score : top2;
547
+ var peakContrast = (top2 + top3) > 0 ? top1 / ((top2 + top3) / 2) : 1;
548
+
549
+ // Phase consistency from IBI variance
550
+ var ibiMean = 0;
551
+ for (var i = 0; i < 4; i++) ibiMean += this._ibiHistory[i];
552
+ ibiMean /= 4;
553
+ var ibiVar = 0;
554
+ for (var i = 0; i < 4; i++) {
555
+ var d = this._ibiHistory[i] - ibiMean;
556
+ ibiVar += d * d;
557
+ }
558
+ ibiVar /= 4;
559
+ var phaseConsistency = ibiMean > 0 ? Math.max(0, 1 - ibiVar / (ibiMean * ibiMean)) : 0;
560
+
561
+ // Bar consistency (use the active posterior)
562
+ var activePosterior = this._activeTs === '3/4' ? this._posterior3 : this._posterior4;
563
+ var activeLen = this._activeTs === '3/4' ? 3 : 4;
564
+ var maxP = 0;
565
+ for (var i = 0; i < activeLen; i++) {
566
+ if (activePosterior[i] > maxP) maxP = activePosterior[i];
567
+ }
568
+ var barConsistency = maxP;
569
+
570
+ var c = Math.sqrt(Math.max(0, peakContrast / 2)) *
571
+ Math.sqrt(Math.max(0, phaseConsistency)) *
572
+ (0.5 + 0.5 * barConsistency);
573
+ this._confidence = Math.max(0, Math.min(1, c));
574
+ }
575
+
576
+ _updateLookahead(lastBeatTime) {
577
+ var lookahead = [];
578
+ var beatInterval = 60 / this._bestBpm;
579
+ var barPos = this._barPosition;
580
+ var barLen = this._activeTs === '3/4' ? 3 : 4;
581
+ for (var i = 0; i < 8; i++) {
582
+ var t = lastBeatTime + (i + 1) * beatInterval;
583
+ var bp = ((barPos - 1 + i) % barLen) + 1;
584
+ lookahead.push({
585
+ audioTime: t,
586
+ tempo: this._bestBpm,
587
+ isDownbeat: bp === 1,
588
+ beatInBar: bp,
589
+ });
590
+ }
591
+ this._lookahead = lookahead;
592
+ }
593
+
594
+ _computeBandEnergy() {
595
+ var low = 0, mid = 0, high = 0;
596
+ for (var b = 0; b < this._lowBandEnd; b++) low += this._melOut[b];
597
+ for (var b = this._lowBandEnd; b < this._midBandEnd; b++) mid += this._melOut[b];
598
+ for (var b = this._midBandEnd; b < this._melBands; b++) high += this._melOut[b];
599
+ var denom = this._lowBandEnd || 1;
600
+ return {
601
+ low: low / denom,
602
+ mid: mid / Math.max(1, this._midBandEnd - this._lowBandEnd),
603
+ high: high / Math.max(1, this._melBands - this._midBandEnd),
604
+ };
605
+ }
606
+
607
+ _sendStateMessage() {
608
+ var settled = (this._sampleCount - this._settledSamples) > 0;
609
+ var beatInterval = this._bestBpm > 0 ? 60 / this._bestBpm : 0;
610
+ var currentTime = this._sampleCount / this._sampleRate;
611
+ var lastBeatTime = this._lastBeatSample >= 0 ? this._lastBeatSample / this._sampleRate : 0;
612
+ var beatPhase = beatInterval > 0
613
+ ? Math.min(1, (currentTime - lastBeatTime) / beatInterval)
614
+ : 0;
615
+ var nextBeatTime = lastBeatTime + beatInterval;
616
+
617
+ var nextDownbeatTime = nextBeatTime;
618
+ for (var i = 0; i < this._lookahead.length; i++) {
619
+ if (this._lookahead[i].isDownbeat) {
620
+ nextDownbeatTime = this._lookahead[i].audioTime;
621
+ break;
622
+ }
623
+ }
624
+
625
+ var be = this._computeBandEnergy();
626
+
627
+ this.port.postMessage({
628
+ type: 'state',
629
+ tempo: settled ? this._bestBpm : 0,
630
+ beatPhase: beatPhase,
631
+ confidence: settled ? this._confidence : 0,
632
+ gridStability: settled ? this._confidence : 0,
633
+ tempoCandidates: settled ? this._candidates.map(function(c) {
634
+ return { bpm: c.bpm, score: c.score };
635
+ }) : [],
636
+ rms: this._rms,
637
+ onsetStrength: this._onsetStrength,
638
+ bandEnergy: be,
639
+ barPosition: this._barPosition,
640
+ barLength: this._activeTs === '3/4' ? 3 : 4,
641
+ timeSignature: this._activeTs === '3/4'
642
+ ? { numerator: 3, denominator: 4 }
643
+ : { numerator: 4, denominator: 4 },
644
+ lookahead: this._lookahead,
645
+ nextBeatTime: settled ? nextBeatTime : 0,
646
+ nextDownbeatTime: settled ? nextDownbeatTime : 0,
647
+ });
648
+ }
649
+ }
650
+
651
+ registerProcessor('${workletName}', BeatDetectorProcessor);
652
+ `;
653
+ // ---------------------------------------------------------------------------
654
+ // Main-thread BeatDetector class
655
+ // ---------------------------------------------------------------------------
656
+ class BeatDetector {
657
+ // ---- Signals ----
658
+ onBeat = new Signal();
659
+ onTempoChange = new Signal();
660
+ onDownbeat = new Signal();
661
+ onBarStart = new Signal();
662
+ onBeatPredicted = new Signal();
663
+ // ---- Options ----
664
+ // enableTimeSignatureDetection not in Required<> since it has a default; keep as full explicit type
665
+ _options;
666
+ // ---- Audio plumbing ----
667
+ _workletNode = null;
668
+ _source = null;
669
+ _tapSource = null;
670
+ _streamSource = null;
671
+ _ready = null;
672
+ // ---- Cached state from worklet ----
673
+ _tempo = 0;
674
+ _beatPhase = 0;
675
+ _nextBeatTime = 0;
676
+ _confidence = 0;
677
+ _gridStability = 0;
678
+ _tempoCandidates = [];
679
+ _rms = 0;
680
+ _onsetStrength = 0;
681
+ _bandEnergy = { low: 0, mid: 0, high: 0 };
682
+ _barPosition = 1;
683
+ _barLength = 4;
684
+ _timeSignature = { numerator: 4, denominator: 4 };
685
+ _nextDownbeatTime = 0;
686
+ _lookahead = Object.freeze([]);
687
+ constructor(options) {
688
+ this._options = {
689
+ minBpm: options?.minBpm ?? 50,
690
+ maxBpm: options?.maxBpm ?? 250,
691
+ fftSize: options?.fftSize ?? 2048,
692
+ hopSize: options?.hopSize ?? 512,
693
+ tempoWindowSec: options?.tempoWindowSec ?? 6,
694
+ settlingMs: options?.settlingMs ?? 1500,
695
+ melBands: options?.melBands ?? 24,
696
+ enableTimeSignatureDetection: options?.enableTimeSignatureDetection ?? true,
697
+ };
698
+ if (isAudioContextReady()) {
699
+ this._setup(getAudioContext());
700
+ }
701
+ else {
702
+ onAudioContextReady.once(this._setup, this);
703
+ }
704
+ }
705
+ // -----------------------------------------------------------------------
706
+ // Source setter (polymorphic tap)
707
+ // -----------------------------------------------------------------------
708
+ get source() {
709
+ return this._source;
710
+ }
711
+ set source(value) {
712
+ if (value === this._source)
713
+ return;
714
+ this._disconnectTap();
715
+ this._source = value;
716
+ if (value === null)
717
+ return;
718
+ if (isAudioContextReady()) {
719
+ this._connectSource(value, getAudioContext());
720
+ }
721
+ else {
722
+ onAudioContextReady.once((ctx) => {
723
+ if (this._source === value) {
724
+ this._connectSource(value, ctx);
725
+ }
726
+ }, this);
727
+ }
728
+ }
729
+ // -----------------------------------------------------------------------
730
+ // Ready promise
731
+ // -----------------------------------------------------------------------
732
+ get ready() {
733
+ return this._ready ?? Promise.resolve();
734
+ }
735
+ // -----------------------------------------------------------------------
736
+ // Stage 1 state accessors
737
+ // -----------------------------------------------------------------------
738
+ get tempo() { return this._tempo; }
739
+ get beatPhase() { return this._beatPhase; }
740
+ get nextBeatTime() { return this._nextBeatTime; }
741
+ get confidence() { return this._confidence; }
742
+ get gridStability() { return this._gridStability; }
743
+ get rms() { return this._rms; }
744
+ get onsetStrength() { return this._onsetStrength; }
745
+ get bandEnergy() { return this._bandEnergy; }
746
+ get tempoCandidates() {
747
+ return this._tempoCandidates;
748
+ }
749
+ // -----------------------------------------------------------------------
750
+ // Stage 2 state accessors
751
+ // -----------------------------------------------------------------------
752
+ get barPosition() { return this._barPosition; }
753
+ get barLength() { return this._barLength; }
754
+ get timeSignature() { return this._timeSignature; }
755
+ get nextDownbeatTime() { return this._nextDownbeatTime; }
756
+ get lookahead() {
757
+ return this._lookahead;
758
+ }
759
+ // -----------------------------------------------------------------------
760
+ // Lifecycle
761
+ // -----------------------------------------------------------------------
762
+ destroy() {
763
+ onAudioContextReady.clearByContext(this);
764
+ this._disconnectTap();
765
+ this._workletNode?.disconnect();
766
+ this._workletNode = null;
767
+ this._ready = null;
768
+ this.onBeat.clear();
769
+ this.onTempoChange.clear();
770
+ this.onDownbeat.clear();
771
+ this.onBarStart.clear();
772
+ this.onBeatPredicted.clear();
773
+ }
774
+ // -----------------------------------------------------------------------
775
+ // Private helpers — setup
776
+ // -----------------------------------------------------------------------
777
+ _setup(audioContext) {
778
+ const opts = this._options;
779
+ this._ready = registerWorkletProcessor(audioContext, workletName, beatDetectorWorkletSource).then(() => {
780
+ const node = new AudioWorkletNode(audioContext, workletName, {
781
+ numberOfInputs: 1,
782
+ numberOfOutputs: 0,
783
+ processorOptions: {
784
+ fftSize: opts.fftSize,
785
+ hopSize: opts.hopSize,
786
+ minBpm: opts.minBpm,
787
+ maxBpm: opts.maxBpm,
788
+ melBands: opts.melBands,
789
+ settlingMs: opts.settlingMs,
790
+ tempoWindowSec: opts.tempoWindowSec,
791
+ enableTimeSignatureDetection: opts.enableTimeSignatureDetection,
792
+ },
793
+ });
794
+ this._workletNode = node;
795
+ node.port.onmessage = this._onWorkletMessage.bind(this);
796
+ // If a source was set before worklet was ready, connect it now.
797
+ if (this._source !== null) {
798
+ this._connectSource(this._source, audioContext);
799
+ }
800
+ });
801
+ }
802
+ _onWorkletMessage(event) {
803
+ const msg = event.data;
804
+ switch (msg['type']) {
805
+ case 'state':
806
+ this._tempo = msg['tempo'] ?? 0;
807
+ this._beatPhase = msg['beatPhase'] ?? 0;
808
+ this._nextBeatTime = msg['nextBeatTime'] ?? 0;
809
+ this._nextDownbeatTime = msg['nextDownbeatTime'] ?? 0;
810
+ this._confidence = msg['confidence'] ?? 0;
811
+ this._gridStability = msg['gridStability'] ?? 0;
812
+ this._tempoCandidates = Object.freeze(msg['tempoCandidates'] ?? []);
813
+ this._rms = msg['rms'] ?? 0;
814
+ this._onsetStrength = msg['onsetStrength'] ?? 0;
815
+ this._bandEnergy = msg['bandEnergy'] ?? { low: 0, mid: 0, high: 0 };
816
+ this._barPosition = msg['barPosition'] ?? 1;
817
+ this._barLength = msg['barLength'] ?? 4;
818
+ this._timeSignature = msg['timeSignature'] ?? { numerator: 4, denominator: 4 };
819
+ {
820
+ const la = msg['lookahead'] ?? [];
821
+ this._lookahead = Object.freeze(la);
822
+ if (la.length > 0) {
823
+ this.onBeatPredicted.dispatch(la[0]);
824
+ }
825
+ }
826
+ break;
827
+ case 'beat': {
828
+ const bi = {
829
+ audioTime: msg['audioTime'],
830
+ tempo: msg['tempo'],
831
+ confidence: msg['confidence'],
832
+ beatPhase: msg['beatPhase'],
833
+ energy: msg['energy'],
834
+ isDownbeat: msg['isDownbeat'],
835
+ beatInBar: msg['beatInBar'],
836
+ };
837
+ this.onBeat.dispatch(bi);
838
+ if (bi.isDownbeat)
839
+ this.onDownbeat.dispatch(bi);
840
+ break;
841
+ }
842
+ case 'tempoChange':
843
+ this.onTempoChange.dispatch(msg['newTempo'], msg['oldTempo']);
844
+ break;
845
+ case 'barStart': {
846
+ const info = {
847
+ audioTime: msg['audioTime'],
848
+ tempo: msg['tempo'],
849
+ confidence: msg['confidence'],
850
+ barNumber: msg['barNumber'],
851
+ };
852
+ this.onBarStart.dispatch(info);
853
+ break;
854
+ }
855
+ }
856
+ }
857
+ // -----------------------------------------------------------------------
858
+ // Private helpers — source tap
859
+ // -----------------------------------------------------------------------
860
+ _connectSource(source, audioContext) {
861
+ if (!this._workletNode)
862
+ return;
863
+ const tap = this._resolveToAudioNode(source, audioContext);
864
+ if (!tap) {
865
+ this._deferConnectionViaBus(source);
866
+ return;
867
+ }
868
+ this._tapSource = tap;
869
+ tap.connect(this._workletNode, 0, 0);
870
+ }
871
+ _resolveToAudioNode(source, audioContext) {
872
+ if (source === null)
873
+ return null;
874
+ // MediaStream — duck-type via getTracks
875
+ const asStream = source;
876
+ if (typeof asStream.getTracks === 'function') {
877
+ if (this._streamSource) {
878
+ this._streamSource.disconnect();
879
+ this._streamSource = null;
880
+ }
881
+ const msNode = audioContext.createMediaStreamSource(source);
882
+ this._streamSource = msNode;
883
+ return msNode;
884
+ }
885
+ // AudioBus — has _getOutputNode (checked before raw AudioNode)
886
+ const asBus = source;
887
+ if (typeof asBus._getOutputNode === 'function') {
888
+ return asBus._getOutputNode();
889
+ }
890
+ // Sound / Music — has analyserTarget
891
+ const asMedia = source;
892
+ if ('analyserTarget' in asMedia) {
893
+ return asMedia.analyserTarget ?? null;
894
+ }
895
+ // Raw AudioNode — duck-type: has connect & disconnect
896
+ const asNode = source;
897
+ if (typeof asNode.connect === 'function' && typeof asNode.disconnect === 'function') {
898
+ return source;
899
+ }
900
+ return null;
901
+ }
902
+ _deferConnectionViaBus(source) {
903
+ const asBus = source;
904
+ if (typeof asBus.onceSetup === 'function') {
905
+ asBus.onceSetup(() => {
906
+ if (this._source === source && this._workletNode && isAudioContextReady()) {
907
+ this._connectSource(source, getAudioContext());
908
+ }
909
+ });
910
+ return;
911
+ }
912
+ onAudioContextReady.once(() => {
913
+ if (this._source === source && this._workletNode && isAudioContextReady()) {
914
+ this._connectSource(source, getAudioContext());
915
+ }
916
+ }, this);
917
+ }
918
+ _disconnectTap() {
919
+ if (this._tapSource && this._workletNode) {
920
+ try {
921
+ this._tapSource.disconnect(this._workletNode);
922
+ }
923
+ catch {
924
+ // Ignore
925
+ }
926
+ }
927
+ this._tapSource = null;
928
+ if (this._streamSource) {
929
+ this._streamSource.disconnect();
930
+ this._streamSource = null;
931
+ }
932
+ }
933
+ }
934
+
935
+ export { BeatDetector };
936
+ //# sourceMappingURL=BeatDetector.js.map