easecut-react 1.1.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/dist/.dist-D4zXwUWp.js +81 -0
  4. package/dist/assets/audio-waveform.worker-DiTI_wJb.js +353 -0
  5. package/dist/assets/fonts/alibaba-puhuiti-2-regular.woff2 +0 -0
  6. package/dist/assets/fonts/licenses/source-han-sans-OFL-1.1.txt +96 -0
  7. package/dist/assets/fonts/licenses/zcool-gaoduanhei-usage-statement.txt +52 -0
  8. package/dist/assets/fonts/source-han-sans-cn-regular.otf +0 -0
  9. package/dist/assets/fonts/zcool-canger-yuyang-w03.ttf +0 -0
  10. package/dist/assets/fonts/zcool-gaoduanhei.ttf +0 -0
  11. package/dist/assets/fonts/zcool-kuaile.ttf +0 -0
  12. package/dist/assets/fonts/zcool-kuhei.ttf +0 -0
  13. package/dist/assets/fonts/zcool-wenyi.ttf +0 -0
  14. package/dist/assets/fonts/zcool-xiaowei-logo.otf +0 -0
  15. package/dist/assets/frame-preview.worker-ruBHl77w.js +345 -0
  16. package/dist/assets/soundtouch-processor-BKTidsbT.js +2062 -0
  17. package/dist/index.js +10802 -0
  18. package/dist/styles.css +1 -0
  19. package/dist/types/editor/EaseCut.d.ts +4 -0
  20. package/dist/types/editor/EaseCut.d.ts.map +1 -0
  21. package/dist/types/editor/api/create-easecut-api.d.ts +12 -0
  22. package/dist/types/editor/api/create-easecut-api.d.ts.map +1 -0
  23. package/dist/types/editor/api/easecut-api-context.d.ts +12 -0
  24. package/dist/types/editor/api/easecut-api-context.d.ts.map +1 -0
  25. package/dist/types/editor/api/errors.d.ts +6 -0
  26. package/dist/types/editor/api/errors.d.ts.map +1 -0
  27. package/dist/types/editor/api/index.d.ts +3 -0
  28. package/dist/types/editor/api/index.d.ts.map +1 -0
  29. package/dist/types/editor/api/source-input.d.ts +7 -0
  30. package/dist/types/editor/api/source-input.d.ts.map +1 -0
  31. package/dist/types/editor/api/source-store.d.ts +16 -0
  32. package/dist/types/editor/api/source-store.d.ts.map +1 -0
  33. package/dist/types/editor/api/types.d.ts +78 -0
  34. package/dist/types/editor/api/types.d.ts.map +1 -0
  35. package/dist/types/editor/components/AudioFloatingInspector.d.ts +10 -0
  36. package/dist/types/editor/components/AudioFloatingInspector.d.ts.map +1 -0
  37. package/dist/types/editor/components/CanvasRatioPanel.d.ts +2 -0
  38. package/dist/types/editor/components/CanvasRatioPanel.d.ts.map +1 -0
  39. package/dist/types/editor/components/ExportMenu.d.ts +8 -0
  40. package/dist/types/editor/components/ExportMenu.d.ts.map +1 -0
  41. package/dist/types/editor/components/FloatingInspector.d.ts +11 -0
  42. package/dist/types/editor/components/FloatingInspector.d.ts.map +1 -0
  43. package/dist/types/editor/components/FloatingInspectorBasicPanel.d.ts +10 -0
  44. package/dist/types/editor/components/FloatingInspectorBasicPanel.d.ts.map +1 -0
  45. package/dist/types/editor/components/FloatingInspectorShell.d.ts +15 -0
  46. package/dist/types/editor/components/FloatingInspectorShell.d.ts.map +1 -0
  47. package/dist/types/editor/components/FloatingInspectorSpeedPanel.d.ts +7 -0
  48. package/dist/types/editor/components/FloatingInspectorSpeedPanel.d.ts.map +1 -0
  49. package/dist/types/editor/components/FormDialog.d.ts +16 -0
  50. package/dist/types/editor/components/FormDialog.d.ts.map +1 -0
  51. package/dist/types/editor/components/PreviewPanel.d.ts +9 -0
  52. package/dist/types/editor/components/PreviewPanel.d.ts.map +1 -0
  53. package/dist/types/editor/components/TextFloatingInspector.d.ts +12 -0
  54. package/dist/types/editor/components/TextFloatingInspector.d.ts.map +1 -0
  55. package/dist/types/editor/components/TimelineToolbar.d.ts +8 -0
  56. package/dist/types/editor/components/TimelineToolbar.d.ts.map +1 -0
  57. package/dist/types/editor/components/TitleContentTextarea.d.ts +8 -0
  58. package/dist/types/editor/components/TitleContentTextarea.d.ts.map +1 -0
  59. package/dist/types/editor/components/VideoFloatingInspector.d.ts +12 -0
  60. package/dist/types/editor/components/VideoFloatingInspector.d.ts.map +1 -0
  61. package/dist/types/editor/components/ui/ColorInput.d.ts +15 -0
  62. package/dist/types/editor/components/ui/ColorInput.d.ts.map +1 -0
  63. package/dist/types/editor/components/ui/IconButton.d.ts +15 -0
  64. package/dist/types/editor/components/ui/IconButton.d.ts.map +1 -0
  65. package/dist/types/editor/components/ui/InputNumber.d.ts +13 -0
  66. package/dist/types/editor/components/ui/InputNumber.d.ts.map +1 -0
  67. package/dist/types/editor/components/ui/Select.d.ts +14 -0
  68. package/dist/types/editor/components/ui/Select.d.ts.map +1 -0
  69. package/dist/types/editor/components/ui/TextInput.d.ts +10 -0
  70. package/dist/types/editor/components/ui/TextInput.d.ts.map +1 -0
  71. package/dist/types/editor/core/audio-waveform-bars.d.ts +50 -0
  72. package/dist/types/editor/core/audio-waveform-bars.d.ts.map +1 -0
  73. package/dist/types/editor/core/canvas-size.d.ts +44 -0
  74. package/dist/types/editor/core/canvas-size.d.ts.map +1 -0
  75. package/dist/types/editor/core/clip-speed.d.ts +11 -0
  76. package/dist/types/editor/core/clip-speed.d.ts.map +1 -0
  77. package/dist/types/editor/core/collision.d.ts +96 -0
  78. package/dist/types/editor/core/collision.d.ts.map +1 -0
  79. package/dist/types/editor/core/composition.d.ts +47 -0
  80. package/dist/types/editor/core/composition.d.ts.map +1 -0
  81. package/dist/types/editor/core/export-schema.d.ts +5 -0
  82. package/dist/types/editor/core/export-schema.d.ts.map +1 -0
  83. package/dist/types/editor/core/index.d.ts +11 -0
  84. package/dist/types/editor/core/index.d.ts.map +1 -0
  85. package/dist/types/editor/core/model.d.ts +187 -0
  86. package/dist/types/editor/core/model.d.ts.map +1 -0
  87. package/dist/types/editor/core/preview-snapping.d.ts +27 -0
  88. package/dist/types/editor/core/preview-snapping.d.ts.map +1 -0
  89. package/dist/types/editor/core/text-fonts.d.ts +76 -0
  90. package/dist/types/editor/core/text-fonts.d.ts.map +1 -0
  91. package/dist/types/editor/core/time.d.ts +15 -0
  92. package/dist/types/editor/core/time.d.ts.map +1 -0
  93. package/dist/types/editor/core/timeline-commands.d.ts +143 -0
  94. package/dist/types/editor/core/timeline-commands.d.ts.map +1 -0
  95. package/dist/types/editor/core/timeline-layout.d.ts +25 -0
  96. package/dist/types/editor/core/timeline-layout.d.ts.map +1 -0
  97. package/dist/types/editor/core/timeline-math.d.ts +22 -0
  98. package/dist/types/editor/core/timeline-math.d.ts.map +1 -0
  99. package/dist/types/editor/core/timeline-tracks.d.ts +47 -0
  100. package/dist/types/editor/core/timeline-tracks.d.ts.map +1 -0
  101. package/dist/types/editor/media/audio-waveform-peaks.d.ts +17 -0
  102. package/dist/types/editor/media/audio-waveform-peaks.d.ts.map +1 -0
  103. package/dist/types/editor/media/audio-waveform.d.ts +8 -0
  104. package/dist/types/editor/media/audio-waveform.d.ts.map +1 -0
  105. package/dist/types/editor/media/frame-preview.d.ts +25 -0
  106. package/dist/types/editor/media/frame-preview.d.ts.map +1 -0
  107. package/dist/types/editor/media/index.d.ts +6 -0
  108. package/dist/types/editor/media/index.d.ts.map +1 -0
  109. package/dist/types/editor/media/media-runtime.d.ts +47 -0
  110. package/dist/types/editor/media/media-runtime.d.ts.map +1 -0
  111. package/dist/types/editor/media/mediabunny-audio-waveform.d.ts +8 -0
  112. package/dist/types/editor/media/mediabunny-audio-waveform.d.ts.map +1 -0
  113. package/dist/types/editor/media/mediabunny-frame-preview.d.ts +21 -0
  114. package/dist/types/editor/media/mediabunny-frame-preview.d.ts.map +1 -0
  115. package/dist/types/editor/media/preview-audio-engine.d.ts +23 -0
  116. package/dist/types/editor/media/preview-audio-engine.d.ts.map +1 -0
  117. package/dist/types/editor/media/preview-playback-controller.d.ts +26 -0
  118. package/dist/types/editor/media/preview-playback-controller.d.ts.map +1 -0
  119. package/dist/types/editor/media/single-frame-preview.d.ts +30 -0
  120. package/dist/types/editor/media/single-frame-preview.d.ts.map +1 -0
  121. package/dist/types/editor/media/text-layout-runtime.d.ts +27 -0
  122. package/dist/types/editor/media/text-layout-runtime.d.ts.map +1 -0
  123. package/dist/types/editor/store/index.d.ts +3 -0
  124. package/dist/types/editor/store/index.d.ts.map +1 -0
  125. package/dist/types/editor/store/timeline-store-context.d.ts +14 -0
  126. package/dist/types/editor/store/timeline-store-context.d.ts.map +1 -0
  127. package/dist/types/editor/store/timeline-store.d.ts +115 -0
  128. package/dist/types/editor/store/timeline-store.d.ts.map +1 -0
  129. package/dist/types/editor/timeline/AudioWaveformCanvas.d.ts +13 -0
  130. package/dist/types/editor/timeline/AudioWaveformCanvas.d.ts.map +1 -0
  131. package/dist/types/editor/timeline/TimelineClip.d.ts +41 -0
  132. package/dist/types/editor/timeline/TimelineClip.d.ts.map +1 -0
  133. package/dist/types/editor/timeline/TimelineDragGhost.d.ts +9 -0
  134. package/dist/types/editor/timeline/TimelineDragGhost.d.ts.map +1 -0
  135. package/dist/types/editor/timeline/TimelinePanel.d.ts +11 -0
  136. package/dist/types/editor/timeline/TimelinePanel.d.ts.map +1 -0
  137. package/dist/types/editor/timeline/TimelineRuler.d.ts +14 -0
  138. package/dist/types/editor/timeline/TimelineRuler.d.ts.map +1 -0
  139. package/dist/types/editor/timeline/TimelineTrimHandle.d.ts +12 -0
  140. package/dist/types/editor/timeline/TimelineTrimHandle.d.ts.map +1 -0
  141. package/dist/types/editor/timeline/TimelineViewport.d.ts +8 -0
  142. package/dist/types/editor/timeline/TimelineViewport.d.ts.map +1 -0
  143. package/dist/types/editor/timeline/clip-reveal.d.ts +29 -0
  144. package/dist/types/editor/timeline/clip-reveal.d.ts.map +1 -0
  145. package/dist/types/editor/timeline/timeline-interaction.d.ts +79 -0
  146. package/dist/types/editor/timeline/timeline-interaction.d.ts.map +1 -0
  147. package/dist/types/editor/timeline/useTimelineController.d.ts +22 -0
  148. package/dist/types/editor/timeline/useTimelineController.d.ts.map +1 -0
  149. package/dist/types/editor/types.d.ts +52 -0
  150. package/dist/types/editor/types.d.ts.map +1 -0
  151. package/dist/types/editor/util/abort-error.d.ts +3 -0
  152. package/dist/types/editor/util/abort-error.d.ts.map +1 -0
  153. package/dist/types/editor/util/browser.d.ts +5 -0
  154. package/dist/types/editor/util/browser.d.ts.map +1 -0
  155. package/dist/types/editor/util/format-canvas-font.d.ts +7 -0
  156. package/dist/types/editor/util/format-canvas-font.d.ts.map +1 -0
  157. package/dist/types/editor/util/format-media-label.d.ts +4 -0
  158. package/dist/types/editor/util/format-media-label.d.ts.map +1 -0
  159. package/dist/types/editor/util/format-text-color.d.ts +4 -0
  160. package/dist/types/editor/util/format-text-color.d.ts.map +1 -0
  161. package/dist/types/editor/util/format-timeline-time.d.ts +6 -0
  162. package/dist/types/editor/util/format-timeline-time.d.ts.map +1 -0
  163. package/dist/types/editor/util/id.d.ts +2 -0
  164. package/dist/types/editor/util/id.d.ts.map +1 -0
  165. package/dist/types/editor/util/media-file-format.d.ts +6 -0
  166. package/dist/types/editor/util/media-file-format.d.ts.map +1 -0
  167. package/dist/types/editor/util/number.d.ts +6 -0
  168. package/dist/types/editor/util/number.d.ts.map +1 -0
  169. package/dist/types/editor/util/text.d.ts +2 -0
  170. package/dist/types/editor/util/text.d.ts.map +1 -0
  171. package/dist/types/editor/util/url.d.ts +5 -0
  172. package/dist/types/editor/util/url.d.ts.map +1 -0
  173. package/dist/types/editor/util/value.d.ts +3 -0
  174. package/dist/types/editor/util/value.d.ts.map +1 -0
  175. package/dist/types/editor/workers/audio-waveform-protocol.d.ts +24 -0
  176. package/dist/types/editor/workers/audio-waveform-protocol.d.ts.map +1 -0
  177. package/dist/types/editor/workers/audio-waveform.worker.d.ts +2 -0
  178. package/dist/types/editor/workers/audio-waveform.worker.d.ts.map +1 -0
  179. package/dist/types/editor/workers/frame-preview-protocol.d.ts +35 -0
  180. package/dist/types/editor/workers/frame-preview-protocol.d.ts.map +1 -0
  181. package/dist/types/editor/workers/frame-preview.worker.d.ts +2 -0
  182. package/dist/types/editor/workers/frame-preview.worker.d.ts.map +1 -0
  183. package/dist/types/index.d.ts +7 -0
  184. package/dist/types/index.d.ts.map +1 -0
  185. package/package.json +74 -0
@@ -0,0 +1,2062 @@
1
+ //#region ../core/src/AbstractSamplePipe.ts
2
+ /**
3
+ * Abstract base class for sample processing pipes.
4
+ *
5
+ * @remarks
6
+ * Manages input and output buffers for audio sample processing chains. Subclasses should implement
7
+ * specific processing logic for audio transformation or analysis. This class is not intended to be used
8
+ * directly, but as a base for concrete audio processing stages.
9
+ *
10
+ * @typeParam TInputBuffer - Concrete input buffer type (defaults to the generic `SampleBuffer` contract).
11
+ * @typeParam TOutputBuffer - Concrete output buffer type (defaults to `TInputBuffer` so input/output share the same buffer type unless a subclass opts into different types).
12
+ */
13
+ var AbstractSamplePipe = class {
14
+ /**
15
+ * Input buffer for audio samples.
16
+ */
17
+ _inputBuffer;
18
+ /**
19
+ * Output buffer for processed audio samples.
20
+ */
21
+ _outputBuffer;
22
+ /**
23
+ * Constructs an AbstractSamplePipe.
24
+ * @param options Constructor options.
25
+ *
26
+ * @remarks
27
+ * When `createBuffers` is true, both factories are required so subclasses can
28
+ * control exact buffer implementations without unsafe casting.
29
+ */
30
+ constructor({ createBuffers = false, inputBufferFactory, outputBufferFactory } = {}) {
31
+ if (createBuffers) {
32
+ if (!inputBufferFactory || !outputBufferFactory) throw new Error("buffer factories are required when createBuffers is true");
33
+ this._inputBuffer = inputBufferFactory();
34
+ this._outputBuffer = outputBufferFactory();
35
+ } else {
36
+ this._inputBuffer = null;
37
+ this._outputBuffer = null;
38
+ }
39
+ }
40
+ /**
41
+ * Gets the input buffer.
42
+ * @returns The current input buffer instance, or null if not set.
43
+ */
44
+ get inputBuffer() {
45
+ return this._inputBuffer;
46
+ }
47
+ /**
48
+ * Sets the input buffer.
49
+ * @param inputBuffer The new input buffer instance, or null to unset.
50
+ */
51
+ set inputBuffer(inputBuffer) {
52
+ this._inputBuffer = inputBuffer;
53
+ }
54
+ /**
55
+ * Gets the output buffer.
56
+ * @returns The current output buffer instance, or null if not set.
57
+ */
58
+ get outputBuffer() {
59
+ return this._outputBuffer;
60
+ }
61
+ /**
62
+ * Sets the output buffer.
63
+ * @param outputBuffer The new output buffer instance, or null to unset.
64
+ */
65
+ set outputBuffer(outputBuffer) {
66
+ this._outputBuffer = outputBuffer;
67
+ }
68
+ /**
69
+ * Clears both input and output buffers.
70
+ *
71
+ * @remarks
72
+ * Resets the state of both input and output buffers, if present, by calling their `clear()` methods.
73
+ */
74
+ clear() {
75
+ this._inputBuffer?.clear();
76
+ this._outputBuffer?.clear();
77
+ }
78
+ };
79
+ //#endregion
80
+ //#region ../core/src/CircularSampleBuffer.ts
81
+ var SAMPLES_PER_FRAME$1 = 2;
82
+ /**
83
+ * Circular frame buffer for interleaved stereo audio samples.
84
+ *
85
+ * @remarks
86
+ * Implements a ring buffer for stereo audio, where each frame consists of two contiguous float values (left, right).
87
+ * Maintains a movable read cursor and appends at the logical end. Capacity grows automatically as needed while preserving frame order.
88
+ * Used for efficient, low-latency audio processing where buffer wraparound and dynamic resizing are required.
89
+ */
90
+ var CircularSampleBuffer = class {
91
+ _buffer;
92
+ _capacityFrames;
93
+ _readFrame;
94
+ _frameCount;
95
+ /**
96
+ * @param capacityFrames Initial frame capacity before automatic growth.
97
+ */
98
+ constructor(capacityFrames = 2048) {
99
+ const normalizedCapacity = Math.max(1, Math.floor(capacityFrames));
100
+ this._capacityFrames = normalizedCapacity;
101
+ this._buffer = new Float32Array(normalizedCapacity * SAMPLES_PER_FRAME$1);
102
+ this._readFrame = 0;
103
+ this._frameCount = 0;
104
+ }
105
+ /**
106
+ * Allocated capacity expressed in frames.
107
+ * @returns The number of frames the buffer can currently hold without resizing.
108
+ */
109
+ get capacityFrames() {
110
+ return this._capacityFrames;
111
+ }
112
+ /**
113
+ * Number of buffered frames currently readable.
114
+ * @returns The number of frames available for reading.
115
+ */
116
+ get frameCount() {
117
+ return this._frameCount;
118
+ }
119
+ /**
120
+ * Clears the buffer without shrinking allocated capacity.
121
+ *
122
+ * @remarks
123
+ * Resets the read cursor and frame count, but does not deallocate the underlying storage.
124
+ */
125
+ clear() {
126
+ this._readFrame = 0;
127
+ this._frameCount = 0;
128
+ }
129
+ /**
130
+ * Ensures the internal storage can hold at least `minCapacityFrames`.
131
+ *
132
+ * @param minCapacityFrames Minimum frame capacity required.
133
+ * @remarks
134
+ * Grows the buffer if needed, preserving all readable frames in order.
135
+ */
136
+ ensureCapacity(minCapacityFrames) {
137
+ const normalizedMinCapacityFrames = Math.max(0, Math.floor(minCapacityFrames));
138
+ if (normalizedMinCapacityFrames <= this._capacityFrames) return;
139
+ const nextCapacity = Math.max(normalizedMinCapacityFrames, this._capacityFrames * 2, this._capacityFrames + 1024);
140
+ const nextBuffer = new Float32Array(nextCapacity * SAMPLES_PER_FRAME$1);
141
+ for (let frame = 0; frame < this._frameCount; frame += 1) {
142
+ const sourceIndex = (this._readFrame + frame) % this._capacityFrames * SAMPLES_PER_FRAME$1;
143
+ const destIndex = frame * SAMPLES_PER_FRAME$1;
144
+ nextBuffer[destIndex] = this._buffer[sourceIndex];
145
+ nextBuffer[destIndex + 1] = this._buffer[sourceIndex + 1];
146
+ }
147
+ this._buffer = nextBuffer;
148
+ this._capacityFrames = nextCapacity;
149
+ this._readFrame = 0;
150
+ }
151
+ /**
152
+ * Appends source frames to the end of the ring.
153
+ *
154
+ * @param source Interleaved stereo source samples.
155
+ * @param sourceFrameOffset Source offset in frames.
156
+ * @param frameCount Number of frames to append; defaults to all complete remaining frames.
157
+ * @remarks
158
+ * Automatically grows the buffer if needed. Only complete frames are appended.
159
+ */
160
+ pushSamples(source, sourceFrameOffset = 0, frameCount = 0) {
161
+ const sourceStartSample = Math.max(0, Math.floor(sourceFrameOffset)) * SAMPLES_PER_FRAME$1;
162
+ const availableFrames = Math.max(0, Math.floor((source.length - sourceStartSample) / SAMPLES_PER_FRAME$1));
163
+ const requestedFrames = frameCount > 0 ? Math.floor(frameCount) : 0;
164
+ const framesToWrite = requestedFrames > 0 ? Math.min(requestedFrames, availableFrames) : availableFrames;
165
+ if (framesToWrite <= 0) return;
166
+ this.ensureCapacity(this._frameCount + framesToWrite);
167
+ const writeFrame = (this._readFrame + this._frameCount) % this._capacityFrames;
168
+ for (let frame = 0; frame < framesToWrite; frame += 1) {
169
+ const sourceIndex = sourceStartSample + frame * SAMPLES_PER_FRAME$1;
170
+ const destIndex = (writeFrame + frame) % this._capacityFrames * SAMPLES_PER_FRAME$1;
171
+ this._buffer[destIndex] = source[sourceIndex];
172
+ this._buffer[destIndex + 1] = source[sourceIndex + 1];
173
+ }
174
+ this._frameCount += framesToWrite;
175
+ }
176
+ /**
177
+ * Contract alias for `pushSamples`.
178
+ *
179
+ * @param source Interleaved stereo source samples.
180
+ * @param sourceFrameOffset Source offset in frames.
181
+ * @param frameCount Number of frames to append.
182
+ */
183
+ putSamples(source, sourceFrameOffset = 0, frameCount = 0) {
184
+ this.pushSamples(source, sourceFrameOffset, frameCount);
185
+ }
186
+ /**
187
+ * Extracts frames from the ring into `target`.
188
+ *
189
+ * @param target Destination array for interleaved stereo samples.
190
+ * @param sourceFrameOffset Read offset in frames.
191
+ * @param frameCount Number of frames requested.
192
+ * @param consume When true, consumed frames are dropped from the front.
193
+ * @returns Number of frames copied.
194
+ * @remarks
195
+ * If `consume` is true, the extracted frames are removed from the buffer.
196
+ */
197
+ extract(target, sourceFrameOffset = 0, frameCount = 0, consume = false) {
198
+ const normalizedSourceFrameOffset = Math.max(0, Math.floor(sourceFrameOffset));
199
+ const requestedFrames = frameCount > 0 ? Math.floor(frameCount) : 0;
200
+ const framesAvailable = Math.max(0, this._frameCount - normalizedSourceFrameOffset);
201
+ const framesToRead = requestedFrames > 0 ? Math.min(requestedFrames, framesAvailable) : framesAvailable;
202
+ if (framesToRead <= 0) return 0;
203
+ for (let frame = 0; frame < framesToRead; frame += 1) {
204
+ const sourceIndex = (this._readFrame + normalizedSourceFrameOffset + frame) % this._capacityFrames * SAMPLES_PER_FRAME$1;
205
+ const targetIndex = frame * SAMPLES_PER_FRAME$1;
206
+ target[targetIndex] = this._buffer[sourceIndex];
207
+ target[targetIndex + 1] = this._buffer[sourceIndex + 1];
208
+ }
209
+ if (consume) {
210
+ const framesToDrop = normalizedSourceFrameOffset + framesToRead;
211
+ this.dropFrames(framesToDrop);
212
+ }
213
+ return framesToRead;
214
+ }
215
+ /**
216
+ * Reads a single sample value by logical sample index.
217
+ *
218
+ * @param sampleIndex Logical sample index relative to the readable head.
219
+ * @returns Sample value, or `0` when the index falls outside readable data.
220
+ * @remarks
221
+ * Used for random access to individual samples within the readable region.
222
+ */
223
+ readSample(sampleIndex) {
224
+ const normalizedSampleIndex = Math.max(0, Math.floor(sampleIndex));
225
+ const frameOffset = Math.floor(normalizedSampleIndex / SAMPLES_PER_FRAME$1);
226
+ if (frameOffset >= this._frameCount) return 0;
227
+ const channelOffset = normalizedSampleIndex % SAMPLES_PER_FRAME$1;
228
+ const sourceIndex = (this._readFrame + frameOffset) % this._capacityFrames * SAMPLES_PER_FRAME$1 + channelOffset;
229
+ return this._buffer[sourceIndex] ?? 0;
230
+ }
231
+ /**
232
+ * Drops frames from the front of the ring.
233
+ *
234
+ * @param frameCount Maximum number of frames to remove.
235
+ * @returns Number of frames removed.
236
+ * @remarks
237
+ * Advances the read cursor and reduces the frame count. If all frames are dropped, resets the read cursor.
238
+ */
239
+ dropFrames(frameCount) {
240
+ const framesToDrop = Math.max(0, Math.min(Math.max(0, Math.floor(frameCount)), this._frameCount));
241
+ if (framesToDrop === 0) return 0;
242
+ this._readFrame = (this._readFrame + framesToDrop) % this._capacityFrames;
243
+ this._frameCount -= framesToDrop;
244
+ if (this._frameCount === 0) this._readFrame = 0;
245
+ return framesToDrop;
246
+ }
247
+ /**
248
+ * Contract alias for `dropFrames`.
249
+ *
250
+ * @param frameCount Number of frames to consume.
251
+ */
252
+ receive(frameCount = this._frameCount) {
253
+ this.dropFrames(frameCount);
254
+ }
255
+ };
256
+ //#endregion
257
+ //#region ../core/src/FifoSampleBuffer.ts
258
+ /**
259
+ * Number of bytes per sample (Float32).
260
+ */
261
+ var BYTES_PER_SAMPLE = 4;
262
+ /**
263
+ * Number of samples per audio frame (stereo).
264
+ */
265
+ var SAMPLES_PER_FRAME = 2;
266
+ /**
267
+ * Number of bytes per audio frame.
268
+ */
269
+ var BYTES_PER_FRAME = BYTES_PER_SAMPLE * SAMPLES_PER_FRAME;
270
+ /**
271
+ * Default maximum number of frames for buffer allocation.
272
+ */
273
+ var DEFAULT_MAX_FRAMES = 131072;
274
+ /**
275
+ * Resizable interleaved sample buffer for audio processing.
276
+ *
277
+ * @remarks
278
+ * Stores stereo audio samples in a contiguous Float32Array and provides methods for efficient buffer management and sample transfer.
279
+ * Uses ES2024 ArrayBuffer for zero-allocation growth. Suitable for scenarios where buffer size may need to grow dynamically during audio processing.
280
+ */
281
+ var FifoSampleBuffer = class {
282
+ /**
283
+ * Backing ArrayBuffer for sample storage.
284
+ * @remarks
285
+ * Underlying memory for the buffer, which may be resized as needed.
286
+ */
287
+ _buffer;
288
+ /**
289
+ * Float32Array view of the buffer.
290
+ * @remarks
291
+ * Provides direct access to the sample data for reading and writing.
292
+ */
293
+ _vector;
294
+ /**
295
+ * Current read position (frame index).
296
+ * @remarks
297
+ * Indicates the logical start of readable data within the buffer.
298
+ */
299
+ _position;
300
+ /**
301
+ * Number of frames currently stored.
302
+ * @remarks
303
+ * Represents the number of complete stereo frames available for reading.
304
+ */
305
+ _frameCount;
306
+ /**
307
+ * Creates a new FifoSampleBuffer.
308
+ * @param maxFrames Maximum number of frames for buffer allocation.
309
+ */
310
+ constructor(maxFrames = DEFAULT_MAX_FRAMES) {
311
+ this._buffer = new ArrayBuffer(0, { maxByteLength: maxFrames * BYTES_PER_FRAME });
312
+ this._vector = new Float32Array(this._buffer);
313
+ this._position = 0;
314
+ this._frameCount = 0;
315
+ }
316
+ /**
317
+ * Returns the Float32Array view of the buffer.
318
+ * @returns The Float32Array containing the sample data.
319
+ */
320
+ get vector() {
321
+ return this._vector;
322
+ }
323
+ /**
324
+ * Returns the current read position (frame index).
325
+ * @returns The current frame index for reading.
326
+ */
327
+ get position() {
328
+ return this._position;
329
+ }
330
+ /**
331
+ * Returns the start sample index for reading.
332
+ * @returns The sample index corresponding to the start of readable data.
333
+ */
334
+ get startIndex() {
335
+ return this._position * 2;
336
+ }
337
+ /**
338
+ * Returns the number of frames currently stored.
339
+ * @returns The number of complete frames available for reading.
340
+ */
341
+ get frameCount() {
342
+ return this._frameCount;
343
+ }
344
+ /**
345
+ * Returns the end sample index for reading.
346
+ * @returns The sample index corresponding to the end of readable data.
347
+ */
348
+ get endIndex() {
349
+ return (this._position + this._frameCount) * 2;
350
+ }
351
+ /**
352
+ * Clears the buffer and resets position and frame count.
353
+ * @remarks
354
+ * Fills the buffer with zeros and resets all internal state.
355
+ */
356
+ clear() {
357
+ this._vector.fill(0);
358
+ this._position = 0;
359
+ this._frameCount = 0;
360
+ }
361
+ /**
362
+ * Adds empty frames to the buffer.
363
+ * @param numFrames Number of frames to add.
364
+ */
365
+ put(numFrames) {
366
+ this._frameCount += numFrames;
367
+ }
368
+ /**
369
+ * Adds samples to the buffer from a Float32Array.
370
+ * @param samples Source samples (interleaved stereo).
371
+ * @param position Start frame index in source.
372
+ * @param numFrames Number of frames to copy (default: all available).
373
+ * @remarks
374
+ * Automatically grows the buffer if needed. Only complete frames are appended.
375
+ */
376
+ putSamples(samples, position = 0, numFrames = 0) {
377
+ const sourceOffset = position * 2;
378
+ if (!(numFrames >= 0) || numFrames === 0) numFrames = (samples.length - sourceOffset) / 2;
379
+ const numSamples = numFrames * 2;
380
+ this.ensureCapacity(numFrames + this._frameCount);
381
+ const destOffset = this.endIndex;
382
+ this._vector.set(samples.subarray(sourceOffset, sourceOffset + numSamples), destOffset);
383
+ this._frameCount += numFrames;
384
+ }
385
+ /**
386
+ * Adds samples from another FifoSampleBuffer.
387
+ * @param buffer Source buffer.
388
+ * @param position Start frame index in source buffer.
389
+ * @param numFrames Number of frames to copy (default: all available).
390
+ */
391
+ putBuffer(buffer, position = 0, numFrames = 0) {
392
+ if (!(numFrames >= 0) || numFrames === 0) numFrames = buffer.frameCount - position;
393
+ this.putSamples(buffer.vector, buffer.position + position, numFrames);
394
+ }
395
+ /**
396
+ * Advances the read position and reduces frame count.
397
+ * @param numFrames Number of frames to receive (default: all available).
398
+ * @remarks
399
+ * Consumed frames are no longer available for reading.
400
+ */
401
+ receive(numFrames) {
402
+ if (numFrames === void 0 || !(numFrames >= 0) || numFrames > this._frameCount) numFrames = this._frameCount;
403
+ this._frameCount -= numFrames;
404
+ this._position += numFrames;
405
+ }
406
+ /**
407
+ * Copies and receives samples into an output array.
408
+ * @param output Destination Float32Array.
409
+ * @param numFrames Number of frames to copy and receive.
410
+ * @remarks
411
+ * Advances the read position after copying.
412
+ */
413
+ receiveSamples(output, numFrames = 0) {
414
+ const numSamples = numFrames * 2;
415
+ const sourceOffset = this.startIndex;
416
+ output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples));
417
+ this.receive(numFrames);
418
+ }
419
+ /**
420
+ * Extracts samples into an output array without advancing position.
421
+ * @param output Destination Float32Array.
422
+ * @param position Start frame index in buffer.
423
+ * @param numFrames Number of frames to extract.
424
+ */
425
+ extract(output, position = 0, numFrames = 0) {
426
+ const sourceOffset = this.startIndex + position * 2;
427
+ const numSamples = numFrames * 2;
428
+ output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples));
429
+ }
430
+ /**
431
+ * Ensures the buffer has capacity for at least numFrames.
432
+ * @param numFrames Minimum number of frames required.
433
+ * @remarks
434
+ * Grows the buffer if needed, preserving all readable frames in order.
435
+ */
436
+ ensureCapacity(numFrames = 0) {
437
+ const minLength = Math.floor(numFrames * SAMPLES_PER_FRAME);
438
+ if (this._vector.length < minLength) {
439
+ const newByteLength = minLength * BYTES_PER_SAMPLE;
440
+ if (newByteLength <= this._buffer.maxByteLength) {
441
+ this.rewind();
442
+ this._buffer.resize(newByteLength);
443
+ this._vector = new Float32Array(this._buffer);
444
+ } else {
445
+ const newMaxBytes = newByteLength * 2;
446
+ const newBuffer = new ArrayBuffer(newByteLength, { maxByteLength: newMaxBytes });
447
+ const newVector = new Float32Array(newBuffer);
448
+ newVector.set(this._vector.subarray(this.startIndex, this.endIndex));
449
+ this._buffer = newBuffer;
450
+ this._vector = newVector;
451
+ this._position = 0;
452
+ }
453
+ } else this.rewind();
454
+ }
455
+ /**
456
+ * Ensures buffer has capacity for additional frames.
457
+ * @param numFrames Number of additional frames required.
458
+ */
459
+ ensureAdditionalCapacity(numFrames = 0) {
460
+ this.ensureCapacity(this._frameCount + numFrames);
461
+ }
462
+ /**
463
+ * Moves all unread samples to the start of the buffer.
464
+ * @remarks
465
+ * Compacts the buffer so that all unread samples are at the beginning, freeing space for new data.
466
+ */
467
+ rewind() {
468
+ if (this._position > 0) {
469
+ this._vector.set(this._vector.subarray(this.startIndex, this.endIndex));
470
+ this._position = 0;
471
+ }
472
+ }
473
+ };
474
+ //#endregion
475
+ //#region ../core/src/SampleBufferAdapter.ts
476
+ var FifoSampleBufferAdapter = class {
477
+ inputBuffer;
478
+ constructor() {
479
+ this.inputBuffer = null;
480
+ }
481
+ get frameCount() {
482
+ return this.inputBuffer?.frameCount ?? 0;
483
+ }
484
+ clear() {
485
+ this.inputBuffer = null;
486
+ }
487
+ syncFromInputBuffer(inputBuffer) {
488
+ this.inputBuffer = inputBuffer;
489
+ }
490
+ extract(target, sourceFrameOffset, frameCount) {
491
+ const buffer = this.inputBuffer;
492
+ if (buffer === null) return 0;
493
+ const availableFrames = Math.max(0, buffer.frameCount - sourceFrameOffset);
494
+ const framesToExtract = Math.max(0, Math.min(frameCount, availableFrames));
495
+ if (framesToExtract === 0) return 0;
496
+ buffer.extract(target, sourceFrameOffset, framesToExtract);
497
+ return framesToExtract;
498
+ }
499
+ receive(frameCount) {
500
+ this.inputBuffer?.receive(frameCount);
501
+ }
502
+ };
503
+ var CircularSampleBufferAdapter = class {
504
+ circularBuffer;
505
+ scratch;
506
+ constructor() {
507
+ this.circularBuffer = new CircularSampleBuffer();
508
+ this.scratch = new Float32Array(0);
509
+ }
510
+ get frameCount() {
511
+ return this.circularBuffer.frameCount;
512
+ }
513
+ clear() {
514
+ this.circularBuffer.clear();
515
+ }
516
+ syncFromInputBuffer(inputBuffer) {
517
+ if (inputBuffer instanceof FifoSampleBuffer) {
518
+ const frames = inputBuffer.frameCount;
519
+ if (frames === 0) return;
520
+ this.circularBuffer.pushSamples(inputBuffer.vector, inputBuffer.position, frames);
521
+ inputBuffer.receive(frames);
522
+ return;
523
+ }
524
+ const frames = inputBuffer.frameCount;
525
+ if (frames === 0) return;
526
+ const sampleCount = frames * 2;
527
+ if (this.scratch.length < sampleCount) this.scratch = new Float32Array(sampleCount);
528
+ inputBuffer.extract(this.scratch, 0, frames);
529
+ this.circularBuffer.pushSamples(this.scratch, 0, frames);
530
+ inputBuffer.receive(frames);
531
+ }
532
+ extract(target, sourceFrameOffset, frameCount) {
533
+ return this.circularBuffer.extract(target, sourceFrameOffset, frameCount, false);
534
+ }
535
+ receive(frameCount) {
536
+ this.circularBuffer.dropFrames(frameCount);
537
+ }
538
+ };
539
+ /** Creates an adapter that reads directly from any `SampleBuffer` contract. */
540
+ var createFifoSampleBufferAdapter = () => new FifoSampleBufferAdapter();
541
+ /**
542
+ * Creates an adapter that stages source frames in a circular buffer for
543
+ * efficient repeated reads.
544
+ */
545
+ var createCircularSampleBufferAdapter = () => new CircularSampleBufferAdapter();
546
+ //#endregion
547
+ //#region ../interpolation-strategy-lanczos/.dist/index.js
548
+ var LANCZOS_DEFAULT_PARAMS = {
549
+ zeroCrossings: 4,
550
+ normalize: false
551
+ };
552
+ function normalizeLanczosParams(params, defaults) {
553
+ const merged = {
554
+ ...defaults,
555
+ ...params ?? {}
556
+ };
557
+ return {
558
+ zeroCrossings: Math.max(2, Math.min(8, Math.round(Number(merged["zeroCrossings"] ?? defaults["zeroCrossings"] ?? 4)))),
559
+ normalize: Boolean(merged["normalize"])
560
+ };
561
+ }
562
+ function applyLanczosParams(state, params) {
563
+ if (typeof state !== "object" || state === null) return;
564
+ const record = state;
565
+ record.params = {
566
+ zeroCrossings: Math.max(2, Math.round(Number(params["zeroCrossings"] ?? 4))),
567
+ normalize: Boolean(params["normalize"])
568
+ };
569
+ }
570
+ function readFrameSample(src, srcOffset, numFrames, frameIndex, channel, state) {
571
+ if (frameIndex < 0) return channel === 0 ? state.prevSampleL : state.prevSampleR;
572
+ if (frameIndex >= numFrames) return src[srcOffset + 2 * (numFrames - 1) + channel];
573
+ return src[srcOffset + 2 * frameIndex + channel];
574
+ }
575
+ function normalizedSinc(x) {
576
+ if (x === 0) return 1;
577
+ const value = Math.PI * x;
578
+ return Math.sin(value) / value;
579
+ }
580
+ function lanczosWeight(distance, radius) {
581
+ if (Math.abs(distance) >= radius) return 0;
582
+ return normalizedSinc(distance) * normalizedSinc(distance / radius);
583
+ }
584
+ var lanczosKernel = (src, srcOffset, numFrames, position, channel, state) => {
585
+ const kernelState = state;
586
+ const radius = kernelState.params.zeroCrossings;
587
+ const normalize = Boolean(kernelState.params.normalize);
588
+ const center = Math.floor(position);
589
+ const start = center - (radius - 1);
590
+ const end = center + radius;
591
+ let numerator = 0;
592
+ let denominator = 0;
593
+ for (let sampleIndex = start; sampleIndex <= end; sampleIndex += 1) {
594
+ const weight = lanczosWeight(position - sampleIndex, radius);
595
+ numerator += readFrameSample(src, srcOffset, numFrames, sampleIndex, channel, kernelState) * weight;
596
+ denominator += weight;
597
+ }
598
+ if (Math.abs(denominator) < 1e-12) return readFrameSample(src, srcOffset, numFrames, Math.round(position), channel, kernelState);
599
+ return normalize ? numerator / denominator : numerator / (denominator || 1);
600
+ };
601
+ lanczosKernel.createState = () => ({
602
+ prevSampleL: 0,
603
+ prevSampleR: 0,
604
+ params: { ...LANCZOS_DEFAULT_PARAMS }
605
+ });
606
+ /** Default Lanczos strategy registration payload. */
607
+ var lanczosStrategy = {
608
+ id: "lanczos",
609
+ baseStrategy: "linear",
610
+ kernel: lanczosKernel,
611
+ defaultParams: LANCZOS_DEFAULT_PARAMS,
612
+ normalizeParams: normalizeLanczosParams,
613
+ applyParams: applyLanczosParams
614
+ };
615
+ //#endregion
616
+ //#region ../core/src/interpolationStrategyRegistry.ts
617
+ var strategyRegistry = /* @__PURE__ */ new Map();
618
+ var activeStrategyId = "lanczos";
619
+ function readStrategySelection(strategy) {
620
+ if (typeof strategy === "string") return { id: strategy };
621
+ if (strategy !== void 0) return strategy;
622
+ return { id: activeStrategyId };
623
+ }
624
+ function readStrategyId(strategy) {
625
+ return readStrategySelection(strategy).id;
626
+ }
627
+ function requireRegisteredStrategy(strategyId) {
628
+ const registered = strategyRegistry.get(strategyId);
629
+ if (registered !== void 0) return registered;
630
+ throw new Error(`Unknown interpolation strategy id "${strategyId}". Register it before use.`);
631
+ }
632
+ function registerBuiltInInterpolationStrategy(registration) {
633
+ const baseStrategy = registration.baseStrategy ?? "lanczos";
634
+ strategyRegistry.set(registration.id, {
635
+ id: registration.id,
636
+ baseStrategy,
637
+ builtIn: true,
638
+ kernel: registration.kernel,
639
+ defaultParams: { ...registration.defaultParams ?? {} },
640
+ normalizeParams: registration.normalizeParams,
641
+ applyParams: registration.applyParams
642
+ });
643
+ }
644
+ function resolveKernelRegistration(registration, visited = /* @__PURE__ */ new Set()) {
645
+ if (registration.kernel !== void 0) return registration;
646
+ if (visited.has(registration.id)) throw new Error(`Interpolation strategy resolution cycle detected at "${registration.id}".`);
647
+ visited.add(registration.id);
648
+ return resolveKernelRegistration(requireRegisteredStrategy(registration.baseStrategy), visited);
649
+ }
650
+ function normalizeParams(registration, params) {
651
+ const defaults = registration.defaultParams;
652
+ if (registration.normalizeParams !== void 0) return registration.normalizeParams(params, defaults);
653
+ const normalized = { ...defaults };
654
+ if (params !== void 0) {
655
+ for (const [key, value] of Object.entries(params)) if (value !== void 0) normalized[key] = value;
656
+ }
657
+ return normalized;
658
+ }
659
+ /**
660
+ * Resolves a strategy to either a built-in base id or a plugin kernel.
661
+ *
662
+ * @throws Error when the strategy id is unknown.
663
+ */
664
+ function resolveInterpolationStrategy(strategy) {
665
+ const registered = requireRegisteredStrategy(readStrategyId(strategy));
666
+ if ("kernel" in registered && registered.kernel) return registered.kernel;
667
+ return registered.baseStrategy;
668
+ }
669
+ /**
670
+ * Resolves runtime strategy state (kernel + normalized params + applier hook).
671
+ */
672
+ function resolveInterpolationStrategyRuntime(strategy) {
673
+ const selection = readStrategySelection(strategy);
674
+ const registered = requireRegisteredStrategy(selection.id);
675
+ const kernelRegistration = resolveKernelRegistration(registered);
676
+ const kernel = kernelRegistration.kernel;
677
+ if (kernel === void 0) throw new Error(`Interpolation strategy "${selection.id}" did not resolve to a kernel.`);
678
+ const params = normalizeParams(registered, selection.params);
679
+ return {
680
+ id: registered.id,
681
+ kernel,
682
+ params,
683
+ applyParams: registered.applyParams ?? kernelRegistration.applyParams
684
+ };
685
+ }
686
+ registerBuiltInInterpolationStrategy({
687
+ ...lanczosStrategy,
688
+ baseStrategy: "lanczos"
689
+ });
690
+ //#endregion
691
+ //#region ../core/src/RateTransposer.ts
692
+ /**
693
+ * Sample rate transposer for pitch and tempo manipulation.
694
+ *
695
+ * @remarks
696
+ * Used internally by SoundTouch for rate-based processing. Applies interpolation strategies to resample audio at different rates, supporting real-time pitch and tempo changes.
697
+ */
698
+ var RateTransposer = class RateTransposer extends AbstractSamplePipe {
699
+ /**
700
+ * Current rate factor for transposition.
701
+ */
702
+ _rate;
703
+ /**
704
+ * Source position (in frames) for the next output sample, relative to the
705
+ * current processing block where 0 is the first frame and -1 is prevSample.
706
+ */
707
+ fractionalPosition;
708
+ /**
709
+ * Previous left channel sample for interpolation.
710
+ */
711
+ previousLeftSample;
712
+ /**
713
+ * Previous right channel sample for interpolation.
714
+ */
715
+ previousRightSample;
716
+ /** Scratch space used for extracted input samples. */
717
+ inputScratch;
718
+ /** Scratch space used for generated output samples. */
719
+ outputScratch;
720
+ /** Factory used when cloning or initializing adapter strategy. */
721
+ sampleBufferAdapterFactory;
722
+ /** Factory used to construct input/output chain buffers. */
723
+ sampleBufferFactory;
724
+ /** Adapter that normalizes reads from the bound input buffer. */
725
+ inputAdapter;
726
+ /** Selected interpolation strategy for transposition. */
727
+ interpolationStrategy;
728
+ /** Resolved kernel used by this transposer instance. */
729
+ resolvedInterpolationKernel;
730
+ /** Optional per-instance state for plugin kernels. */
731
+ kernelState;
732
+ /** Normalized params for the selected interpolation strategy. */
733
+ interpolationStrategyParams;
734
+ /** Optional params application hook from the strategy registration. */
735
+ applyKernelParams;
736
+ /**
737
+ * Creates a RateTransposer instance.
738
+ * @param options Constructor options.
739
+ * @remarks
740
+ * Accepts factories for buffer and adapter creation, and allows specifying the interpolation strategy.
741
+ */
742
+ constructor({ createBuffers = false, sampleBufferAdapterFactory = createCircularSampleBufferAdapter, sampleBufferFactory = () => new CircularSampleBuffer(), interpolationStrategy } = {}) {
743
+ super({
744
+ createBuffers,
745
+ inputBufferFactory: sampleBufferFactory,
746
+ outputBufferFactory: sampleBufferFactory
747
+ });
748
+ this.fractionalPosition = -1;
749
+ this.previousLeftSample = 0;
750
+ this.previousRightSample = 0;
751
+ this._rate = 1;
752
+ this.inputScratch = new Float32Array(0);
753
+ this.outputScratch = new Float32Array(0);
754
+ this.sampleBufferAdapterFactory = sampleBufferAdapterFactory;
755
+ this.sampleBufferFactory = sampleBufferFactory;
756
+ this.inputAdapter = sampleBufferAdapterFactory();
757
+ this.interpolationStrategy = "lanczos";
758
+ this.resolvedInterpolationKernel = () => 0;
759
+ this.kernelState = void 0;
760
+ this.interpolationStrategyParams = {};
761
+ this.applyKernelParams = void 0;
762
+ this.setInterpolationStrategy(interpolationStrategy ?? "lanczos");
763
+ }
764
+ /**
765
+ * Sets the rate factor for transposition.
766
+ * @param rate Rate factor.
767
+ */
768
+ set rate(rate) {
769
+ this._rate = rate;
770
+ }
771
+ /**
772
+ * Active interpolation strategy.
773
+ * @returns The current interpolation strategy identifier.
774
+ */
775
+ get strategy() {
776
+ return this.interpolationStrategy;
777
+ }
778
+ /**
779
+ * Active interpolation strategy params.
780
+ * @returns The current interpolation strategy parameters.
781
+ */
782
+ get strategyParams() {
783
+ return { ...this.interpolationStrategyParams };
784
+ }
785
+ /**
786
+ * Switches interpolation strategy at runtime.
787
+ * @param strategy The new interpolation strategy to use.
788
+ */
789
+ setInterpolationStrategy(strategy) {
790
+ const resolved = resolveInterpolationStrategyRuntime(strategy);
791
+ this.interpolationStrategy = resolved.id;
792
+ this.resolvedInterpolationKernel = resolved.kernel;
793
+ this.interpolationStrategyParams = { ...resolved.params };
794
+ this.applyKernelParams = resolved.applyParams;
795
+ if ("createState" in this.resolvedInterpolationKernel && typeof this.resolvedInterpolationKernel.createState === "function") this.kernelState = this.resolvedInterpolationKernel.createState();
796
+ else this.kernelState = void 0;
797
+ if (this.applyKernelParams !== void 0) this.applyKernelParams(this.kernelState, this.interpolationStrategyParams);
798
+ this.reset();
799
+ }
800
+ /**
801
+ * Applies a partial params update to the current interpolation strategy.
802
+ * @param params Partial set of parameters to update.
803
+ */
804
+ setInterpolationStrategyParams(params) {
805
+ const nextParams = { ...this.interpolationStrategyParams };
806
+ for (const [key, value] of Object.entries(params)) if (value !== void 0) nextParams[key] = value;
807
+ this.interpolationStrategyParams = nextParams;
808
+ if (this.applyKernelParams !== void 0) this.applyKernelParams(this.kernelState, this.interpolationStrategyParams);
809
+ }
810
+ /**
811
+ * Resets internal state for interpolation.
812
+ * @remarks
813
+ * Clears previous sample values and resets the fractional position for output generation.
814
+ */
815
+ reset() {
816
+ this.fractionalPosition = -1;
817
+ this.previousLeftSample = 0;
818
+ this.previousRightSample = 0;
819
+ }
820
+ /**
821
+ * Clears buffers and resets internal state.
822
+ * @remarks
823
+ * Calls clear on all internal buffers and resets interpolation state.
824
+ */
825
+ clear() {
826
+ super.clear();
827
+ this.inputAdapter.clear();
828
+ this.reset();
829
+ }
830
+ /**
831
+ * Creates a clone of this RateTransposer with the same rate.
832
+ * @returns Cloned RateTransposer instance.
833
+ */
834
+ clone() {
835
+ const result = new RateTransposer({
836
+ createBuffers: false,
837
+ sampleBufferAdapterFactory: this.sampleBufferAdapterFactory,
838
+ sampleBufferFactory: this.sampleBufferFactory,
839
+ interpolationStrategy: {
840
+ id: this.interpolationStrategy,
841
+ params: this.interpolationStrategyParams
842
+ }
843
+ });
844
+ result.rate = this._rate;
845
+ return result;
846
+ }
847
+ /**
848
+ * Processes input buffer and writes transposed samples to output buffer.
849
+ * @remarks
850
+ * Reads frames from the input buffer, applies rate transposition, and writes to the output buffer.
851
+ */
852
+ process() {
853
+ if (this._inputBuffer === null || this._outputBuffer === null) return;
854
+ this.inputAdapter.syncFromInputBuffer(this._inputBuffer);
855
+ const numFrames = this.inputAdapter.frameCount;
856
+ if (numFrames === 0) return;
857
+ const numFramesOutput = this.transpose(numFrames);
858
+ this.inputAdapter.receive(numFrames);
859
+ if (numFramesOutput > 0) this._outputBuffer.putSamples(this.outputScratch, 0, numFramesOutput);
860
+ }
861
+ /**
862
+ * Ensures temporary scratch arrays are large enough for the current frame request and estimated output size.
863
+ *
864
+ * @param numInputFrames Number of input frames that will be processed.
865
+ * @remarks
866
+ * Allocates or resizes scratch arrays as needed for efficient processing.
867
+ */
868
+ ensureScratchCapacity(numInputFrames) {
869
+ const inputSamples = numInputFrames * 2;
870
+ if (this.inputScratch.length < inputSamples) this.inputScratch = new Float32Array(inputSamples);
871
+ const estimatedOutputFrames = Math.ceil(numInputFrames / this._rate) + 2;
872
+ const outputSamples = Math.max(0, estimatedOutputFrames) * 2;
873
+ if (this.outputScratch.length < outputSamples) this.outputScratch = new Float32Array(outputSamples);
874
+ }
875
+ /**
876
+ * Transposes input samples by the current rate.
877
+ * @param numFrames Number of input frames to transpose.
878
+ * @returns Number of output frames written.
879
+ * @remarks
880
+ * Applies the selected interpolation kernel to generate output samples at the new rate.
881
+ */
882
+ transpose(numFrames = 0) {
883
+ if (this._inputBuffer !== null) {
884
+ this.inputAdapter.syncFromInputBuffer(this._inputBuffer);
885
+ if (numFrames === 0) numFrames = this.inputAdapter.frameCount;
886
+ }
887
+ if (numFrames === 0) return 0;
888
+ this.ensureScratchCapacity(numFrames);
889
+ const src = this.inputScratch;
890
+ const extractedFrames = this.inputAdapter.extract(src, 0, numFrames);
891
+ if (extractedFrames === 0) return 0;
892
+ numFrames = extractedFrames;
893
+ return this.transposePluginKernel(numFrames);
894
+ }
895
+ /**
896
+ * Handles transposition using a plugin kernel.
897
+ * @remarks
898
+ * Invokes the selected interpolation kernel for each output sample.
899
+ */
900
+ transposePluginKernel(numFrames) {
901
+ const src = this.inputScratch;
902
+ const dest = this.outputScratch;
903
+ const srcOffset = 0;
904
+ const destOffset = 0;
905
+ const kernel = this.resolvedInterpolationKernel;
906
+ const state = this.kernelState;
907
+ const stateRecord = this.getKernelStateRecord(state);
908
+ if (stateRecord !== void 0) {
909
+ stateRecord.prevSampleL = this.previousLeftSample;
910
+ stateRecord.prevSampleR = this.previousRightSample;
911
+ }
912
+ let i = 0;
913
+ let position = this.fractionalPosition;
914
+ const maxPosition = numFrames - 1;
915
+ while (position <= maxPosition) {
916
+ dest[destOffset + 2 * i] = kernel(src, srcOffset, numFrames, position, 0, state);
917
+ dest[destOffset + 2 * i + 1] = kernel(src, srcOffset, numFrames, position, 1, state);
918
+ i = i + 1;
919
+ position += this._rate;
920
+ }
921
+ this.fractionalPosition = position - numFrames;
922
+ this.previousLeftSample = src[srcOffset + 2 * numFrames - 2];
923
+ this.previousRightSample = src[srcOffset + 2 * numFrames - 1];
924
+ if (stateRecord !== void 0) {
925
+ stateRecord.prevSampleL = this.previousLeftSample;
926
+ stateRecord.prevSampleR = this.previousRightSample;
927
+ }
928
+ return i;
929
+ }
930
+ /**
931
+ * Returns the kernel state record if available.
932
+ * @param state The kernel state object.
933
+ * @returns The state record with previous sample values, or undefined if not present.
934
+ */
935
+ getKernelStateRecord(state) {
936
+ if (typeof state !== "object" || state === null) return;
937
+ const record = state;
938
+ const prevSampleL = record["prevSampleL"];
939
+ const prevSampleR = record["prevSampleR"];
940
+ if (typeof prevSampleL === "number" && typeof prevSampleR === "number") return record;
941
+ }
942
+ };
943
+ //#endregion
944
+ //#region ../core/src/Stretch.ts
945
+ /**
946
+ * Read adapter optimized for FIFO-backed buffers with a generic fallback path.
947
+ */
948
+ var FifoStretchBufferAdapter = class {
949
+ buffer;
950
+ fallbackBuffer;
951
+ fallbackScratch;
952
+ constructor() {
953
+ this.buffer = null;
954
+ this.fallbackBuffer = new FifoSampleBuffer();
955
+ this.fallbackScratch = new Float32Array(0);
956
+ }
957
+ /**
958
+ * @param buffer Source buffer to expose through FIFO-style reads.
959
+ */
960
+ setBuffer(buffer) {
961
+ if (buffer instanceof FifoSampleBuffer) {
962
+ this.buffer = buffer;
963
+ return;
964
+ }
965
+ const frameCount = buffer.frameCount;
966
+ if (frameCount > 0) {
967
+ const sampleCount = frameCount * 2;
968
+ if (this.fallbackScratch.length < sampleCount) this.fallbackScratch = new Float32Array(sampleCount);
969
+ buffer.extract(this.fallbackScratch, 0, frameCount);
970
+ this.fallbackBuffer.clear();
971
+ this.fallbackBuffer.putSamples(this.fallbackScratch, 0, frameCount);
972
+ buffer.receive(frameCount);
973
+ } else this.fallbackBuffer.clear();
974
+ this.buffer = this.fallbackBuffer;
975
+ }
976
+ /**
977
+ * Returns the currently bound FIFO buffer.
978
+ * @throws Error when `setBuffer` has not been called yet.
979
+ */
980
+ getBoundBuffer() {
981
+ if (this.buffer === null) throw new Error("buffer is not set");
982
+ return this.buffer;
983
+ }
984
+ get frameCount() {
985
+ return this.getBoundBuffer().frameCount;
986
+ }
987
+ get startIndex() {
988
+ return this.getBoundBuffer().startIndex;
989
+ }
990
+ readSample(sampleIndex) {
991
+ const boundBuffer = this.getBoundBuffer();
992
+ const start = boundBuffer.startIndex;
993
+ const end = start + boundBuffer.frameCount * 2;
994
+ if (sampleIndex < start || sampleIndex >= end) return 0;
995
+ return boundBuffer.vector[sampleIndex];
996
+ }
997
+ readSubarray(start, end) {
998
+ return this.getBoundBuffer().vector.subarray(start, end);
999
+ }
1000
+ receive(numFrames) {
1001
+ this.getBoundBuffer().receive(numFrames);
1002
+ }
1003
+ receiveSamples(output, numFrames) {
1004
+ this.getBoundBuffer().receiveSamples(output, numFrames);
1005
+ }
1006
+ };
1007
+ var GenericStretchWriteBufferAdapter = class {
1008
+ buffer;
1009
+ constructor() {
1010
+ this.buffer = null;
1011
+ }
1012
+ setOutputBuffer(buffer) {
1013
+ this.buffer = buffer;
1014
+ }
1015
+ /**
1016
+ * Returns the currently bound output buffer.
1017
+ * @throws Error when `setOutputBuffer` has not been called.
1018
+ */
1019
+ getBoundBuffer() {
1020
+ if (this.buffer === null) throw new Error("output buffer is not set");
1021
+ return this.buffer;
1022
+ }
1023
+ appendSamples(samples, numFrames) {
1024
+ this.getBoundBuffer().putSamples(samples, 0, numFrames);
1025
+ }
1026
+ putFrom(source, position, numFrames) {
1027
+ const sourceStart = source.startIndex + position * 2;
1028
+ const sourceEnd = sourceStart + numFrames * 2;
1029
+ const chunk = source.readSubarray(sourceStart, sourceEnd);
1030
+ this.getBoundBuffer().putSamples(chunk, 0, numFrames);
1031
+ }
1032
+ };
1033
+ var CircularStretchInputBufferAdapter = class {
1034
+ circularBuffer;
1035
+ rangeScratch;
1036
+ constructor() {
1037
+ this.circularBuffer = new CircularSampleBuffer();
1038
+ this.rangeScratch = new Float32Array(0);
1039
+ }
1040
+ /**
1041
+ * Binds a source buffer and stages its readable frames into the internal
1042
+ * circular storage.
1043
+ *
1044
+ * @param buffer Source buffer to import.
1045
+ */
1046
+ setBuffer(buffer) {
1047
+ if (buffer instanceof FifoSampleBuffer) {
1048
+ const frames = buffer.frameCount;
1049
+ if (frames > 0) {
1050
+ this.circularBuffer.pushSamples(buffer.vector, buffer.position, frames);
1051
+ buffer.receive(frames);
1052
+ }
1053
+ return;
1054
+ }
1055
+ const frames = buffer.frameCount;
1056
+ if (frames > 0) {
1057
+ const sampleCount = frames * 2;
1058
+ if (this.rangeScratch.length < sampleCount) this.rangeScratch = new Float32Array(sampleCount);
1059
+ buffer.extract(this.rangeScratch, 0, frames);
1060
+ this.circularBuffer.pushSamples(this.rangeScratch, 0, frames);
1061
+ buffer.receive(frames);
1062
+ }
1063
+ }
1064
+ get frameCount() {
1065
+ return this.circularBuffer.frameCount;
1066
+ }
1067
+ get startIndex() {
1068
+ return 0;
1069
+ }
1070
+ readSample(sampleIndex) {
1071
+ return this.circularBuffer.readSample(sampleIndex);
1072
+ }
1073
+ /**
1074
+ * Returns a contiguous range from circular storage, padding trailing values
1075
+ * with zeros when the requested range extends past available data.
1076
+ */
1077
+ readSubarray(start, end) {
1078
+ const normalizedStart = Math.max(0, Math.floor(start));
1079
+ const requestedSamples = Math.max(normalizedStart, Math.floor(end)) - normalizedStart;
1080
+ const requestedFrames = Math.floor(requestedSamples / 2);
1081
+ if (requestedFrames <= 0) return this.rangeScratch.subarray(0, 0);
1082
+ const needed = requestedFrames * 2;
1083
+ if (this.rangeScratch.length < needed) this.rangeScratch = new Float32Array(needed);
1084
+ const sourceFrameOffset = Math.floor(normalizedStart / 2);
1085
+ const readSamples = this.circularBuffer.extract(this.rangeScratch, sourceFrameOffset, requestedFrames, false) * 2;
1086
+ if (readSamples < needed) this.rangeScratch.fill(0, readSamples, needed);
1087
+ return this.rangeScratch.subarray(0, needed);
1088
+ }
1089
+ receive(numFrames) {
1090
+ this.circularBuffer.dropFrames(numFrames);
1091
+ }
1092
+ receiveSamples(output, numFrames) {
1093
+ this.circularBuffer.extract(output, 0, numFrames, true);
1094
+ }
1095
+ };
1096
+ /**
1097
+ * Creates a stretch input adapter that reads from FIFO-compatible buffers.
1098
+ */
1099
+ var createFifoStretchInputBufferAdapter = () => new FifoStretchBufferAdapter();
1100
+ /**
1101
+ * Creates a stretch input adapter backed by `CircularSampleBuffer`.
1102
+ */
1103
+ var createCircularStretchInputBufferAdapter = () => new CircularStretchInputBufferAdapter();
1104
+ var DEFAULT_SEQUENCE_MS = 0;
1105
+ var DEFAULT_SEEKWINDOW_MS = 0;
1106
+ var DEFAULT_OVERLAP_MS = 8;
1107
+ var AUTOSEQ_TEMPO_LOW = .25;
1108
+ var AUTOSEQ_TEMPO_TOP = 4;
1109
+ var AUTOSEQ_AT_MIN = 125;
1110
+ var AUTOSEQ_AT_MAX = 50;
1111
+ var AUTOSEQ_K = (AUTOSEQ_AT_MAX - AUTOSEQ_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW);
1112
+ var AUTOSEQ_C = AUTOSEQ_AT_MIN - AUTOSEQ_K * AUTOSEQ_TEMPO_LOW;
1113
+ var AUTOSEEK_AT_MIN = 25;
1114
+ var AUTOSEEK_AT_MAX = 15;
1115
+ var AUTOSEEK_K = (AUTOSEEK_AT_MAX - AUTOSEEK_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW);
1116
+ var AUTOSEEK_C = AUTOSEEK_AT_MIN - AUTOSEEK_K * AUTOSEQ_TEMPO_LOW;
1117
+ var NORMALIZED_CORRELATION_EPSILON = 1e-12;
1118
+ var QUICK_SEEK_FALLBACK_THRESHOLD = 256;
1119
+ var QUICK_SEEK_MIN_VALID_CANDIDATES = 8;
1120
+ /**
1121
+ * Time-stretch processor for tempo adjustment without affecting pitch.
1122
+ * Used internally by SoundTouch for time-stretching audio.
1123
+ */
1124
+ var Stretch = class Stretch extends AbstractSamplePipe {
1125
+ inputBufferAdapterFactory;
1126
+ sampleBufferFactory;
1127
+ inputBufferAdapter;
1128
+ outputBufferAdapter;
1129
+ overlapScratch;
1130
+ _quickSeek;
1131
+ midBufferDirty;
1132
+ midBuffer;
1133
+ refMidBuffer;
1134
+ refMidBufferEnergy;
1135
+ overlapLength;
1136
+ autoSeqSetting;
1137
+ autoSeekSetting;
1138
+ _tempo;
1139
+ sampleRate;
1140
+ _overlapMs;
1141
+ sequenceMs;
1142
+ seekWindowMs;
1143
+ seekWindowLength;
1144
+ seekLength;
1145
+ nominalSkip;
1146
+ skipFract;
1147
+ sampleReq;
1148
+ /**
1149
+ * Creates a Stretch instance.
1150
+ * @param options Constructor options.
1151
+ */
1152
+ constructor({ sampleRate = 44100, createBuffers = false, inputBufferAdapterFactory = createFifoStretchInputBufferAdapter, sampleBufferFactory = () => new FifoSampleBuffer() } = {}) {
1153
+ super({
1154
+ createBuffers,
1155
+ inputBufferFactory: sampleBufferFactory,
1156
+ outputBufferFactory: sampleBufferFactory
1157
+ });
1158
+ this.inputBufferAdapterFactory = inputBufferAdapterFactory;
1159
+ this.sampleBufferFactory = sampleBufferFactory;
1160
+ this.inputBufferAdapter = inputBufferAdapterFactory();
1161
+ this.outputBufferAdapter = new GenericStretchWriteBufferAdapter();
1162
+ this.overlapScratch = new Float32Array(0);
1163
+ this._quickSeek = true;
1164
+ this.midBufferDirty = true;
1165
+ this.midBuffer = null;
1166
+ this.refMidBufferEnergy = 0;
1167
+ this.overlapLength = 0;
1168
+ this.autoSeqSetting = true;
1169
+ this.autoSeekSetting = true;
1170
+ this._tempo = 1;
1171
+ this.setParameters(sampleRate, DEFAULT_SEQUENCE_MS, DEFAULT_SEEKWINDOW_MS, DEFAULT_OVERLAP_MS);
1172
+ }
1173
+ clear() {
1174
+ super.clear();
1175
+ this.clearMidBuffer();
1176
+ }
1177
+ clearMidBuffer() {
1178
+ this.midBufferDirty = true;
1179
+ if (this.midBuffer) this.midBuffer.fill(0);
1180
+ if (this.refMidBuffer) this.refMidBuffer.fill(0);
1181
+ this.skipFract = 0;
1182
+ }
1183
+ setParameters(sampleRate, sequenceMs, seekWindowMs, overlapMs) {
1184
+ if (sampleRate > 0) this.sampleRate = sampleRate;
1185
+ if (overlapMs > 0) this._overlapMs = overlapMs;
1186
+ if (sequenceMs > 0) {
1187
+ this.sequenceMs = sequenceMs;
1188
+ this.autoSeqSetting = false;
1189
+ } else this.autoSeqSetting = true;
1190
+ if (seekWindowMs > 0) {
1191
+ this.seekWindowMs = seekWindowMs;
1192
+ this.autoSeekSetting = false;
1193
+ } else this.autoSeekSetting = true;
1194
+ this.calculateSequenceParameters();
1195
+ this.calculateOverlapLength(this._overlapMs);
1196
+ this.updateTempoDerivedState();
1197
+ }
1198
+ set tempo(newTempo) {
1199
+ this._tempo = newTempo;
1200
+ this.updateTempoDerivedState();
1201
+ }
1202
+ get tempo() {
1203
+ return this._tempo;
1204
+ }
1205
+ get inputChunkSize() {
1206
+ return this.sampleReq;
1207
+ }
1208
+ get outputChunkSize() {
1209
+ return this.overlapLength + Math.max(0, this.seekWindowLength - 2 * this.overlapLength);
1210
+ }
1211
+ calculateOverlapLength(overlapInMsec = 0) {
1212
+ let newOvl = this.sampleRate * overlapInMsec / 1e3;
1213
+ newOvl = newOvl < 16 ? 16 : newOvl;
1214
+ newOvl -= newOvl % 8;
1215
+ if (newOvl === this.overlapLength && this.midBuffer !== null) return;
1216
+ this.overlapLength = newOvl;
1217
+ const needed = this.overlapLength * 2;
1218
+ if (!this.refMidBuffer || this.refMidBuffer.length < needed) this.refMidBuffer = new Float32Array(needed);
1219
+ if (!this.midBuffer || this.midBuffer.length < needed) this.midBuffer = new Float32Array(needed);
1220
+ }
1221
+ checkLimits(x, mi, ma) {
1222
+ return x < mi ? mi : x > ma ? ma : x;
1223
+ }
1224
+ calculateSequenceParameters() {
1225
+ if (this.autoSeqSetting) {
1226
+ let seq = AUTOSEQ_C + AUTOSEQ_K * this._tempo;
1227
+ seq = this.checkLimits(seq, AUTOSEQ_AT_MAX, AUTOSEQ_AT_MIN);
1228
+ this.sequenceMs = Math.floor(seq + .5);
1229
+ }
1230
+ if (this.autoSeekSetting) {
1231
+ let seek = AUTOSEEK_C + AUTOSEEK_K * this._tempo;
1232
+ seek = this.checkLimits(seek, AUTOSEEK_AT_MAX, AUTOSEEK_AT_MIN);
1233
+ this.seekWindowMs = Math.floor(seek + .5);
1234
+ }
1235
+ this.seekWindowLength = Math.floor(this.sampleRate * this.sequenceMs / 1e3);
1236
+ this.seekLength = Math.floor(this.sampleRate * this.seekWindowMs / 1e3);
1237
+ this.normalizeWindowInvariants();
1238
+ }
1239
+ normalizeWindowInvariants() {
1240
+ this.seekLength = Math.max(1, this.seekLength);
1241
+ this.seekWindowLength = Math.max(this.seekWindowLength, this.overlapLength);
1242
+ }
1243
+ updateTempoDerivedState() {
1244
+ this.calculateSequenceParameters();
1245
+ this.nominalSkip = this._tempo * (this.seekWindowLength - this.overlapLength);
1246
+ this.skipFract = 0;
1247
+ const intskip = Math.floor(this.nominalSkip + .5);
1248
+ this.sampleReq = Math.max(intskip + this.overlapLength, this.seekWindowLength) + this.seekLength;
1249
+ }
1250
+ /**
1251
+ * Whether the fast multi-pass seek algorithm is active.
1252
+ * @returns `true` if quick seek is enabled (default); `false` for exhaustive search.
1253
+ */
1254
+ get quickSeek() {
1255
+ return this._quickSeek;
1256
+ }
1257
+ set quickSeek(enable) {
1258
+ this._quickSeek = enable;
1259
+ }
1260
+ /**
1261
+ * Current overlap crossfade length in milliseconds.
1262
+ * @returns The overlap period used at the current sample rate.
1263
+ */
1264
+ get overlapMs() {
1265
+ return this._overlapMs;
1266
+ }
1267
+ /**
1268
+ * Sets the overlap crossfade length and recalculates derived parameters.
1269
+ * @param ms Overlap period in milliseconds (must be > 0).
1270
+ */
1271
+ set overlapMs(ms) {
1272
+ if (ms > 0) {
1273
+ this._overlapMs = ms;
1274
+ this.calculateOverlapLength(this._overlapMs);
1275
+ this.calculateSequenceParameters();
1276
+ this.updateTempoDerivedState();
1277
+ }
1278
+ }
1279
+ /**
1280
+ * Applies a partial set of WSOLA timing parameters.
1281
+ *
1282
+ * @remarks
1283
+ * Only the provided fields are updated; omitted fields remain unchanged.
1284
+ * Pass `sequenceMs: 0` or `seekWindowMs: 0` to switch that dimension back to auto-calculation.
1285
+ *
1286
+ * @param params Partial set of WSOLA timing parameters to apply.
1287
+ *
1288
+ * @example
1289
+ * stretch.setStretchParameters({ overlapMs: 12, quickSeek: false });
1290
+ */
1291
+ setStretchParameters(params) {
1292
+ if (params.quickSeek !== void 0) this._quickSeek = params.quickSeek;
1293
+ let needsRecalc = false;
1294
+ if (params.sequenceMs !== void 0) {
1295
+ if (params.sequenceMs > 0) {
1296
+ this.sequenceMs = params.sequenceMs;
1297
+ this.autoSeqSetting = false;
1298
+ } else this.autoSeqSetting = true;
1299
+ needsRecalc = true;
1300
+ }
1301
+ if (params.seekWindowMs !== void 0) {
1302
+ if (params.seekWindowMs > 0) {
1303
+ this.seekWindowMs = params.seekWindowMs;
1304
+ this.autoSeekSetting = false;
1305
+ } else this.autoSeekSetting = true;
1306
+ needsRecalc = true;
1307
+ }
1308
+ if (params.overlapMs !== void 0 && params.overlapMs > 0) {
1309
+ this._overlapMs = params.overlapMs;
1310
+ this.calculateOverlapLength(this._overlapMs);
1311
+ needsRecalc = true;
1312
+ }
1313
+ if (needsRecalc) {
1314
+ this.calculateSequenceParameters();
1315
+ this.updateTempoDerivedState();
1316
+ }
1317
+ }
1318
+ clone() {
1319
+ const result = new Stretch({
1320
+ createBuffers: false,
1321
+ inputBufferAdapterFactory: this.inputBufferAdapterFactory,
1322
+ sampleBufferFactory: this.sampleBufferFactory
1323
+ });
1324
+ result.tempo = this._tempo;
1325
+ result.setParameters(this.sampleRate, this.sequenceMs, this.seekWindowMs, this._overlapMs);
1326
+ return result;
1327
+ }
1328
+ seekBestOverlapPosition(inputBuffer) {
1329
+ const resolvedInputBuffer = inputBuffer ?? this.getInputBufferAdapter();
1330
+ if (!this._quickSeek || this.seekLength <= QUICK_SEEK_FALLBACK_THRESHOLD) return this.seekBestOverlapPositionStereo(resolvedInputBuffer);
1331
+ return this.seekBestOverlapPositionStereoQuick(resolvedInputBuffer);
1332
+ }
1333
+ seekBestOverlapPositionStereo(inputBuffer) {
1334
+ let bestOffset;
1335
+ let bestCorrelation;
1336
+ let correlation;
1337
+ this.preCalculateCorrelationReferenceStereo();
1338
+ bestOffset = 0;
1339
+ bestCorrelation = -Infinity;
1340
+ for (let i = 0; i < this.seekLength; i++) {
1341
+ correlation = this.calculateCrossCorrelationStereo(2 * i, this.refMidBuffer, inputBuffer);
1342
+ if (correlation > bestCorrelation) {
1343
+ bestCorrelation = correlation;
1344
+ bestOffset = i;
1345
+ }
1346
+ }
1347
+ return bestOffset;
1348
+ }
1349
+ seekBestOverlapPositionStereoQuick(inputBuffer) {
1350
+ let bestOffset;
1351
+ let bestCorrelation;
1352
+ let correlation;
1353
+ let correlationOffset;
1354
+ let tempOffset;
1355
+ let evaluatedCandidates;
1356
+ this.preCalculateCorrelationReferenceStereo();
1357
+ bestCorrelation = this.calculateCrossCorrelationStereo(0, this.refMidBuffer, inputBuffer);
1358
+ evaluatedCandidates = 1;
1359
+ bestOffset = 0;
1360
+ correlationOffset = 0;
1361
+ for (let scanCount = 0; scanCount < 4; scanCount++) {
1362
+ let previousTempOffset = Number.MIN_SAFE_INTEGER;
1363
+ const scanOffsets = this.getQuickScanOffsets(scanCount);
1364
+ for (const scanOffset of scanOffsets) {
1365
+ tempOffset = correlationOffset + scanOffset;
1366
+ if (tempOffset === previousTempOffset) continue;
1367
+ previousTempOffset = tempOffset;
1368
+ if (tempOffset < 0) continue;
1369
+ if (tempOffset >= this.seekLength) continue;
1370
+ correlation = this.calculateCrossCorrelationStereo(2 * tempOffset, this.refMidBuffer, inputBuffer);
1371
+ evaluatedCandidates++;
1372
+ if (correlation > bestCorrelation) {
1373
+ bestCorrelation = correlation;
1374
+ bestOffset = tempOffset;
1375
+ }
1376
+ }
1377
+ correlationOffset = bestOffset;
1378
+ }
1379
+ if (evaluatedCandidates < QUICK_SEEK_MIN_VALID_CANDIDATES) return this.seekBestOverlapPositionStereo(inputBuffer);
1380
+ return bestOffset;
1381
+ }
1382
+ getQuickScanOffsets(stage) {
1383
+ const maxOffset = Math.max(1, this.seekLength - 1);
1384
+ if (stage === 0) return this.generateFractionalScanOffsets(maxOffset, 2, 1, 14, 24);
1385
+ if (stage === 1) return this.generateSymmetricScanOffsets(maxOffset, .2);
1386
+ if (stage === 2) return this.generateSymmetricScanOffsets(maxOffset, .06);
1387
+ return this.generateSymmetricScanOffsets(maxOffset, .015);
1388
+ }
1389
+ generateFractionalScanOffsets(maxOffset, startNumerator, stepNumerator, denominator, steps) {
1390
+ const offsets = [];
1391
+ const seen = /* @__PURE__ */ new Set();
1392
+ const safeDenominator = Math.max(1, denominator);
1393
+ const safeSteps = Math.max(1, steps);
1394
+ for (let i = 0; i < safeSteps; i++) {
1395
+ const numerator = startNumerator + i * stepNumerator;
1396
+ const value = Math.round(maxOffset * numerator / safeDenominator);
1397
+ if (value <= 0 || value >= this.seekLength || seen.has(value)) continue;
1398
+ seen.add(value);
1399
+ offsets.push(value);
1400
+ }
1401
+ return offsets;
1402
+ }
1403
+ generateSymmetricScanOffsets(maxOffset, spanRatio) {
1404
+ const span = Math.max(1, Math.round(maxOffset * spanRatio));
1405
+ const scales = [
1406
+ 1,
1407
+ .75,
1408
+ .5,
1409
+ .25
1410
+ ];
1411
+ const negative = [];
1412
+ const positive = [];
1413
+ const seen = /* @__PURE__ */ new Set();
1414
+ for (const scale of scales) {
1415
+ const magnitude = Math.max(1, Math.round(span * scale));
1416
+ const neg = -magnitude;
1417
+ const pos = magnitude;
1418
+ if (!seen.has(neg)) {
1419
+ seen.add(neg);
1420
+ negative.push(neg);
1421
+ }
1422
+ if (!seen.has(pos)) {
1423
+ seen.add(pos);
1424
+ positive.push(pos);
1425
+ }
1426
+ }
1427
+ return negative.concat(positive);
1428
+ }
1429
+ preCalculateCorrelationReferenceStereo() {
1430
+ let energy = 0;
1431
+ for (let i = 0; i < this.overlapLength; i++) {
1432
+ const temp = i * (this.overlapLength - i);
1433
+ const ctx = i * 2;
1434
+ const left = this.midBuffer[ctx] * temp;
1435
+ const right = this.midBuffer[ctx + 1] * temp;
1436
+ this.refMidBuffer[ctx] = left;
1437
+ this.refMidBuffer[ctx + 1] = right;
1438
+ energy += left * left + right * right;
1439
+ }
1440
+ this.refMidBufferEnergy = energy;
1441
+ }
1442
+ calculateCrossCorrelationStereo(mixingPos, compare, inputBuffer) {
1443
+ mixingPos += inputBuffer.startIndex;
1444
+ let dot = 0;
1445
+ let sourceEnergy = 0;
1446
+ const calcLength = 2 * this.overlapLength;
1447
+ const source = inputBuffer.readSubarray(mixingPos, mixingPos + calcLength);
1448
+ for (let i = 0; i < calcLength; i += 2) {
1449
+ const sourceLeft = i < source.length ? source[i] : 0;
1450
+ const sourceRight = i + 1 < source.length ? source[i + 1] : 0;
1451
+ const compareLeft = compare[i];
1452
+ const compareRight = compare[i + 1];
1453
+ dot += sourceLeft * compareLeft + sourceRight * compareRight;
1454
+ sourceEnergy += sourceLeft * sourceLeft + sourceRight * sourceRight;
1455
+ }
1456
+ if (sourceEnergy <= NORMALIZED_CORRELATION_EPSILON || this.refMidBufferEnergy <= NORMALIZED_CORRELATION_EPSILON) return -1;
1457
+ return dot / Math.sqrt(sourceEnergy * this.refMidBufferEnergy);
1458
+ }
1459
+ overlapStereo(inputPosition, inputBuffer, outputBuffer) {
1460
+ inputPosition += inputBuffer.startIndex;
1461
+ const overlapSamples = this.overlapLength * 2;
1462
+ if (this.overlapScratch.length < overlapSamples) this.overlapScratch = new Float32Array(overlapSamples);
1463
+ const output = this.overlapScratch;
1464
+ const input = inputBuffer.readSubarray(inputPosition, inputPosition + overlapSamples);
1465
+ const frameScale = 1 / this.overlapLength;
1466
+ for (let i = 0; i < this.overlapLength; i++) {
1467
+ const tempFrame = (this.overlapLength - i) * frameScale;
1468
+ const fi = i * frameScale;
1469
+ const ctx = 2 * i;
1470
+ const inputLeft = ctx < input.length ? input[ctx] : 0;
1471
+ const inputRight = ctx + 1 < input.length ? input[ctx + 1] : 0;
1472
+ output[ctx] = inputLeft * fi + this.midBuffer[ctx] * tempFrame;
1473
+ output[ctx + 1] = inputRight * fi + this.midBuffer[ctx + 1] * tempFrame;
1474
+ }
1475
+ outputBuffer.appendSamples(output, this.overlapLength);
1476
+ }
1477
+ process() {
1478
+ const inputBuffer = this.getInputBufferAdapter();
1479
+ const outputBuffer = this.getOutputBufferAdapter();
1480
+ if (!this.bootstrapMidBuffer(inputBuffer)) return;
1481
+ while (inputBuffer.frameCount >= this.sampleReq) this.processOneWindow(inputBuffer, outputBuffer);
1482
+ }
1483
+ bootstrapMidBuffer(inputBuffer) {
1484
+ if (!this.midBufferDirty) return true;
1485
+ if (inputBuffer.frameCount < this.overlapLength) return false;
1486
+ const needed = this.overlapLength * 2;
1487
+ if (!this.midBuffer || this.midBuffer.length < needed) this.midBuffer = new Float32Array(needed);
1488
+ inputBuffer.receiveSamples(this.midBuffer, this.overlapLength);
1489
+ this.midBufferDirty = false;
1490
+ return true;
1491
+ }
1492
+ processOneWindow(inputBuffer, outputBuffer) {
1493
+ const offset = this.seekBestOverlapPosition(inputBuffer);
1494
+ this.overlapStereo(2 * Math.floor(offset), inputBuffer, outputBuffer);
1495
+ const middleFrames = this.seekWindowLength - 2 * this.overlapLength;
1496
+ if (middleFrames > 0) outputBuffer.putFrom(inputBuffer, offset + this.overlapLength, middleFrames);
1497
+ this.captureOverlapHistory(offset, inputBuffer);
1498
+ this.advanceInputByNominalSkip(inputBuffer);
1499
+ }
1500
+ captureOverlapHistory(offset, inputBuffer) {
1501
+ const start = inputBuffer.startIndex + 2 * (offset + this.seekWindowLength - this.overlapLength);
1502
+ this.midBuffer.set(inputBuffer.readSubarray(start, start + 2 * this.overlapLength));
1503
+ }
1504
+ advanceInputByNominalSkip(inputBuffer) {
1505
+ this.skipFract += this.nominalSkip;
1506
+ const overlapSkip = Math.floor(this.skipFract);
1507
+ this.skipFract -= overlapSkip;
1508
+ inputBuffer.receive(overlapSkip);
1509
+ }
1510
+ getInputBufferAdapter() {
1511
+ if (this._inputBuffer === null) throw new Error("inputBuffer is not set");
1512
+ this.inputBufferAdapter.setBuffer(this._inputBuffer);
1513
+ return this.inputBufferAdapter;
1514
+ }
1515
+ getOutputBufferAdapter() {
1516
+ if (this._outputBuffer === null) throw new Error("outputBuffer is not set");
1517
+ this.outputBufferAdapter.setOutputBuffer(this._outputBuffer);
1518
+ return this.outputBufferAdapter;
1519
+ }
1520
+ };
1521
+ //#endregion
1522
+ //#region ../core/src/testFloatEqual.ts
1523
+ /**
1524
+ * Tests whether two floating-point numbers differ beyond a fixed epsilon.
1525
+ *
1526
+ * @param a First number to compare.
1527
+ * @param b Second number to compare.
1528
+ * @returns True when the numbers differ by more than epsilon, false otherwise.
1529
+ * @remarks
1530
+ * Uses a fixed epsilon threshold to determine significant difference.
1531
+ */
1532
+ function isFloatDifferent(a, b) {
1533
+ return (a > b ? a - b : b - a) > 1e-10;
1534
+ }
1535
+ //#endregion
1536
+ //#region ../core/src/SoundTouch.ts
1537
+ /**
1538
+ * Main processing engine for pitch shifting and time-stretching.
1539
+ *
1540
+ * @remarks
1541
+ * Chains a `RateTransposer` and `Stretch` stage to deliver real-time pitch manipulation
1542
+ * without affecting playback tempo. Set `pitch`, `pitchOctaves`, or `pitchSemitones` to
1543
+ * control the output. The internal `_rate` and `_tempo` pipeline values are derived
1544
+ * automatically from `virtualPitch`.
1545
+ */
1546
+ var SoundTouch = class SoundTouch {
1547
+ transposer;
1548
+ stretch;
1549
+ _sampleRate;
1550
+ _sampleBufferType;
1551
+ _sampleBufferFactory;
1552
+ _interpolationStrategy;
1553
+ _inputBuffer;
1554
+ _intermediateBuffer;
1555
+ _outputBuffer;
1556
+ _rate;
1557
+ _tempo;
1558
+ /** Current pitch multiplier. Updated by the `pitch`, `pitchOctaves`, and `pitchSemitones` setters. */
1559
+ virtualPitch;
1560
+ /**
1561
+ * Creates a new SoundTouch processor instance.
1562
+ * @param options Construction options for sample rate, buffer strategy, and factories.
1563
+ */
1564
+ constructor(options = {}) {
1565
+ this._sampleBufferType = options.sampleBufferType ?? "circular";
1566
+ this._sampleBufferFactory = options.sampleBufferFactory ?? (this._sampleBufferType === "fifo" ? () => new FifoSampleBuffer() : () => new CircularSampleBuffer());
1567
+ this.transposer = new RateTransposer({
1568
+ createBuffers: false,
1569
+ sampleBufferAdapterFactory: this._sampleBufferType === "circular" ? createCircularSampleBufferAdapter : createFifoSampleBufferAdapter,
1570
+ sampleBufferFactory: this._sampleBufferFactory,
1571
+ interpolationStrategy: options.interpolationStrategy
1572
+ });
1573
+ this._interpolationStrategy = this.transposer.strategy;
1574
+ this._sampleRate = options.sampleRate ?? 44100;
1575
+ if (options.stretchFactory) this.stretch = options.stretchFactory(this._sampleRate, {
1576
+ sampleBufferFactory: this._sampleBufferFactory,
1577
+ sampleBufferType: this._sampleBufferType
1578
+ });
1579
+ else this.stretch = new Stretch({
1580
+ sampleRate: this._sampleRate,
1581
+ createBuffers: false,
1582
+ inputBufferAdapterFactory: this._sampleBufferType === "circular" ? createCircularStretchInputBufferAdapter : createFifoStretchInputBufferAdapter,
1583
+ sampleBufferFactory: this._sampleBufferFactory
1584
+ });
1585
+ this.stretch.setParameters(this._sampleRate, 0, 0, 0);
1586
+ this._inputBuffer = this._sampleBufferFactory();
1587
+ this._intermediateBuffer = this._sampleBufferFactory();
1588
+ this._outputBuffer = this._sampleBufferFactory();
1589
+ this._rate = 0;
1590
+ this._tempo = 0;
1591
+ this.virtualPitch = 1;
1592
+ this.calculateEffectiveRateAndTempo();
1593
+ }
1594
+ /**
1595
+ * Clears both processing stages and their internal buffers.
1596
+ * @remarks
1597
+ * Resets the state of the transposer and stretch stages, including all internal buffers.
1598
+ */
1599
+ clear() {
1600
+ this.transposer.clear();
1601
+ this.stretch.clear();
1602
+ }
1603
+ /**
1604
+ * Creates an independent copy with equivalent runtime configuration.
1605
+ */
1606
+ clone() {
1607
+ const result = new SoundTouch({
1608
+ sampleRate: this._sampleRate,
1609
+ sampleBufferType: this._sampleBufferType,
1610
+ sampleBufferFactory: this._sampleBufferFactory,
1611
+ interpolationStrategy: {
1612
+ id: this._interpolationStrategy,
1613
+ params: this.transposer.strategyParams
1614
+ }
1615
+ });
1616
+ result.pitch = this.virtualPitch;
1617
+ return result;
1618
+ }
1619
+ /**
1620
+ * Active interpolation strategy id used by the transposer stage.
1621
+ * @returns The current interpolation strategy identifier.
1622
+ */
1623
+ get interpolationStrategy() {
1624
+ return this._interpolationStrategy;
1625
+ }
1626
+ /**
1627
+ * Active interpolation strategy params used by the transposer stage.
1628
+ * @returns The current interpolation strategy parameters.
1629
+ */
1630
+ get interpolationStrategyParams() {
1631
+ return this.transposer.strategyParams;
1632
+ }
1633
+ /**
1634
+ * Switches interpolation strategy at runtime.
1635
+ * @param strategy The new interpolation strategy to use.
1636
+ */
1637
+ setInterpolationStrategy(strategy) {
1638
+ this.transposer.setInterpolationStrategy(strategy);
1639
+ this._interpolationStrategy = this.transposer.strategy;
1640
+ }
1641
+ /**
1642
+ * Applies a partial runtime params update to the current strategy.
1643
+ * @param params Partial set of parameters to update.
1644
+ */
1645
+ setInterpolationStrategyParams(params) {
1646
+ this.transposer.setInterpolationStrategyParams(params);
1647
+ }
1648
+ /**
1649
+ * Applies a partial set of WSOLA timing parameters to the stretch stage.
1650
+ *
1651
+ * @remarks
1652
+ * Delegates directly to {@link Stretch.setStretchParameters}. Only the provided
1653
+ * fields are updated; omitted fields remain unchanged. Pass `sequenceMs: 0` or
1654
+ * `seekWindowMs: 0` to switch that dimension back to auto-calculation.
1655
+ *
1656
+ * @param params Partial set of WSOLA timing parameters to apply.
1657
+ *
1658
+ * @example
1659
+ * st.setStretchParameters({ overlapMs: 12, quickSeek: false });
1660
+ */
1661
+ setStretchParameters(params) {
1662
+ this.stretch.setStretchParameters(params);
1663
+ }
1664
+ /**
1665
+ * Sets the pitch multiplier and recomputes the derived pipeline rate and tempo.
1666
+ *
1667
+ * @remarks
1668
+ * Internally sets `_rate = pitch` and `_tempo = 1 / pitch`, rewiring the
1669
+ * Transposer→Stretch stage order when pitch > 1.
1670
+ */
1671
+ set pitch(pitch) {
1672
+ this.virtualPitch = pitch;
1673
+ this.calculateEffectiveRateAndTempo();
1674
+ }
1675
+ /**
1676
+ * Sets pitch by octave offset.
1677
+ */
1678
+ set pitchOctaves(pitchOctaves) {
1679
+ this.pitch = Math.exp(.69314718056 * pitchOctaves);
1680
+ this.calculateEffectiveRateAndTempo();
1681
+ }
1682
+ /**
1683
+ * Sets pitch by semitone offset.
1684
+ */
1685
+ set pitchSemitones(pitchSemitones) {
1686
+ this.pitchOctaves = pitchSemitones / 12;
1687
+ }
1688
+ /**
1689
+ * Input buffer for upstream interleaved stereo frames.
1690
+ * @returns The input buffer for writing audio frames.
1691
+ */
1692
+ get inputBuffer() {
1693
+ return this._inputBuffer;
1694
+ }
1695
+ /**
1696
+ * Output buffer that downstream consumers read from.
1697
+ * @returns The output buffer for reading processed audio frames.
1698
+ */
1699
+ get outputBuffer() {
1700
+ return this._outputBuffer;
1701
+ }
1702
+ /**
1703
+ * Recomputes the effective pipeline rate/tempo from `virtualPitch` and rewires stage order when needed.
1704
+ *
1705
+ * @remarks
1706
+ * `_rate` is set to `virtualPitch`; `_tempo` to `1 / virtualPitch`. When `_rate > 1` the
1707
+ * Stretch stage feeds the Transposer; otherwise the order is reversed.
1708
+ */
1709
+ calculateEffectiveRateAndTempo() {
1710
+ const previousTempo = this._tempo;
1711
+ const previousRate = this._rate;
1712
+ this._tempo = 1 / this.virtualPitch;
1713
+ this._rate = this.virtualPitch;
1714
+ if (isFloatDifferent(this._tempo, previousTempo)) this.stretch.tempo = this._tempo;
1715
+ if (isFloatDifferent(this._rate, previousRate)) this.transposer.rate = this._rate;
1716
+ if (this._rate > 1) {
1717
+ if (this._outputBuffer !== this.transposer.outputBuffer) {
1718
+ this.stretch.inputBuffer = this._inputBuffer;
1719
+ this.stretch.outputBuffer = this._intermediateBuffer;
1720
+ this.transposer.inputBuffer = this._intermediateBuffer;
1721
+ this.transposer.outputBuffer = this._outputBuffer;
1722
+ }
1723
+ } else if (this._outputBuffer !== this.stretch.outputBuffer) {
1724
+ this.transposer.inputBuffer = this._inputBuffer;
1725
+ this.transposer.outputBuffer = this._intermediateBuffer;
1726
+ this.stretch.inputBuffer = this._intermediateBuffer;
1727
+ this.stretch.outputBuffer = this._outputBuffer;
1728
+ }
1729
+ }
1730
+ /**
1731
+ * Runs one processing step through the currently selected stage order.
1732
+ * @remarks
1733
+ * Processes available frames through the pipeline, updating output buffers.
1734
+ */
1735
+ process() {
1736
+ if (this._rate > 1) {
1737
+ this.stretch.process();
1738
+ this.transposer.process();
1739
+ } else {
1740
+ this.transposer.process();
1741
+ this.stretch.process();
1742
+ }
1743
+ }
1744
+ };
1745
+ //#endregion
1746
+ //#region ../worklet-base/src/SoundTouchProcessorBase.ts
1747
+ /**
1748
+ * Abstract base class for all SoundTouchJS AudioWorklet processor implementations.
1749
+ *
1750
+ * @remarks
1751
+ * Centralises shared state (pipe, sample buffers, runtime-update queue), the
1752
+ * `applyPendingRuntimeUpdates` helper, and the core DSP pipeline in
1753
+ * `processCore`. Subclasses must implement `process` and `onProcessComplete`.
1754
+ *
1755
+ * The default `process` implementation calls `applyPendingRuntimeUpdates`,
1756
+ * `processCore`, and then `onProcessComplete`. Subclasses that need to
1757
+ * interleave additional logic (e.g. LPC analysis) can override `beforePipeProcess`
1758
+ * and/or `extractSamples` instead of overriding `process` entirely.
1759
+ */
1760
+ var SoundTouchProcessorBase = class extends AudioWorkletProcessor {
1761
+ /** The SoundTouch DSP pipeline instance. */
1762
+ _pipe;
1763
+ /** Interleaved (L, R, L, R, …) input staging buffer. */
1764
+ _samples;
1765
+ /** Interleaved output staging buffer populated by `extractSamples`. */
1766
+ _outputSamples;
1767
+ /** Cumulative count of render blocks where the output buffer ran short. */
1768
+ _underrunCount = 0;
1769
+ /** Total render blocks processed since construction. */
1770
+ _blockCount = 0;
1771
+ _pendingInterpolationStrategy = null;
1772
+ _pendingInterpolationStrategyParams = null;
1773
+ _pendingStretchParameters = null;
1774
+ /** Label used in console messages (e.g. `'[SoundTouchProcessor]'`). */
1775
+ processorLabel;
1776
+ /**
1777
+ * Validates and resolves an interpolation strategy id, falling back to
1778
+ * `'lanczos'` if the id is unrecognised.
1779
+ *
1780
+ * @remarks
1781
+ * Call this as a static expression inside the `super()` argument list of a
1782
+ * subclass constructor so that strategy resolution happens before pipe creation.
1783
+ *
1784
+ * @param strategy - The strategy id provided by the caller.
1785
+ * @param processorLabel - Label included in the fallback console message.
1786
+ * @returns The original `strategy` if valid, or `'lanczos'` as a fallback.
1787
+ */
1788
+ static resolveStrategy(strategy, processorLabel) {
1789
+ try {
1790
+ if (strategy) resolveInterpolationStrategy(strategy);
1791
+ return strategy;
1792
+ } catch {
1793
+ console.info(`${processorLabel} Unknown interpolation strategy id:`, strategy, "— falling back to lanczos.");
1794
+ return "lanczos";
1795
+ }
1796
+ }
1797
+ /**
1798
+ * @param processorLabel - Label string used in diagnostic messages.
1799
+ * @param pipeOptions - Options forwarded to the `SoundTouch` constructor. The
1800
+ * `sampleRate` global must be available in the AudioWorklet scope.
1801
+ */
1802
+ constructor(processorLabel, pipeOptions) {
1803
+ super();
1804
+ this.processorLabel = processorLabel;
1805
+ this._pipe = new SoundTouch(pipeOptions);
1806
+ this._samples = new Float32Array(256);
1807
+ this._outputSamples = new Float32Array(256);
1808
+ const port = this.port;
1809
+ if (port !== void 0) port.onmessage = (event) => {
1810
+ const message = event.data;
1811
+ if (message.type === "set-interpolation-strategy") {
1812
+ this._pendingInterpolationStrategy = message.strategy;
1813
+ return;
1814
+ }
1815
+ if (message.type === "set-interpolation-strategy-params") {
1816
+ this._pendingInterpolationStrategyParams = message.params;
1817
+ return;
1818
+ }
1819
+ if (message.type === "set-stretch-parameters") this._pendingStretchParameters = message.params;
1820
+ };
1821
+ }
1822
+ /**
1823
+ * Flushes any pending interpolation-strategy or stretch-parameter change
1824
+ * that arrived via `port.onmessage` since the last render block.
1825
+ *
1826
+ * @remarks
1827
+ * Call this at the top of `process` before touching `_pipe`.
1828
+ */
1829
+ applyPendingRuntimeUpdates() {
1830
+ if (this._pendingInterpolationStrategy !== null) {
1831
+ try {
1832
+ this._pipe.setInterpolationStrategy(this._pendingInterpolationStrategy);
1833
+ } catch {
1834
+ console.info(`${this.processorLabel} Failed to switch interpolation strategy:`, this._pendingInterpolationStrategy);
1835
+ }
1836
+ this._pendingInterpolationStrategy = null;
1837
+ }
1838
+ if (this._pendingInterpolationStrategyParams !== null) {
1839
+ try {
1840
+ this._pipe.setInterpolationStrategyParams(this._pendingInterpolationStrategyParams);
1841
+ } catch {
1842
+ console.info(`${this.processorLabel} Failed to update interpolation strategy params.`);
1843
+ }
1844
+ this._pendingInterpolationStrategyParams = null;
1845
+ }
1846
+ if (this._pendingStretchParameters !== null) {
1847
+ try {
1848
+ this._pipe.setStretchParameters(this._pendingStretchParameters);
1849
+ } catch {
1850
+ console.info(`${this.processorLabel} Failed to update stretch parameters.`);
1851
+ }
1852
+ this._pendingStretchParameters = null;
1853
+ }
1854
+ }
1855
+ /**
1856
+ * Optional hook called after input routing and buffer resize but **before**
1857
+ * the SoundTouch pipe processes the block.
1858
+ *
1859
+ * @remarks
1860
+ * Override in subclasses that need to inspect or transform the raw input
1861
+ * before it enters the DSP pipeline (e.g. computing LPC coefficients).
1862
+ *
1863
+ * @param _leftInput - Left-channel input for this render block.
1864
+ * @param _rightInput - Right-channel input (same as left for mono sources).
1865
+ * @param _frameCount - Number of frames in this block.
1866
+ * @param _parameters - AudioParam k-rate values for this render block.
1867
+ */
1868
+ beforePipeProcess(_leftInput, _rightInput, _frameCount, _parameters) {}
1869
+ /**
1870
+ * Extracts rendered frames from the output buffer, writes them to
1871
+ * `leftOutput`/`rightOutput`, zero-fills any gap, and returns RMS/peak metrics.
1872
+ *
1873
+ * @remarks
1874
+ * Override in subclasses that apply post-extraction transforms (e.g. formant
1875
+ * correction). Overrides are responsible for the full extraction, write-back,
1876
+ * silence fill, and returning `{ outputRms, outputPeak }`.
1877
+ *
1878
+ * @param leftOutput - Destination view for the left channel.
1879
+ * @param rightOutput - Destination view for the right channel.
1880
+ * @param frameCount - Total frames expected in this block.
1881
+ * @param toExtract - Frames available to extract (≤ frameCount).
1882
+ * @param _parameters - AudioParam k-rate values (available for overrides).
1883
+ * @returns RMS and peak of the extracted block.
1884
+ */
1885
+ extractSamples(leftOutput, rightOutput, frameCount, toExtract, _parameters) {
1886
+ let outputRms = 0;
1887
+ let outputPeak = 0;
1888
+ if (toExtract > 0) {
1889
+ const extracted = this._outputSamples;
1890
+ this._pipe.outputBuffer.extract(extracted, 0, toExtract);
1891
+ this._pipe.outputBuffer.receive(toExtract);
1892
+ let sumSq = 0;
1893
+ let peak = 0;
1894
+ for (let i = 0; i < toExtract; i++) {
1895
+ const l = extracted[i * 2];
1896
+ const r = extracted[i * 2 + 1];
1897
+ leftOutput[i] = Number.isFinite(l) ? l : 0;
1898
+ rightOutput[i] = Number.isFinite(r) ? r : 0;
1899
+ sumSq += l * l + r * r;
1900
+ peak = Math.max(peak, Math.abs(l), Math.abs(r));
1901
+ }
1902
+ outputRms = Math.sqrt(sumSq / (toExtract * 2));
1903
+ outputPeak = peak;
1904
+ }
1905
+ for (let i = toExtract; i < frameCount; i++) {
1906
+ leftOutput[i] = 0;
1907
+ rightOutput[i] = 0;
1908
+ }
1909
+ return {
1910
+ outputRms,
1911
+ outputPeak
1912
+ };
1913
+ }
1914
+ /**
1915
+ * Runs the full DSP pipeline for one render block and returns metrics.
1916
+ *
1917
+ * @remarks
1918
+ * Handles input routing, buffer resize, `beforePipeProcess`, pitch
1919
+ * calculation, sample interleaving, pipe feed/process, counter updates,
1920
+ * and `extractSamples`. Returns `null` when the input is empty or
1921
+ * the output has not been allocated, keeping the processor alive.
1922
+ *
1923
+ * @param inputs - AudioWorklet input buses.
1924
+ * @param outputs - AudioWorklet output buses.
1925
+ * @param parameters - k-rate AudioParam values.
1926
+ * @returns Render-block result, or `null` if inputs are not ready.
1927
+ */
1928
+ processCore(inputs, outputs, parameters) {
1929
+ const input = inputs[0];
1930
+ const output = outputs[0];
1931
+ if (!input || !input.length || !output[0] || !output[0].length) return null;
1932
+ const leftInput = input[0];
1933
+ const rightInput = input.length > 1 ? input[1] : input[0];
1934
+ const leftOutput = output[0];
1935
+ const rightOutput = output.length > 1 ? output[1] : output[0];
1936
+ const frameCount = leftInput.length;
1937
+ if (this._samples.length < frameCount * 2) {
1938
+ this._samples = new Float32Array(frameCount * 2);
1939
+ this._outputSamples = new Float32Array(frameCount * 2);
1940
+ }
1941
+ this.beforePipeProcess(leftInput, rightInput, frameCount, parameters);
1942
+ const pitch = parameters["pitch"][0];
1943
+ const pitchSemitones = parameters["pitchSemitones"][0];
1944
+ const playbackRate = parameters["playbackRate"][0];
1945
+ this._pipe.pitch = pitch * Math.pow(2, pitchSemitones / 12) / playbackRate;
1946
+ const samples = this._samples;
1947
+ for (let i = 0; i < frameCount; i++) {
1948
+ samples[i * 2] = leftInput[i];
1949
+ samples[i * 2 + 1] = rightInput[i];
1950
+ }
1951
+ this._pipe.inputBuffer.putSamples(samples, 0, frameCount);
1952
+ this._pipe.process();
1953
+ const available = this._pipe.outputBuffer.frameCount;
1954
+ const toExtract = Math.min(available, frameCount);
1955
+ this._blockCount++;
1956
+ if (available < frameCount) this._underrunCount++;
1957
+ const { outputRms, outputPeak } = this.extractSamples(leftOutput, rightOutput, frameCount, toExtract, parameters);
1958
+ return {
1959
+ frameCount,
1960
+ toExtract,
1961
+ available,
1962
+ leftInput,
1963
+ rightInput,
1964
+ leftOutput,
1965
+ rightOutput,
1966
+ outputRms,
1967
+ outputPeak
1968
+ };
1969
+ }
1970
+ /**
1971
+ * AudioWorkletProcessor render callback. Keeps the processor alive by always returning `true`.
1972
+ *
1973
+ * @remarks
1974
+ * The default implementation calls `applyPendingRuntimeUpdates`, `processCore`,
1975
+ * and `onProcessComplete`. Override only when the execution order must differ
1976
+ * (e.g. pre-pipe analysis steps not covered by `beforePipeProcess`).
1977
+ *
1978
+ * @param inputs - AudioWorklet input buses.
1979
+ * @param outputs - AudioWorklet output buses.
1980
+ * @param parameters - k-rate AudioParam values.
1981
+ * @returns Always `true` to keep the processor alive.
1982
+ */
1983
+ process(inputs, outputs, parameters) {
1984
+ this.applyPendingRuntimeUpdates();
1985
+ const result = this.processCore(inputs, outputs, parameters);
1986
+ if (result !== null) this.onProcessComplete(result);
1987
+ return true;
1988
+ }
1989
+ };
1990
+ //#endregion
1991
+ //#region ../worklet-base/src/types.ts
1992
+ /**
1993
+ * Standard pitch, pitchSemitones, and playbackRate AudioParam descriptors
1994
+ * shared by all SoundTouchJS worklet processors.
1995
+ */
1996
+ var STANDARD_PARAMETER_DESCRIPTORS = [
1997
+ {
1998
+ name: "pitch",
1999
+ defaultValue: 1,
2000
+ minValue: .1,
2001
+ maxValue: 8,
2002
+ automationRate: "k-rate"
2003
+ },
2004
+ {
2005
+ name: "pitchSemitones",
2006
+ defaultValue: 0,
2007
+ minValue: -24,
2008
+ maxValue: 24,
2009
+ automationRate: "k-rate"
2010
+ },
2011
+ {
2012
+ name: "playbackRate",
2013
+ defaultValue: 1,
2014
+ minValue: .1,
2015
+ maxValue: 8,
2016
+ automationRate: "k-rate"
2017
+ }
2018
+ ];
2019
+ //#endregion
2020
+ //#region src/processor.ts
2021
+ var PROCESSOR_NAME = "soundtouch-processor";
2022
+ /**
2023
+ * Audio render-thread processor that applies SoundTouch transformations to stereo blocks.
2024
+ *
2025
+ * @remarks
2026
+ * Receives audio from the main thread, applies pitch, tempo, and rate transformations,
2027
+ * and outputs processed stereo audio. Handles runtime strategy switching via messages.
2028
+ */
2029
+ var SoundTouchProcessor = class extends SoundTouchProcessorBase {
2030
+ /** Static AudioParam metadata consumed by the browser. */
2031
+ static get parameterDescriptors() {
2032
+ return STANDARD_PARAMETER_DESCRIPTORS;
2033
+ }
2034
+ /**
2035
+ * @param options Worklet constructor options provided by the main thread.
2036
+ *
2037
+ * @remarks
2038
+ * Unknown interpolation strategy ids are logged and coerced to `lanczos`
2039
+ * so render-thread startup remains resilient.
2040
+ */
2041
+ constructor(options) {
2042
+ super("[SoundTouchProcessor]", {
2043
+ sampleRate,
2044
+ sampleBufferType: options?.processorOptions?.sampleBufferType ?? "circular",
2045
+ interpolationStrategy: SoundTouchProcessorBase.resolveStrategy(options?.processorOptions?.interpolationStrategy, "[SoundTouchProcessor]")
2046
+ });
2047
+ }
2048
+ onProcessComplete(result) {
2049
+ if (this._blockCount % 100 === 0) this.port.postMessage({
2050
+ type: "metrics",
2051
+ framesBuffered: result.available,
2052
+ underrunCount: this._underrunCount,
2053
+ blockCount: this._blockCount,
2054
+ outputRms: result.outputRms,
2055
+ outputPeak: result.outputPeak
2056
+ });
2057
+ }
2058
+ };
2059
+ registerProcessor(PROCESSOR_NAME, SoundTouchProcessor);
2060
+ //#endregion
2061
+
2062
+ //# sourceMappingURL=soundtouch-processor.js.map