wavesurfer.js 7.12.3 → 7.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugins/envelope.cjs +1 -0
- package/dist/plugins/envelope.esm.js +1 -0
- package/dist/plugins/envelope.js +1 -426
- package/dist/plugins/envelope.min.js +1 -0
- package/dist/plugins/hover.cjs +1 -0
- package/dist/plugins/hover.esm.js +1 -0
- package/dist/plugins/hover.js +1 -131
- package/dist/plugins/hover.min.js +1 -0
- package/dist/plugins/minimap.cjs +1 -0
- package/dist/plugins/minimap.esm.js +1 -0
- package/dist/plugins/minimap.js +1 -229
- package/dist/plugins/minimap.min.js +1 -0
- package/dist/plugins/record.cjs +1 -0
- package/dist/plugins/record.esm.js +1 -0
- package/dist/plugins/record.js +1 -319
- package/dist/plugins/record.min.js +1 -0
- package/dist/plugins/regions.cjs +1 -0
- package/dist/plugins/regions.esm.js +1 -0
- package/dist/plugins/regions.js +1 -699
- package/dist/plugins/regions.min.js +1 -0
- package/dist/plugins/spectrogram-windowed.cjs +1 -0
- package/dist/plugins/spectrogram-windowed.esm.js +1 -0
- package/dist/plugins/spectrogram-windowed.js +1 -967
- package/dist/plugins/spectrogram-windowed.min.js +1 -0
- package/dist/plugins/spectrogram.cjs +1 -0
- package/dist/plugins/spectrogram.esm.js +1 -0
- package/dist/plugins/spectrogram.min.js +1 -0
- package/package.json +4 -2
|
@@ -1,967 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Windowed Spectrogram plugin - Optimized for very long audio files
|
|
3
|
-
*
|
|
4
|
-
* Only renders frequency data in a sliding window around the current viewport,
|
|
5
|
-
* keeping memory usage constant regardless of audio length.
|
|
6
|
-
*/
|
|
7
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
8
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
9
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
11
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
12
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
13
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
// @ts-nocheck
|
|
17
|
-
import BasePlugin from '../base-plugin.js';
|
|
18
|
-
import createElement from '../dom.js';
|
|
19
|
-
// Import centralized FFT functionality
|
|
20
|
-
import FFT, { hzToScale, scaleToHz, createFilterBankForScale, applyFilterBank, setupColorMap, createWrapperClickHandler, } from '../fft.js';
|
|
21
|
-
// Import the worker using rollup-plugin-web-worker-loader
|
|
22
|
-
import SpectrogramWorker from 'web-worker:./spectrogram-worker.ts';
|
|
23
|
-
class WindowedSpectrogramPlugin extends BasePlugin {
|
|
24
|
-
static create(options) {
|
|
25
|
-
return new WindowedSpectrogramPlugin(options || {});
|
|
26
|
-
}
|
|
27
|
-
constructor(options) {
|
|
28
|
-
var _a, _b;
|
|
29
|
-
super(options);
|
|
30
|
-
this.segments = new Map();
|
|
31
|
-
this.buffer = null;
|
|
32
|
-
this.currentPosition = 0; // current playback position in seconds
|
|
33
|
-
this.pixelsPerSecond = 0;
|
|
34
|
-
this.isRendering = false;
|
|
35
|
-
this.renderTimeout = null;
|
|
36
|
-
// FFT and processing
|
|
37
|
-
this.fft = null;
|
|
38
|
-
// Progressive loading
|
|
39
|
-
this.progressiveLoadTimeout = null;
|
|
40
|
-
this.isProgressiveLoading = false;
|
|
41
|
-
this.nextProgressiveSegmentTime = 0; // Track which segment to load next
|
|
42
|
-
// Web worker for FFT calculations
|
|
43
|
-
this.worker = null;
|
|
44
|
-
this.workerPromises = new Map();
|
|
45
|
-
this.qualityUpdateTimeout = null;
|
|
46
|
-
this._onWrapperClick = (e) => {
|
|
47
|
-
const rect = this.wrapper.getBoundingClientRect();
|
|
48
|
-
const relativeX = e.clientX - rect.left;
|
|
49
|
-
const relativeWidth = rect.width;
|
|
50
|
-
const relativePosition = relativeX / relativeWidth;
|
|
51
|
-
this.emit('click', relativePosition);
|
|
52
|
-
};
|
|
53
|
-
this.container =
|
|
54
|
-
'string' == typeof options.container ? document.querySelector(options.container) : options.container;
|
|
55
|
-
// Set up color map using shared utility
|
|
56
|
-
this.colorMap = setupColorMap(options.colorMap);
|
|
57
|
-
// FFT and processing options
|
|
58
|
-
this.fftSamples = options.fftSamples || 512;
|
|
59
|
-
this.height = options.height || 200;
|
|
60
|
-
this.noverlap = options.noverlap || null; // Will be calculated later based on canvas size, like normal plugin
|
|
61
|
-
this.windowFunc = options.windowFunc || 'hann';
|
|
62
|
-
this.alpha = options.alpha;
|
|
63
|
-
this.frequencyMin = options.frequencyMin || 0;
|
|
64
|
-
this.frequencyMax = options.frequencyMax || 0;
|
|
65
|
-
this.gainDB = (_a = options.gainDB) !== null && _a !== void 0 ? _a : 20;
|
|
66
|
-
this.rangeDB = (_b = options.rangeDB) !== null && _b !== void 0 ? _b : 80;
|
|
67
|
-
this.scale = options.scale || 'mel';
|
|
68
|
-
// Windowing options
|
|
69
|
-
this.windowSize = options.windowSize || 30; // 30 seconds window
|
|
70
|
-
this.bufferSize = options.bufferSize || 5000; // 5000 pixels buffer
|
|
71
|
-
// Progressive loading (disabled by default to avoid system overload)
|
|
72
|
-
this.progressiveLoading = options.progressiveLoading === true;
|
|
73
|
-
// Web worker (disabled by default in SSR environments like Next.js)
|
|
74
|
-
this.useWebWorker = options.useWebWorker === true && typeof window !== 'undefined';
|
|
75
|
-
// Filter banks
|
|
76
|
-
this.numMelFilters = this.fftSamples / 2;
|
|
77
|
-
this.numLogFilters = this.fftSamples / 2;
|
|
78
|
-
this.numBarkFilters = this.fftSamples / 2;
|
|
79
|
-
this.numErbFilters = this.fftSamples / 2;
|
|
80
|
-
this.createWrapper();
|
|
81
|
-
this.createCanvas();
|
|
82
|
-
// Initialize worker if enabled
|
|
83
|
-
if (this.useWebWorker) {
|
|
84
|
-
this.initializeWorker();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
initializeWorker() {
|
|
88
|
-
// Skip worker initialization in SSR environments (Next.js server-side)
|
|
89
|
-
if (typeof window === 'undefined' || typeof Worker === 'undefined') {
|
|
90
|
-
console.warn('Worker not available in this environment, using main thread calculation');
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
// Create worker using imported worker constructor
|
|
95
|
-
this.worker = new SpectrogramWorker();
|
|
96
|
-
this.worker.onmessage = (e) => {
|
|
97
|
-
const { type, id, result, error } = e.data;
|
|
98
|
-
if (type === 'frequenciesResult') {
|
|
99
|
-
const promise = this.workerPromises.get(id);
|
|
100
|
-
if (promise) {
|
|
101
|
-
this.workerPromises.delete(id);
|
|
102
|
-
if (error) {
|
|
103
|
-
promise.reject(new Error(error));
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
promise.resolve(result);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
this.worker.onerror = (error) => {
|
|
112
|
-
console.warn('Spectrogram worker error, falling back to main thread:', error);
|
|
113
|
-
// Fallback to main thread calculation
|
|
114
|
-
this.worker = null;
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
console.warn('Failed to initialize worker, falling back to main thread:', error);
|
|
119
|
-
this.worker = null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
onInit() {
|
|
123
|
-
// Recreate DOM elements if they were destroyed
|
|
124
|
-
if (!this.wrapper) {
|
|
125
|
-
this.createWrapper();
|
|
126
|
-
}
|
|
127
|
-
if (!this.canvasContainer) {
|
|
128
|
-
this.createCanvas();
|
|
129
|
-
}
|
|
130
|
-
// Always get fresh container reference to avoid stale references
|
|
131
|
-
this.container = this.wavesurfer.getWrapper();
|
|
132
|
-
this.container.appendChild(this.wrapper);
|
|
133
|
-
// Set up styling
|
|
134
|
-
if (this.wavesurfer.options.fillParent) {
|
|
135
|
-
Object.assign(this.wrapper.style, {
|
|
136
|
-
width: '100%',
|
|
137
|
-
overflowX: 'hidden',
|
|
138
|
-
overflowY: 'hidden',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
// Listen for playback position changes
|
|
142
|
-
this.subscriptions.push(this.wavesurfer.on('timeupdate', (currentTime) => {
|
|
143
|
-
this.updatePosition(currentTime);
|
|
144
|
-
}));
|
|
145
|
-
// Listen for scroll events
|
|
146
|
-
this.subscriptions.push(this.wavesurfer.on('scroll', () => {
|
|
147
|
-
this.handleScroll();
|
|
148
|
-
}));
|
|
149
|
-
// Listen for zoom changes
|
|
150
|
-
this.subscriptions.push(this.wavesurfer.on('redraw', () => this.handleRedraw()));
|
|
151
|
-
// Listen for audio data ready
|
|
152
|
-
this.subscriptions.push(this.wavesurfer.on('ready', () => {
|
|
153
|
-
const decodedData = this.wavesurfer.getDecodedData();
|
|
154
|
-
if (decodedData) {
|
|
155
|
-
this.render(decodedData);
|
|
156
|
-
}
|
|
157
|
-
}));
|
|
158
|
-
// Trigger initial render after re-initialization
|
|
159
|
-
// This ensures the spectrogram appears even if no redraw event is fired
|
|
160
|
-
if (this.wavesurfer.getDecodedData()) {
|
|
161
|
-
// Use setTimeout to ensure DOM is fully ready
|
|
162
|
-
setTimeout(() => {
|
|
163
|
-
this.render(this.wavesurfer.getDecodedData());
|
|
164
|
-
}, 0);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
createWrapper() {
|
|
168
|
-
this.wrapper = createElement('div', {
|
|
169
|
-
style: {
|
|
170
|
-
display: 'block',
|
|
171
|
-
position: 'relative',
|
|
172
|
-
userSelect: 'none',
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
// Labels canvas
|
|
176
|
-
if (this.options.labels) {
|
|
177
|
-
this.labelsEl = createElement('canvas', {
|
|
178
|
-
part: 'spec-labels',
|
|
179
|
-
style: {
|
|
180
|
-
position: 'absolute',
|
|
181
|
-
zIndex: 9,
|
|
182
|
-
width: '55px',
|
|
183
|
-
height: '100%',
|
|
184
|
-
},
|
|
185
|
-
}, this.wrapper);
|
|
186
|
-
}
|
|
187
|
-
// Create wrapper click handler using shared utility
|
|
188
|
-
this._onWrapperClick = createWrapperClickHandler(this.wrapper, this.emit.bind(this));
|
|
189
|
-
this.wrapper.addEventListener('click', this._onWrapperClick);
|
|
190
|
-
}
|
|
191
|
-
createCanvas() {
|
|
192
|
-
this.canvasContainer = createElement('div', {
|
|
193
|
-
style: {
|
|
194
|
-
position: 'absolute',
|
|
195
|
-
left: 0,
|
|
196
|
-
top: 0,
|
|
197
|
-
width: '100%',
|
|
198
|
-
height: '100%',
|
|
199
|
-
zIndex: 4,
|
|
200
|
-
},
|
|
201
|
-
}, this.wrapper);
|
|
202
|
-
}
|
|
203
|
-
handleRedraw() {
|
|
204
|
-
const oldPixelsPerSecond = this.pixelsPerSecond;
|
|
205
|
-
this.pixelsPerSecond = this.getPixelsPerSecond();
|
|
206
|
-
// Only update canvas positions if zoom changed, keep frequency data!
|
|
207
|
-
if (oldPixelsPerSecond !== this.pixelsPerSecond && this.segments.size > 0) {
|
|
208
|
-
this.updateSegmentPositions(oldPixelsPerSecond, this.pixelsPerSecond);
|
|
209
|
-
}
|
|
210
|
-
this.scheduleRender();
|
|
211
|
-
}
|
|
212
|
-
updateSegmentPositions(oldPxPerSec, newPxPerSec) {
|
|
213
|
-
for (const segment of this.segments.values()) {
|
|
214
|
-
// Update pixel positions based on new zoom level
|
|
215
|
-
segment.startPixel = segment.startTime * newPxPerSec;
|
|
216
|
-
segment.endPixel = segment.endTime * newPxPerSec;
|
|
217
|
-
// Update canvas positioning and size WITHOUT recalculating frequencies
|
|
218
|
-
if (segment.canvas) {
|
|
219
|
-
const segmentWidth = segment.endPixel - segment.startPixel;
|
|
220
|
-
segment.canvas.style.left = `${segment.startPixel}px`;
|
|
221
|
-
segment.canvas.style.width = `${segmentWidth}px`;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Schedule a gentle re-render of visible segments only if zoom changed significantly
|
|
225
|
-
const zoomRatio = newPxPerSec / oldPxPerSec;
|
|
226
|
-
if (zoomRatio < 0.5 || zoomRatio > 2.0) {
|
|
227
|
-
this.scheduleSegmentQualityUpdate();
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
scheduleSegmentQualityUpdate() {
|
|
231
|
-
// Debounce quality updates to avoid rapid re-renders during zoom
|
|
232
|
-
if (this.qualityUpdateTimeout) {
|
|
233
|
-
clearTimeout(this.qualityUpdateTimeout);
|
|
234
|
-
}
|
|
235
|
-
this.qualityUpdateTimeout = window.setTimeout(() => {
|
|
236
|
-
this.updateVisibleSegmentQuality();
|
|
237
|
-
}, 500); // Wait 500ms after zoom stops
|
|
238
|
-
}
|
|
239
|
-
updateVisibleSegmentQuality() {
|
|
240
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
241
|
-
var _a;
|
|
242
|
-
if (!this.buffer)
|
|
243
|
-
return;
|
|
244
|
-
const wrapper = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getWrapper();
|
|
245
|
-
if (!wrapper)
|
|
246
|
-
return;
|
|
247
|
-
// Get current viewport
|
|
248
|
-
const scrollLeft = this.getScrollLeft(wrapper);
|
|
249
|
-
const viewportWidth = this.getViewportWidth(wrapper);
|
|
250
|
-
const pixelsPerSec = this.getPixelsPerSecond();
|
|
251
|
-
const visibleStartTime = scrollLeft / pixelsPerSec;
|
|
252
|
-
const visibleEndTime = (scrollLeft + viewportWidth) / pixelsPerSec;
|
|
253
|
-
// Find segments that overlap with visible area
|
|
254
|
-
const visibleSegments = Array.from(this.segments.values()).filter((segment) => segment.startTime < visibleEndTime && segment.endTime > visibleStartTime);
|
|
255
|
-
if (visibleSegments.length === 0) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
// Re-render only the visible segments with current zoom level
|
|
259
|
-
for (const segment of visibleSegments) {
|
|
260
|
-
if (segment.canvas) {
|
|
261
|
-
yield this.renderSegment(segment);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
getScrollLeft(wrapper) {
|
|
267
|
-
var _a;
|
|
268
|
-
// Try multiple sources for scroll position
|
|
269
|
-
if (wrapper.scrollLeft)
|
|
270
|
-
return wrapper.scrollLeft;
|
|
271
|
-
if ((_a = wrapper.parentElement) === null || _a === void 0 ? void 0 : _a.scrollLeft)
|
|
272
|
-
return wrapper.parentElement.scrollLeft;
|
|
273
|
-
if (document.documentElement.scrollLeft)
|
|
274
|
-
return document.documentElement.scrollLeft;
|
|
275
|
-
if (document.body.scrollLeft)
|
|
276
|
-
return document.body.scrollLeft;
|
|
277
|
-
if (window.scrollX)
|
|
278
|
-
return window.scrollX;
|
|
279
|
-
if (window.pageXOffset)
|
|
280
|
-
return window.pageXOffset;
|
|
281
|
-
// Look for scrollable ancestors
|
|
282
|
-
let element = wrapper.parentElement;
|
|
283
|
-
while (element) {
|
|
284
|
-
const computedStyle = window.getComputedStyle(element);
|
|
285
|
-
if (computedStyle.overflowX === 'scroll' || computedStyle.overflowX === 'auto') {
|
|
286
|
-
if (element.scrollLeft > 0)
|
|
287
|
-
return element.scrollLeft;
|
|
288
|
-
}
|
|
289
|
-
element = element.parentElement;
|
|
290
|
-
}
|
|
291
|
-
return 0;
|
|
292
|
-
}
|
|
293
|
-
getViewportWidth(wrapper) {
|
|
294
|
-
var _a, _b;
|
|
295
|
-
const wrapperWidth = wrapper.offsetWidth || wrapper.clientWidth;
|
|
296
|
-
const parentWidth = ((_a = wrapper.parentElement) === null || _a === void 0 ? void 0 : _a.offsetWidth) || ((_b = wrapper.parentElement) === null || _b === void 0 ? void 0 : _b.clientWidth);
|
|
297
|
-
const windowWidth = window.innerWidth;
|
|
298
|
-
// Use the smallest reasonable width
|
|
299
|
-
if (parentWidth && parentWidth < wrapperWidth)
|
|
300
|
-
return parentWidth;
|
|
301
|
-
return Math.min(wrapperWidth || 800, windowWidth * 0.8);
|
|
302
|
-
}
|
|
303
|
-
handleScroll() {
|
|
304
|
-
var _a, _b;
|
|
305
|
-
const wrapper = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getWrapper();
|
|
306
|
-
// Use the same scroll detection logic as renderVisibleWindow
|
|
307
|
-
let scrollLeft = 0;
|
|
308
|
-
if (wrapper === null || wrapper === void 0 ? void 0 : wrapper.scrollLeft) {
|
|
309
|
-
scrollLeft = wrapper.scrollLeft;
|
|
310
|
-
}
|
|
311
|
-
else if ((_b = wrapper === null || wrapper === void 0 ? void 0 : wrapper.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) {
|
|
312
|
-
scrollLeft = wrapper.parentElement.scrollLeft;
|
|
313
|
-
}
|
|
314
|
-
else if (document.documentElement.scrollLeft || document.body.scrollLeft) {
|
|
315
|
-
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
|
|
316
|
-
}
|
|
317
|
-
else if (window.scrollX || window.pageXOffset) {
|
|
318
|
-
scrollLeft = window.scrollX || window.pageXOffset;
|
|
319
|
-
}
|
|
320
|
-
else if (wrapper) {
|
|
321
|
-
// Look for scrollable ancestors
|
|
322
|
-
let element = wrapper.parentElement;
|
|
323
|
-
while (element && scrollLeft === 0) {
|
|
324
|
-
const computedStyle = window.getComputedStyle(element);
|
|
325
|
-
if (computedStyle.overflowX === 'scroll' || computedStyle.overflowX === 'auto') {
|
|
326
|
-
if (element.scrollLeft > 0) {
|
|
327
|
-
scrollLeft = element.scrollLeft;
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
element = element.parentElement;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const pixelsPerSec = this.getPixelsPerSecond();
|
|
335
|
-
const currentViewTime = scrollLeft / pixelsPerSec;
|
|
336
|
-
this.scheduleRender();
|
|
337
|
-
}
|
|
338
|
-
updatePosition(currentTime) {
|
|
339
|
-
this.currentPosition = currentTime;
|
|
340
|
-
this.scheduleRender();
|
|
341
|
-
}
|
|
342
|
-
scheduleRender() {
|
|
343
|
-
if (this.renderTimeout) {
|
|
344
|
-
clearTimeout(this.renderTimeout);
|
|
345
|
-
}
|
|
346
|
-
this.renderTimeout = window.setTimeout(() => {
|
|
347
|
-
this.renderVisibleWindow();
|
|
348
|
-
}, 16); // 60fps
|
|
349
|
-
}
|
|
350
|
-
renderVisibleWindow() {
|
|
351
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
352
|
-
var _a;
|
|
353
|
-
if (this.isRendering || !this.buffer)
|
|
354
|
-
return;
|
|
355
|
-
this.isRendering = true;
|
|
356
|
-
try {
|
|
357
|
-
const wrapper = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getWrapper();
|
|
358
|
-
if (!wrapper)
|
|
359
|
-
return;
|
|
360
|
-
// Use helper functions for consistency
|
|
361
|
-
const scrollLeft = this.getScrollLeft(wrapper);
|
|
362
|
-
const actualViewportWidth = this.getViewportWidth(wrapper);
|
|
363
|
-
const pixelsPerSec = this.getPixelsPerSecond();
|
|
364
|
-
const totalAudioDuration = this.buffer.duration;
|
|
365
|
-
const visibleStartTime = scrollLeft / pixelsPerSec;
|
|
366
|
-
const visibleEndTime = (scrollLeft + actualViewportWidth) / pixelsPerSec;
|
|
367
|
-
// Reasonable buffer time based on visible duration
|
|
368
|
-
const visibleDuration = visibleEndTime - visibleStartTime;
|
|
369
|
-
let bufferTimeSeconds = Math.min(2, visibleDuration * 0.5); // Buffer is at most half the visible duration or 2s
|
|
370
|
-
if (visibleDuration > 30) {
|
|
371
|
-
console.warn(`⚠️ Large visible duration: ${visibleDuration.toFixed(1)}s - limiting buffer`);
|
|
372
|
-
bufferTimeSeconds = 1; // Smaller buffer for very zoomed out views
|
|
373
|
-
}
|
|
374
|
-
const windowStartTime = Math.max(0, visibleStartTime - bufferTimeSeconds);
|
|
375
|
-
const windowEndTime = Math.min(this.buffer.duration, visibleEndTime + bufferTimeSeconds);
|
|
376
|
-
// Generate segments for this window
|
|
377
|
-
yield this.generateSegments(windowStartTime, windowEndTime);
|
|
378
|
-
// Don't clean up old segments - keep them all in memory for performance
|
|
379
|
-
}
|
|
380
|
-
finally {
|
|
381
|
-
this.isRendering = false;
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
generateSegments(startTime, endTime) {
|
|
386
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
387
|
-
if (!this.buffer)
|
|
388
|
-
return;
|
|
389
|
-
const pixelsPerSec = this.getPixelsPerSecond();
|
|
390
|
-
const containerWidth = this.getWidth(); // Get full container width
|
|
391
|
-
const totalAudioDuration = this.buffer.duration;
|
|
392
|
-
// Progressive loading always uses fixed segment sizes, never fill container mode
|
|
393
|
-
const isProgressiveLoadCall = this.isProgressiveLoading && endTime - startTime <= 35; // Progressive loading uses ~30s segments
|
|
394
|
-
// Calculate if this is a short audio that should fill the container
|
|
395
|
-
const totalAudioPixelWidth = totalAudioDuration * pixelsPerSec;
|
|
396
|
-
const shouldFillContainer = !isProgressiveLoadCall && totalAudioPixelWidth <= containerWidth && totalAudioDuration <= 60; // 60s max for fill mode
|
|
397
|
-
let segmentPixelWidth;
|
|
398
|
-
let segmentDuration;
|
|
399
|
-
if (isProgressiveLoadCall) {
|
|
400
|
-
// For progressive loading, respect the requested time range exactly
|
|
401
|
-
segmentDuration = endTime - startTime;
|
|
402
|
-
segmentPixelWidth = segmentDuration * pixelsPerSec;
|
|
403
|
-
}
|
|
404
|
-
else if (shouldFillContainer) {
|
|
405
|
-
// For short audio, create one segment that fills the entire container width
|
|
406
|
-
segmentPixelWidth = containerWidth;
|
|
407
|
-
segmentDuration = totalAudioDuration; // Use full audio duration
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
// For long audio viewport rendering, use windowing approach
|
|
411
|
-
segmentPixelWidth = 15000; // 15000 pixels per segment
|
|
412
|
-
segmentDuration = segmentPixelWidth / pixelsPerSec; // Calculate duration based on pixel width
|
|
413
|
-
}
|
|
414
|
-
// Show existing segments
|
|
415
|
-
if (this.segments.size > 0) {
|
|
416
|
-
for (const [key, segment] of this.segments) {
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// Check coverage first to avoid duplicate work
|
|
420
|
-
const uncoveredRanges = this.findUncoveredTimeRanges(startTime, endTime, segmentDuration);
|
|
421
|
-
if (uncoveredRanges.length === 0) {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
let newSegmentsCreated = 0;
|
|
425
|
-
// Only generate segments for uncovered ranges
|
|
426
|
-
for (const range of uncoveredRanges) {
|
|
427
|
-
// Create segments covering this uncovered range
|
|
428
|
-
for (let time = range.start; time < range.end; time += segmentDuration) {
|
|
429
|
-
const segmentStart = time;
|
|
430
|
-
const segmentEnd = Math.min(time + segmentDuration, range.end, this.buffer.duration);
|
|
431
|
-
const segmentKey = `${Math.floor(segmentStart * 10)}_${Math.floor(segmentEnd * 10)}`;
|
|
432
|
-
// Double-check if segment already exists (shouldn't happen but be safe)
|
|
433
|
-
if (this.segments.has(segmentKey)) {
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
newSegmentsCreated++;
|
|
437
|
-
// Calculate frequency data for this segment
|
|
438
|
-
const freqStartTime = performance.now();
|
|
439
|
-
const frequencies = yield this.calculateFrequencies(segmentStart, segmentEnd);
|
|
440
|
-
const freqEndTime = performance.now();
|
|
441
|
-
if (frequencies && frequencies.length > 0) {
|
|
442
|
-
const segment = {
|
|
443
|
-
startTime: segmentStart,
|
|
444
|
-
endTime: segmentEnd,
|
|
445
|
-
startPixel: shouldFillContainer ? 0 : segmentStart * pixelsPerSec, // Start at 0 for fill mode
|
|
446
|
-
endPixel: shouldFillContainer ? containerWidth : segmentEnd * pixelsPerSec, // End at container width for fill mode
|
|
447
|
-
frequencies: frequencies,
|
|
448
|
-
};
|
|
449
|
-
this.segments.set(segmentKey, segment);
|
|
450
|
-
// Render this segment
|
|
451
|
-
const renderStartTime = performance.now();
|
|
452
|
-
yield this.renderSegment(segment);
|
|
453
|
-
const renderEndTime = performance.now();
|
|
454
|
-
// Emit progress update
|
|
455
|
-
this.emitProgress();
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// Start progressive loading if not already running
|
|
462
|
-
if (!this.isProgressiveLoading) {
|
|
463
|
-
this.startProgressiveLoading();
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
findUncoveredTimeRanges(startTime, endTime, segmentDuration) {
|
|
468
|
-
// Get all existing segments sorted by start time
|
|
469
|
-
const existingSegments = Array.from(this.segments.values()).sort((a, b) => a.startTime - b.startTime);
|
|
470
|
-
const uncoveredRanges = [];
|
|
471
|
-
let currentTime = startTime;
|
|
472
|
-
for (const segment of existingSegments) {
|
|
473
|
-
// If there's a gap before this segment
|
|
474
|
-
if (currentTime < segment.startTime && currentTime < endTime) {
|
|
475
|
-
const gapEnd = Math.min(segment.startTime, endTime);
|
|
476
|
-
uncoveredRanges.push({
|
|
477
|
-
start: currentTime,
|
|
478
|
-
end: gapEnd,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
// Move past this segment
|
|
482
|
-
currentTime = Math.max(currentTime, segment.endTime);
|
|
483
|
-
// If we've covered the requested range, stop
|
|
484
|
-
if (currentTime >= endTime) {
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
// If there's still uncovered time at the end
|
|
489
|
-
if (currentTime < endTime) {
|
|
490
|
-
uncoveredRanges.push({
|
|
491
|
-
start: currentTime,
|
|
492
|
-
end: endTime,
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
return uncoveredRanges;
|
|
496
|
-
}
|
|
497
|
-
startProgressiveLoading() {
|
|
498
|
-
if (this.isProgressiveLoading || !this.buffer || !this.progressiveLoading)
|
|
499
|
-
return;
|
|
500
|
-
this.isProgressiveLoading = true;
|
|
501
|
-
this.nextProgressiveSegmentTime = 0; // Start from the beginning
|
|
502
|
-
// Start loading after a short delay to not interfere with user interactions
|
|
503
|
-
this.progressiveLoadTimeout = window.setTimeout(() => {
|
|
504
|
-
this.progressiveLoadNextSegment();
|
|
505
|
-
}, 1000); // Wait 1 second before starting
|
|
506
|
-
}
|
|
507
|
-
progressiveLoadNextSegment() {
|
|
508
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
509
|
-
if (!this.buffer || !this.isProgressiveLoading)
|
|
510
|
-
return;
|
|
511
|
-
// For progressive loading, use fixed time-based segments (not pixel-based)
|
|
512
|
-
const segmentDuration = 30; // 30 seconds per segment for progressive loading
|
|
513
|
-
const totalDuration = this.buffer.duration;
|
|
514
|
-
// Check if we've reached the end
|
|
515
|
-
if (this.nextProgressiveSegmentTime >= totalDuration) {
|
|
516
|
-
this._stopProgressiveLoading();
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const segmentStart = this.nextProgressiveSegmentTime;
|
|
520
|
-
const segmentEnd = Math.min(segmentStart + segmentDuration, totalDuration);
|
|
521
|
-
// Check if this segment is already loaded
|
|
522
|
-
const segmentKey = `${Math.floor(segmentStart * 10)}_${Math.floor(segmentEnd * 10)}`;
|
|
523
|
-
const isAlreadyLoaded = this.segments.has(segmentKey);
|
|
524
|
-
if (!isAlreadyLoaded) {
|
|
525
|
-
try {
|
|
526
|
-
yield this.generateSegments(segmentStart, segmentEnd);
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
console.warn('Progressive loading failed:', error);
|
|
530
|
-
this._stopProgressiveLoading();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
// Move to next segment
|
|
535
|
-
this.nextProgressiveSegmentTime = segmentEnd;
|
|
536
|
-
// Schedule next progressive load
|
|
537
|
-
this.progressiveLoadTimeout = window.setTimeout(() => {
|
|
538
|
-
this.progressiveLoadNextSegment();
|
|
539
|
-
}, 2000); // Wait 2 seconds between segments
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
_stopProgressiveLoading() {
|
|
543
|
-
this.isProgressiveLoading = false;
|
|
544
|
-
if (this.progressiveLoadTimeout) {
|
|
545
|
-
clearTimeout(this.progressiveLoadTimeout);
|
|
546
|
-
this.progressiveLoadTimeout = null;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/** Get the current loading progress as a percentage (0-100) */
|
|
550
|
-
getLoadingProgress() {
|
|
551
|
-
if (!this.buffer)
|
|
552
|
-
return 0;
|
|
553
|
-
const totalDuration = this.buffer.duration;
|
|
554
|
-
if (totalDuration === 0)
|
|
555
|
-
return 100;
|
|
556
|
-
if (!this.isProgressiveLoading && this.segments.size === 0)
|
|
557
|
-
return 0;
|
|
558
|
-
// Calculate progress based on how far we've progressed through the audio
|
|
559
|
-
const progress = Math.min(100, (this.nextProgressiveSegmentTime / totalDuration) * 100);
|
|
560
|
-
// If progressive loading is complete, return 100%
|
|
561
|
-
if (!this.isProgressiveLoading && this.nextProgressiveSegmentTime >= totalDuration) {
|
|
562
|
-
return 100;
|
|
563
|
-
}
|
|
564
|
-
return progress;
|
|
565
|
-
}
|
|
566
|
-
emitProgress() {
|
|
567
|
-
const progress = this.getLoadingProgress() / 100; // Convert to 0-1 range
|
|
568
|
-
this.emit('progress', progress);
|
|
569
|
-
}
|
|
570
|
-
calculateFrequencies(startTime, endTime) {
|
|
571
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
572
|
-
if (!this.buffer)
|
|
573
|
-
return [];
|
|
574
|
-
const calcStartTime = performance.now();
|
|
575
|
-
const sampleRate = this.buffer.sampleRate;
|
|
576
|
-
const channels = this.options.splitChannels ? this.buffer.numberOfChannels : 1;
|
|
577
|
-
// Try to use web worker first
|
|
578
|
-
if (this.worker) {
|
|
579
|
-
try {
|
|
580
|
-
const result = yield this.calculateFrequenciesWithWorker(startTime, endTime);
|
|
581
|
-
const totalTime = performance.now() - calcStartTime;
|
|
582
|
-
return result;
|
|
583
|
-
}
|
|
584
|
-
catch (error) {
|
|
585
|
-
console.warn('Worker calculation failed, falling back to main thread:', error);
|
|
586
|
-
// Fall through to main thread calculation
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
// Fallback to main thread calculation
|
|
590
|
-
return this.calculateFrequenciesMainThread(startTime, endTime);
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
calculateFrequenciesWithWorker(startTime, endTime) {
|
|
594
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
595
|
-
if (!this.buffer || !this.worker) {
|
|
596
|
-
throw new Error('Worker not available');
|
|
597
|
-
}
|
|
598
|
-
const sampleRate = this.buffer.sampleRate;
|
|
599
|
-
const channels = this.options.splitChannels ? this.buffer.numberOfChannels : 1;
|
|
600
|
-
// Calculate noverlap
|
|
601
|
-
let noverlap = this.noverlap;
|
|
602
|
-
if (!noverlap) {
|
|
603
|
-
const segmentDuration = endTime - startTime;
|
|
604
|
-
const pixelsPerSec = this.getPixelsPerSecond();
|
|
605
|
-
const segmentWidth = segmentDuration * pixelsPerSec;
|
|
606
|
-
const startSample = Math.floor(startTime * sampleRate);
|
|
607
|
-
const endSample = Math.floor(endTime * sampleRate);
|
|
608
|
-
const uniqueSamplesPerPx = (endSample - startSample) / segmentWidth;
|
|
609
|
-
noverlap = Math.max(0, Math.round(this.fftSamples - uniqueSamplesPerPx));
|
|
610
|
-
}
|
|
611
|
-
// Prepare audio data for worker
|
|
612
|
-
const audioData = [];
|
|
613
|
-
for (let c = 0; c < channels; c++) {
|
|
614
|
-
audioData.push(this.buffer.getChannelData(c));
|
|
615
|
-
}
|
|
616
|
-
// Generate unique ID for this request
|
|
617
|
-
const id = `${Date.now()}_${Math.random()}`;
|
|
618
|
-
// Create promise for worker response
|
|
619
|
-
const promise = new Promise((resolve, reject) => {
|
|
620
|
-
this.workerPromises.set(id, { resolve, reject });
|
|
621
|
-
// Set timeout to avoid hanging
|
|
622
|
-
setTimeout(() => {
|
|
623
|
-
if (this.workerPromises.has(id)) {
|
|
624
|
-
this.workerPromises.delete(id);
|
|
625
|
-
reject(new Error('Worker timeout'));
|
|
626
|
-
}
|
|
627
|
-
}, 30000); // 30 second timeout
|
|
628
|
-
});
|
|
629
|
-
// Send message to worker
|
|
630
|
-
this.worker.postMessage({
|
|
631
|
-
type: 'calculateFrequencies',
|
|
632
|
-
id,
|
|
633
|
-
audioData,
|
|
634
|
-
options: {
|
|
635
|
-
startTime,
|
|
636
|
-
endTime,
|
|
637
|
-
sampleRate,
|
|
638
|
-
fftSamples: this.fftSamples,
|
|
639
|
-
windowFunc: this.windowFunc,
|
|
640
|
-
alpha: this.alpha,
|
|
641
|
-
noverlap,
|
|
642
|
-
scale: this.scale,
|
|
643
|
-
gainDB: this.gainDB,
|
|
644
|
-
rangeDB: this.rangeDB,
|
|
645
|
-
splitChannels: this.options.splitChannels || false,
|
|
646
|
-
},
|
|
647
|
-
});
|
|
648
|
-
return promise;
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
calculateFrequenciesMainThread(startTime, endTime) {
|
|
652
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
653
|
-
if (!this.buffer)
|
|
654
|
-
return [];
|
|
655
|
-
const sampleRate = this.buffer.sampleRate;
|
|
656
|
-
const startSample = Math.floor(startTime * sampleRate);
|
|
657
|
-
const endSample = Math.floor(endTime * sampleRate);
|
|
658
|
-
const channels = this.options.splitChannels ? this.buffer.numberOfChannels : 1;
|
|
659
|
-
// Initialize FFT if needed
|
|
660
|
-
if (!this.fft) {
|
|
661
|
-
this.fft = new FFT(this.fftSamples, sampleRate, this.windowFunc, this.alpha);
|
|
662
|
-
}
|
|
663
|
-
// Calculate noverlap like the normal plugin
|
|
664
|
-
let noverlap = this.noverlap;
|
|
665
|
-
if (!noverlap) {
|
|
666
|
-
const segmentDuration = endTime - startTime;
|
|
667
|
-
const pixelsPerSec = this.getPixelsPerSecond();
|
|
668
|
-
const segmentWidth = segmentDuration * pixelsPerSec;
|
|
669
|
-
const uniqueSamplesPerPx = (endSample - startSample) / segmentWidth;
|
|
670
|
-
noverlap = Math.max(0, Math.round(this.fftSamples - uniqueSamplesPerPx));
|
|
671
|
-
}
|
|
672
|
-
// OPTIMIZATION: For windowed mode, reduce overlap to speed up processing
|
|
673
|
-
const maxOverlap = this.fftSamples * 0.5;
|
|
674
|
-
noverlap = Math.min(noverlap, maxOverlap);
|
|
675
|
-
const minHopSize = Math.max(64, this.fftSamples * 0.25);
|
|
676
|
-
const hopSize = Math.max(minHopSize, this.fftSamples - noverlap);
|
|
677
|
-
const frequencies = [];
|
|
678
|
-
const fftStartTime = performance.now();
|
|
679
|
-
let totalFFTs = 0;
|
|
680
|
-
for (let c = 0; c < channels; c++) {
|
|
681
|
-
const channelData = this.buffer.getChannelData(c);
|
|
682
|
-
const channelFreq = [];
|
|
683
|
-
for (let sample = startSample; sample + this.fftSamples < endSample; sample += hopSize) {
|
|
684
|
-
const segment = channelData.slice(sample, sample + this.fftSamples);
|
|
685
|
-
let spectrum = this.fft.calculateSpectrum(segment);
|
|
686
|
-
totalFFTs++;
|
|
687
|
-
// Apply filter bank if needed
|
|
688
|
-
const filterBank = this.getFilterBank(sampleRate);
|
|
689
|
-
if (filterBank) {
|
|
690
|
-
spectrum = applyFilterBank(spectrum, filterBank);
|
|
691
|
-
}
|
|
692
|
-
// Convert to uint8 color indices
|
|
693
|
-
const freqBins = new Uint8Array(spectrum.length);
|
|
694
|
-
const gainPlusRange = this.gainDB + this.rangeDB;
|
|
695
|
-
for (let j = 0; j < spectrum.length; j++) {
|
|
696
|
-
const magnitude = spectrum[j] > 1e-12 ? spectrum[j] : 1e-12;
|
|
697
|
-
const valueDB = 20 * Math.log10(magnitude);
|
|
698
|
-
if (valueDB < -gainPlusRange) {
|
|
699
|
-
freqBins[j] = 0;
|
|
700
|
-
}
|
|
701
|
-
else if (valueDB > -this.gainDB) {
|
|
702
|
-
freqBins[j] = 255;
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
freqBins[j] = Math.round(((valueDB + this.gainDB) / this.rangeDB) * 255);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
channelFreq.push(freqBins);
|
|
709
|
-
}
|
|
710
|
-
frequencies.push(channelFreq);
|
|
711
|
-
}
|
|
712
|
-
const fftEndTime = performance.now();
|
|
713
|
-
return frequencies;
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
renderSegment(segment) {
|
|
717
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
718
|
-
var _a;
|
|
719
|
-
const segmentWidth = segment.endPixel - segment.startPixel;
|
|
720
|
-
const totalHeight = this.height * segment.frequencies.length;
|
|
721
|
-
// Create canvas for this segment
|
|
722
|
-
const canvas = document.createElement('canvas');
|
|
723
|
-
canvas.width = Math.round(segmentWidth);
|
|
724
|
-
canvas.height = Math.round(totalHeight);
|
|
725
|
-
canvas.style.position = 'absolute';
|
|
726
|
-
canvas.style.left = `${segment.startPixel}px`;
|
|
727
|
-
canvas.style.top = '0';
|
|
728
|
-
canvas.style.width = `${segmentWidth}px`;
|
|
729
|
-
canvas.style.height = `${totalHeight}px`;
|
|
730
|
-
const ctx = canvas.getContext('2d');
|
|
731
|
-
if (!ctx)
|
|
732
|
-
return;
|
|
733
|
-
// Get frequency scaling parameters like the normal plugin
|
|
734
|
-
const freqFrom = ((_a = this.buffer) === null || _a === void 0 ? void 0 : _a.sampleRate) ? this.buffer.sampleRate / 2 : 0;
|
|
735
|
-
const freqMin = this.frequencyMin;
|
|
736
|
-
const freqMax = this.frequencyMax || freqFrom;
|
|
737
|
-
// Render frequency data to canvas with proper scaling
|
|
738
|
-
for (let c = 0; c < segment.frequencies.length; c++) {
|
|
739
|
-
yield this.renderChannelToCanvas(segment.frequencies[c], ctx, segmentWidth, this.height, c * this.height, freqFrom, freqMin, freqMax);
|
|
740
|
-
}
|
|
741
|
-
// Add canvas to container
|
|
742
|
-
segment.canvas = canvas;
|
|
743
|
-
this.canvasContainer.appendChild(canvas);
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
renderChannelToCanvas(channelFreq, ctx, width, height, yOffset, freqFrom, freqMin, freqMax) {
|
|
747
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
748
|
-
if (channelFreq.length === 0)
|
|
749
|
-
return;
|
|
750
|
-
const freqBins = channelFreq[0].length;
|
|
751
|
-
const imageData = new ImageData(channelFreq.length, freqBins);
|
|
752
|
-
const data = imageData.data;
|
|
753
|
-
// Fill image data
|
|
754
|
-
for (let i = 0; i < channelFreq.length; i++) {
|
|
755
|
-
const column = channelFreq[i];
|
|
756
|
-
for (let j = 0; j < freqBins; j++) {
|
|
757
|
-
const colorIndex = Math.min(255, Math.max(0, column[j]));
|
|
758
|
-
const color = this.colorMap[colorIndex];
|
|
759
|
-
const pixelIndex = ((freqBins - j - 1) * channelFreq.length + i) * 4;
|
|
760
|
-
data[pixelIndex] = color[0] * 255;
|
|
761
|
-
data[pixelIndex + 1] = color[1] * 255;
|
|
762
|
-
data[pixelIndex + 2] = color[2] * 255;
|
|
763
|
-
data[pixelIndex + 3] = color[3] * 255;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
// Calculate frequency scaling like the normal plugin
|
|
767
|
-
const rMin = hzToScale(freqMin, this.scale) / hzToScale(freqFrom, this.scale);
|
|
768
|
-
const rMax = hzToScale(freqMax, this.scale) / hzToScale(freqFrom, this.scale);
|
|
769
|
-
const rMax1 = Math.min(1, rMax);
|
|
770
|
-
// Use the same frequency scaling approach as the regular spectrogram plugin
|
|
771
|
-
const drawHeight = (height * rMax1) / rMax;
|
|
772
|
-
const drawY = yOffset + height * (1 - rMax1 / rMax);
|
|
773
|
-
const bitmapSourceY = Math.round(freqBins * (1 - rMax1));
|
|
774
|
-
const bitmapSourceHeight = Math.round(freqBins * (rMax1 - rMin));
|
|
775
|
-
// Create and draw bitmap with proper frequency scaling
|
|
776
|
-
const bitmap = yield createImageBitmap(imageData, 0, bitmapSourceY, channelFreq.length, bitmapSourceHeight);
|
|
777
|
-
ctx.drawImage(bitmap, 0, drawY, width, drawHeight);
|
|
778
|
-
// Clean up
|
|
779
|
-
if ('close' in bitmap) {
|
|
780
|
-
bitmap.close();
|
|
781
|
-
}
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
clearAllSegments() {
|
|
785
|
-
for (const segment of this.segments.values()) {
|
|
786
|
-
if (segment.canvas) {
|
|
787
|
-
segment.canvas.remove();
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
this.segments.clear();
|
|
791
|
-
}
|
|
792
|
-
getFilterBank(sampleRate) {
|
|
793
|
-
const numFilters = this.fftSamples / 2;
|
|
794
|
-
return createFilterBankForScale(this.scale, numFilters, this.fftSamples, sampleRate);
|
|
795
|
-
}
|
|
796
|
-
freqType(freq) {
|
|
797
|
-
return freq >= 1000 ? (freq / 1000).toFixed(1) : Math.round(freq);
|
|
798
|
-
}
|
|
799
|
-
unitType(freq) {
|
|
800
|
-
return freq >= 1000 ? 'kHz' : 'Hz';
|
|
801
|
-
}
|
|
802
|
-
getLabelFrequency(index, labelIndex) {
|
|
803
|
-
const scaleMin = hzToScale(this.frequencyMin, this.scale);
|
|
804
|
-
const scaleMax = hzToScale(this.frequencyMax, this.scale);
|
|
805
|
-
return scaleToHz(scaleMin + (index / labelIndex) * (scaleMax - scaleMin), this.scale);
|
|
806
|
-
}
|
|
807
|
-
loadLabels(bgFill, fontSizeFreq, fontSizeUnit, fontType, textColorFreq, textColorUnit, textAlign, container, channels) {
|
|
808
|
-
const frequenciesHeight = this.height;
|
|
809
|
-
bgFill = bgFill || 'rgba(68,68,68,0)';
|
|
810
|
-
fontSizeFreq = fontSizeFreq || '12px';
|
|
811
|
-
fontSizeUnit = fontSizeUnit || '12px';
|
|
812
|
-
fontType = fontType || 'Helvetica';
|
|
813
|
-
textColorFreq = textColorFreq || '#fff';
|
|
814
|
-
textColorUnit = textColorUnit || '#fff';
|
|
815
|
-
textAlign = textAlign || 'center';
|
|
816
|
-
container = container || '#specLabels';
|
|
817
|
-
const bgWidth = 55;
|
|
818
|
-
const getMaxY = frequenciesHeight || 512;
|
|
819
|
-
const labelIndex = 5 * (getMaxY / 256);
|
|
820
|
-
const freqStart = this.frequencyMin;
|
|
821
|
-
// prepare canvas element for labels
|
|
822
|
-
const ctx = this.labelsEl.getContext('2d');
|
|
823
|
-
const dispScale = window.devicePixelRatio;
|
|
824
|
-
this.labelsEl.height = this.height * channels * dispScale;
|
|
825
|
-
this.labelsEl.width = bgWidth * dispScale;
|
|
826
|
-
ctx.scale(dispScale, dispScale);
|
|
827
|
-
if (!ctx) {
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
for (let c = 0; c < channels; c++) {
|
|
831
|
-
// for each channel
|
|
832
|
-
// fill background
|
|
833
|
-
ctx.fillStyle = bgFill;
|
|
834
|
-
ctx.fillRect(0, c * getMaxY, bgWidth, (1 + c) * getMaxY);
|
|
835
|
-
ctx.fill();
|
|
836
|
-
let i;
|
|
837
|
-
// render labels
|
|
838
|
-
for (i = 0; i <= labelIndex; i++) {
|
|
839
|
-
ctx.textAlign = textAlign;
|
|
840
|
-
ctx.textBaseline = 'middle';
|
|
841
|
-
const freq = this.getLabelFrequency(i, labelIndex);
|
|
842
|
-
const label = this.freqType(freq);
|
|
843
|
-
const units = this.unitType(freq);
|
|
844
|
-
const x = 16;
|
|
845
|
-
let y = (1 + c) * getMaxY - (i / labelIndex) * getMaxY;
|
|
846
|
-
// Make sure label remains in view
|
|
847
|
-
y = Math.min(Math.max(y, c * getMaxY + 10), (1 + c) * getMaxY - 10);
|
|
848
|
-
// unit label
|
|
849
|
-
ctx.fillStyle = textColorUnit;
|
|
850
|
-
ctx.font = fontSizeUnit + ' ' + fontType;
|
|
851
|
-
ctx.fillText(units, x + 24, y);
|
|
852
|
-
// freq label
|
|
853
|
-
ctx.fillStyle = textColorFreq;
|
|
854
|
-
ctx.font = fontSizeFreq + ' ' + fontType;
|
|
855
|
-
ctx.fillText(label.toString(), x, y);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
render(audioData) {
|
|
860
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
861
|
-
this.buffer = audioData;
|
|
862
|
-
this.pixelsPerSecond = this.getPixelsPerSecond();
|
|
863
|
-
this.frequencyMax = this.frequencyMax || audioData.sampleRate / 2;
|
|
864
|
-
// Set wrapper height
|
|
865
|
-
const channels = this.options.splitChannels ? audioData.numberOfChannels : 1;
|
|
866
|
-
this.wrapper.style.height = this.height * channels + 'px';
|
|
867
|
-
// Clear existing data and reset progressive loading
|
|
868
|
-
this.clearAllSegments();
|
|
869
|
-
this.nextProgressiveSegmentTime = 0;
|
|
870
|
-
// Render frequency labels if enabled
|
|
871
|
-
if (this.options.labels) {
|
|
872
|
-
this.loadLabels(this.options.labelsBackground, '12px', '12px', '', this.options.labelsColor, this.options.labelsHzColor || this.options.labelsColor, 'center', '#specLabels', channels);
|
|
873
|
-
}
|
|
874
|
-
// Start initial render
|
|
875
|
-
this.scheduleRender();
|
|
876
|
-
this.emit('ready');
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
destroy() {
|
|
880
|
-
this.unAll();
|
|
881
|
-
if (this.renderTimeout) {
|
|
882
|
-
clearTimeout(this.renderTimeout);
|
|
883
|
-
this.renderTimeout = null;
|
|
884
|
-
}
|
|
885
|
-
if (this.qualityUpdateTimeout) {
|
|
886
|
-
clearTimeout(this.qualityUpdateTimeout);
|
|
887
|
-
this.qualityUpdateTimeout = null;
|
|
888
|
-
}
|
|
889
|
-
// Stop progressive loading
|
|
890
|
-
this.stopProgressiveLoading();
|
|
891
|
-
this.nextProgressiveSegmentTime = 0;
|
|
892
|
-
// Clean up worker
|
|
893
|
-
if (this.worker) {
|
|
894
|
-
this.worker.terminate();
|
|
895
|
-
this.worker = null;
|
|
896
|
-
}
|
|
897
|
-
// Clear any pending worker promises
|
|
898
|
-
for (const [id, promise] of this.workerPromises) {
|
|
899
|
-
promise.reject(new Error('Plugin destroyed'));
|
|
900
|
-
}
|
|
901
|
-
this.workerPromises.clear();
|
|
902
|
-
this.clearAllSegments();
|
|
903
|
-
// Clean up DOM elements properly
|
|
904
|
-
if (this.canvasContainer) {
|
|
905
|
-
this.canvasContainer.remove();
|
|
906
|
-
this.canvasContainer = null;
|
|
907
|
-
}
|
|
908
|
-
if (this.wrapper) {
|
|
909
|
-
this.wrapper.remove();
|
|
910
|
-
this.wrapper = null;
|
|
911
|
-
}
|
|
912
|
-
if (this.labelsEl) {
|
|
913
|
-
this.labelsEl.remove();
|
|
914
|
-
this.labelsEl = null;
|
|
915
|
-
}
|
|
916
|
-
// Reset state for potential re-initialization
|
|
917
|
-
this.container = null;
|
|
918
|
-
this.buffer = null;
|
|
919
|
-
this.fft = null;
|
|
920
|
-
this.isRendering = false;
|
|
921
|
-
this.currentPosition = 0;
|
|
922
|
-
this.pixelsPerSecond = 0;
|
|
923
|
-
super.destroy();
|
|
924
|
-
}
|
|
925
|
-
// Add width calculation methods like the normal plugin
|
|
926
|
-
getWidth() {
|
|
927
|
-
var _a, _b;
|
|
928
|
-
return ((_b = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getWrapper()) === null || _b === void 0 ? void 0 : _b.offsetWidth) || 0;
|
|
929
|
-
}
|
|
930
|
-
getPixelsPerSecond() {
|
|
931
|
-
var _a;
|
|
932
|
-
// Handle default case when no zoom is specified
|
|
933
|
-
const minPxPerSec = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.options.minPxPerSec;
|
|
934
|
-
if (minPxPerSec && minPxPerSec > 0) {
|
|
935
|
-
return minPxPerSec;
|
|
936
|
-
}
|
|
937
|
-
// For windowed mode, enforce a minimum zoom level so we never try to fit entire audio on screen
|
|
938
|
-
const WINDOWED_MIN_PX_PER_SEC = 50; // At least 50 pixels per second for windowed mode
|
|
939
|
-
// Fallback: calculate based on wrapper width and audio duration
|
|
940
|
-
if (this.buffer) {
|
|
941
|
-
const wrapperWidth = this.getWidth();
|
|
942
|
-
const calculatedPxPerSec = wrapperWidth > 0 ? wrapperWidth / this.buffer.duration : 100;
|
|
943
|
-
// For windowed mode, we want to show only a small portion of audio at a time
|
|
944
|
-
// Use the maximum of calculated value and our minimum to ensure reasonable zoom
|
|
945
|
-
const finalPxPerSec = Math.max(calculatedPxPerSec, WINDOWED_MIN_PX_PER_SEC);
|
|
946
|
-
return finalPxPerSec;
|
|
947
|
-
}
|
|
948
|
-
return WINDOWED_MIN_PX_PER_SEC;
|
|
949
|
-
}
|
|
950
|
-
/** Stop progressive loading if it's currently running */
|
|
951
|
-
stopProgressiveLoading() {
|
|
952
|
-
this.isProgressiveLoading = false;
|
|
953
|
-
if (this.progressiveLoadTimeout) {
|
|
954
|
-
clearTimeout(this.progressiveLoadTimeout);
|
|
955
|
-
this.progressiveLoadTimeout = null;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
/** Restart progressive loading from the beginning */
|
|
959
|
-
restartProgressiveLoading() {
|
|
960
|
-
this.stopProgressiveLoading();
|
|
961
|
-
this.nextProgressiveSegmentTime = 0;
|
|
962
|
-
if (this.progressiveLoading) {
|
|
963
|
-
this.startProgressiveLoading();
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
export default WindowedSpectrogramPlugin;
|
|
1
|
+
function e(e,t,s,i){return new(s||(s=Promise))((function(r,n){function o(e){try{a(i.next(e))}catch(e){n(e)}}function l(e){try{a(i.throw(e))}catch(e){n(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(o,l)}a((i=i.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{constructor(){this.listeners={}}on(e,t,s){if(this.listeners[e]||(this.listeners[e]=new Set),null==s?void 0:s.once){const s=(...i)=>{this.un(e,s),t(...i)};return this.listeners[e].add(s),()=>this.un(e,s)}return this.listeners[e].add(t),()=>this.un(e,t)}un(e,t){var s;null===(s=this.listeners[e])||void 0===s||s.delete(t)}once(e,t){return this.on(e,t,{once:!0})}unAll(){this.listeners={}}emit(e,...t){this.listeners[e]&&this.listeners[e].forEach((e=>e(...t)))}}class s extends t{constructor(e){super(),this.subscriptions=[],this.isDestroyed=!1,this.options=e}onInit(){}_init(e){this.isDestroyed&&(this.subscriptions=[],this.isDestroyed=!1),this.wavesurfer=e,this.onInit()}destroy(){this.emit("destroy"),this.subscriptions.forEach((e=>e())),this.subscriptions=[],this.isDestroyed=!0,this.wavesurfer=void 0}}function i(e,t){const s=t.xmlns?document.createElementNS(t.xmlns,e):document.createElement(e);for(const[e,r]of Object.entries(t))if("children"===e&&r)for(const[e,t]of Object.entries(r))t instanceof Node?s.appendChild(t):"string"==typeof t?s.appendChild(document.createTextNode(t)):s.appendChild(i(e,t));else"style"===e?Object.assign(s.style,r):"textContent"===e?s.textContent=r:s.setAttribute(e,r.toString());return s}function r(e,t,s){const r=i(e,t||{});return null==s||s.appendChild(r),r}const n=1e3*Math.log(10)/107.939;function o(e){return 2595*Math.log10(1+e/700)}function l(e){return 700*(Math.pow(10,e/2595)-1)}function a(e){return Math.log10(Math.max(1,e))}function h(e){return Math.pow(10,e)}function c(e){let t=26.81*e/(1960+e)-.53;return t<2&&(t+=.15*(2-t)),t>20.1&&(t+=.22*(t-20.1)),t}function d(e){return e<2&&(e=(e-.3)/.85),e>20.1&&(e=(e+4.422)/1.22),(e+.53)/(26.28-e)*1960}function u(e){return n*Math.log10(1+.00437*e)}function f(e){return(Math.pow(10,e/n)-1)/.00437}function m(e,t){switch(t){case"mel":return o(e);case"logarithmic":return a(e);case"bark":return c(e);case"erb":return u(e);default:return e}}function p(e,t,s,i,r){const n=i(0),o=i(s/2),l=Array.from({length:e},(()=>Array(t/2+1).fill(0))),a=s/t;for(let t=0;t<e;t++){const s=r(n+t/e*(o-n)),i=Math.floor(s/a),h=i*a,c=(s-h)/((i+1)*a-h);l[t][i]=1-c,l[t][i+1]=c}return l}function g(e,t){const s=t.length,i=Float32Array.from({length:s},(()=>0));for(let r=0;r<s;r++)for(let s=0;s<e.length;s++)i[r]+=e[s]*t[r][s];return i}const b={gray:()=>{const e=[];for(let t=0;t<256;t++){const s=(255-t)/256;e.push([s,s,s,1])}return e},igray:()=>{const e=[];for(let t=0;t<256;t++){const s=t/256;e.push([s,s,s,1])}return e},roseus:()=>[[.004528,.004341,.004307,1],[.005625,.006156,.00601,1],[.006628,.008293,.008161,1],[.007551,.010738,.01079,1],[.008382,.013482,.013941,1],[.009111,.01652,.017662,1],[.009727,.019846,.022009,1],[.010223,.023452,.027035,1],[.010593,.027331,.032799,1],[.010833,.031475,.039361,1],[.010941,.035875,.046415,1],[.010918,.04052,.053597,1],[.010768,.045158,.060914,1],[.010492,.049708,.068367,1],[.010098,.054171,.075954,1],[.009594,.058549,.083672,1],[.008989,.06284,.091521,1],[.008297,.067046,.099499,1],[.00753,.071165,.107603,1],[.006704,.075196,.11583,1],[.005838,.07914,.124178,1],[.004949,.082994,.132643,1],[.004062,.086758,.141223,1],[.003198,.09043,.149913,1],[.002382,.09401,.158711,1],[.001643,.097494,.167612,1],[.001009,.100883,.176612,1],[514e-6,.104174,.185704,1],[187e-6,.107366,.194886,1],[66e-6,.110457,.204151,1],[186e-6,.113445,.213496,1],[587e-6,.116329,.222914,1],[.001309,.119106,.232397,1],[.002394,.121776,.241942,1],[.003886,.124336,.251542,1],[.005831,.126784,.261189,1],[.008276,.12912,.270876,1],[.011268,.131342,.280598,1],[.014859,.133447,.290345,1],[.0191,.135435,.300111,1],[.024043,.137305,.309888,1],[.029742,.139054,.319669,1],[.036252,.140683,.329441,1],[.043507,.142189,.339203,1],[.050922,.143571,.348942,1],[.058432,.144831,.358649,1],[.066041,.145965,.368319,1],[.073744,.146974,.377938,1],[.081541,.147858,.387501,1],[.089431,.148616,.396998,1],[.097411,.149248,.406419,1],[.105479,.149754,.415755,1],[.113634,.150134,.424998,1],[.121873,.150389,.434139,1],[.130192,.150521,.443167,1],[.138591,.150528,.452075,1],[.147065,.150413,.460852,1],[.155614,.150175,.469493,1],[.164232,.149818,.477985,1],[.172917,.149343,.486322,1],[.181666,.148751,.494494,1],[.190476,.148046,.502493,1],[.199344,.147229,.510313,1],[.208267,.146302,.517944,1],[.217242,.145267,.52538,1],[.226264,.144131,.532613,1],[.235331,.142894,.539635,1],[.24444,.141559,.546442,1],[.253587,.140131,.553026,1],[.262769,.138615,.559381,1],[.271981,.137016,.5655,1],[.281222,.135335,.571381,1],[.290487,.133581,.577017,1],[.299774,.131757,.582404,1],[.30908,.129867,.587538,1],[.318399,.12792,.592415,1],[.32773,.125921,.597032,1],[.337069,.123877,.601385,1],[.346413,.121793,.605474,1],[.355758,.119678,.609295,1],[.365102,.11754,.612846,1],[.374443,.115386,.616127,1],[.383774,.113226,.619138,1],[.393096,.111066,.621876,1],[.402404,.108918,.624343,1],[.411694,.106794,.62654,1],[.420967,.104698,.628466,1],[.430217,.102645,.630123,1],[.439442,.100647,.631513,1],[.448637,.098717,.632638,1],[.457805,.096861,.633499,1],[.46694,.095095,.6341,1],[.47604,.093433,.634443,1],[.485102,.091885,.634532,1],[.494125,.090466,.63437,1],[.503104,.08919,.633962,1],[.512041,.088067,.633311,1],[.520931,.087108,.63242,1],[.529773,.086329,.631297,1],[.538564,.085738,.629944,1],[.547302,.085346,.628367,1],[.555986,.085162,.626572,1],[.564615,.08519,.624563,1],[.573187,.085439,.622345,1],[.581698,.085913,.619926,1],[.590149,.086615,.617311,1],[.598538,.087543,.614503,1],[.606862,.0887,.611511,1],[.61512,.090084,.608343,1],[.623312,.09169,.605001,1],[.631438,.093511,.601489,1],[.639492,.095546,.597821,1],[.647476,.097787,.593999,1],[.655389,.100226,.590028,1],[.66323,.102856,.585914,1],[.670995,.105669,.581667,1],[.678686,.108658,.577291,1],[.686302,.111813,.57279,1],[.69384,.115129,.568175,1],[.7013,.118597,.563449,1],[.708682,.122209,.558616,1],[.715984,.125959,.553687,1],[.723206,.12984,.548666,1],[.730346,.133846,.543558,1],[.737406,.13797,.538366,1],[.744382,.142209,.533101,1],[.751274,.146556,.527767,1],[.758082,.151008,.522369,1],[.764805,.155559,.516912,1],[.771443,.160206,.511402,1],[.777995,.164946,.505845,1],[.784459,.169774,.500246,1],[.790836,.174689,.494607,1],[.797125,.179688,.488935,1],[.803325,.184767,.483238,1],[.809435,.189925,.477518,1],[.815455,.19516,.471781,1],[.821384,.200471,.466028,1],[.827222,.205854,.460267,1],[.832968,.211308,.454505,1],[.838621,.216834,.448738,1],[.844181,.222428,.442979,1],[.849647,.22809,.43723,1],[.855019,.233819,.431491,1],[.860295,.239613,.425771,1],[.865475,.245471,.420074,1],[.870558,.251393,.414403,1],[.875545,.25738,.408759,1],[.880433,.263427,.403152,1],[.885223,.269535,.397585,1],[.889913,.275705,.392058,1],[.894503,.281934,.386578,1],[.898993,.288222,.381152,1],[.903381,.294569,.375781,1],[.907667,.300974,.370469,1],[.911849,.307435,.365223,1],[.915928,.313953,.360048,1],[.919902,.320527,.354948,1],[.923771,.327155,.349928,1],[.927533,.333838,.344994,1],[.931188,.340576,.340149,1],[.934736,.347366,.335403,1],[.938175,.354207,.330762,1],[.941504,.361101,.326229,1],[.944723,.368045,.321814,1],[.947831,.375039,.317523,1],[.950826,.382083,.313364,1],[.953709,.389175,.309345,1],[.956478,.396314,.305477,1],[.959133,.403499,.301766,1],[.961671,.410731,.298221,1],[.964093,.418008,.294853,1],[.966399,.425327,.291676,1],[.968586,.43269,.288696,1],[.970654,.440095,.285926,1],[.972603,.44754,.28338,1],[.974431,.455025,.281067,1],[.976139,.462547,.279003,1],[.977725,.470107,.277198,1],[.979188,.477703,.275666,1],[.980529,.485332,.274422,1],[.981747,.492995,.273476,1],[.98284,.50069,.272842,1],[.983808,.508415,.272532,1],[.984653,.516168,.27256,1],[.985373,.523948,.272937,1],[.985966,.531754,.273673,1],[.986436,.539582,.274779,1],[.98678,.547434,.276264,1],[.986998,.555305,.278135,1],[.987091,.563195,.280401,1],[.987061,.5711,.283066,1],[.986907,.579019,.286137,1],[.986629,.58695,.289615,1],[.986229,.594891,.293503,1],[.985709,.602839,.297802,1],[.985069,.610792,.302512,1],[.98431,.618748,.307632,1],[.983435,.626704,.313159,1],[.982445,.634657,.319089,1],[.981341,.642606,.32542,1],[.98013,.650546,.332144,1],[.978812,.658475,.339257,1],[.977392,.666391,.346753,1],[.97587,.67429,.354625,1],[.974252,.68217,.362865,1],[.972545,.690026,.371466,1],[.97075,.697856,.380419,1],[.968873,.705658,.389718,1],[.966921,.713426,.399353,1],[.964901,.721157,.409313,1],[.962815,.728851,.419594,1],[.960677,.7365,.430181,1],[.95849,.744103,.44107,1],[.956263,.751656,.452248,1],[.954009,.759153,.463702,1],[.951732,.766595,.475429,1],[.949445,.773974,.487414,1],[.947158,.781289,.499647,1],[.944885,.788535,.512116,1],[.942634,.795709,.524811,1],[.940423,.802807,.537717,1],[.938261,.809825,.550825,1],[.936163,.81676,.564121,1],[.934146,.823608,.577591,1],[.932224,.830366,.59122,1],[.930412,.837031,.604997,1],[.928727,.843599,.618904,1],[.927187,.850066,.632926,1],[.925809,.856432,.647047,1],[.92461,.862691,.661249,1],[.923607,.868843,.675517,1],[.92282,.874884,.689832,1],[.922265,.880812,.704174,1],[.921962,.886626,.718523,1],[.92193,.892323,.732859,1],[.922183,.897903,.747163,1],[.922741,.903364,.76141,1],[.92362,.908706,.77558,1],[.924837,.913928,.789648,1],[.926405,.919031,.80359,1],[.92834,.924015,.817381,1],[.930655,.928881,.830995,1],[.93336,.933631,.844405,1],[.936466,.938267,.857583,1],[.939982,.942791,.870499,1],[.943914,.947207,.883122,1],[.948267,.951519,.895421,1],[.953044,.955732,.907359,1],[.958246,.959852,.918901,1],[.963869,.963887,.930004,1],[.969909,.967845,.940623,1],[.976355,.971737,.950704,1],[.983195,.97558,.960181,1],[.990402,.979395,.968966,1],[.99793,.983217,.97692,1]]};function w(e,t,s,i){switch(this.bufferSize=e,this.sampleRate=t,this.bandwidth=2/e*(t/2),this.sinTable=new Float32Array(e),this.cosTable=new Float32Array(e),this.windowValues=new Float32Array(e),this.reverseTable=new Uint32Array(e),this.peakBand=0,this.peak=0,s){case"bartlett":for(let t=0;t<e;t++)this.windowValues[t]=2/(e-1)*((e-1)/2-Math.abs(t-(e-1)/2));break;case"bartlettHann":for(let t=0;t<e;t++)this.windowValues[t]=.62-.48*Math.abs(t/(e-1)-.5)-.38*Math.cos(2*Math.PI*t/(e-1));break;case"blackman":i=i||.16;for(let t=0;t<e;t++)this.windowValues[t]=(1-i)/2-.5*Math.cos(2*Math.PI*t/(e-1))+i/2*Math.cos(4*Math.PI*t/(e-1));break;case"cosine":for(let t=0;t<e;t++)this.windowValues[t]=Math.cos(Math.PI*t/(e-1)-Math.PI/2);break;case"gauss":i=i||.25;for(let t=0;t<e;t++)this.windowValues[t]=Math.pow(Math.E,-.5*Math.pow((t-(e-1)/2)/(i*(e-1)/2),2));break;case"hamming":for(let t=0;t<e;t++)this.windowValues[t]=.54-.46*Math.cos(2*Math.PI*t/(e-1));break;case"hann":case void 0:for(let t=0;t<e;t++)this.windowValues[t]=.5*(1-Math.cos(2*Math.PI*t/(e-1)));break;case"lanczoz":for(let t=0;t<e;t++)this.windowValues[t]=Math.sin(Math.PI*(2*t/(e-1)-1))/(Math.PI*(2*t/(e-1)-1));break;case"rectangular":for(let t=0;t<e;t++)this.windowValues[t]=1;break;case"triangular":for(let t=0;t<e;t++)this.windowValues[t]=2/e*(e/2-Math.abs(t-(e-1)/2));break;default:throw Error("No such window function '"+s+"'")}let r=1,n=e>>1;for(;r<e;){for(let e=0;e<r;e++)this.reverseTable[e+r]=this.reverseTable[e]+n;r<<=1,n>>=1}for(let t=0;t<e;t++)this.sinTable[t]=Math.sin(-Math.PI/t),this.cosTable[t]=Math.cos(-Math.PI/t);this.calculateSpectrum=function(e){const t=this.bufferSize,s=this.cosTable,i=this.sinTable,r=this.reverseTable,n=new Float32Array(t),o=new Float32Array(t),l=2/this.bufferSize,a=Math.sqrt,h=new Float32Array(t/2);let c,d,u;const f=Math.floor(Math.log(t)/Math.LN2);if(Math.pow(2,f)!==t)throw"Invalid buffer size, must be a power of 2.";if(t!==e.length)throw"Supplied buffer is not the same size as defined FFT. FFT Size: "+t+" Buffer Size: "+e.length;let m,p,g,b,w,v,y,x,S=1;for(let s=0;s<t;s++)n[s]=e[r[s]]*this.windowValues[r[s]],o[s]=0;for(;S<t;){m=s[S],p=i[S],g=1,b=0;for(let e=0;e<S;e++){let s=e;for(;s<t;)w=s+S,v=g*n[w]-b*o[w],y=g*o[w]+b*n[w],n[w]=n[s]-v,o[w]=o[s]-y,n[s]+=v,o[s]+=y,s+=S<<1;x=g,g=x*m-b*p,b=x*p+b*m}S<<=1}for(let e=0,s=t/2;e<s;e++)c=n[e],d=o[e],u=l*a(c*c+d*d),u>this.peak&&(this.peakBand=e,this.peak=u),h[e]=u;return h}}var v=null;try{var y="undefined"!=typeof module&&"function"==typeof module.require&&module.require("worker_threads")||"function"==typeof __non_webpack_require__&&__non_webpack_require__("worker_threads")||"function"==typeof require&&require("worker_threads");v=y.Worker}catch(e){}function x(e,t,s){var i=function(e){return Buffer.from(e,"base64").toString("utf8")}(e),r=i.indexOf("\n",10)+1,n=i.substring(r)+"";return function(e){return new v(n,Object.assign({},e,{eval:!0}))}}function S(e,t,s){var i=function(e){return atob(e)}(e),r=i.indexOf("\n",10)+1,n=i.substring(r)+"",o=new Blob([n],{type:"application/javascript"});return URL.createObjectURL(o)}var M="[object process]"===Object.prototype.toString.call("undefined"!=typeof process?process:0);function L(e,t,s){return M?x(e):function(e){var t;return function(s){return t=t||S(e),new Worker(t,s)}}(e)}var Z=L("Lyogcm9sbHVwLXBsdWdpbi13ZWItd29ya2VyLWxvYWRlciAqLwohZnVuY3Rpb24oKXsidXNlIHN0cmljdCI7Y29uc3QgdD0xZTMqTWF0aC5sb2coMTApLzEwNy45Mzk7ZnVuY3Rpb24gZSh0KXtyZXR1cm4gMjU5NSpNYXRoLmxvZzEwKDErdC83MDApfWZ1bmN0aW9uIGEodCl7cmV0dXJuIDcwMCooTWF0aC5wb3coMTAsdC8yNTk1KS0xKX1mdW5jdGlvbiByKHQpe3JldHVybiBNYXRoLmxvZzEwKE1hdGgubWF4KDEsdCkpfWZ1bmN0aW9uIHModCl7cmV0dXJuIE1hdGgucG93KDEwLHQpfWZ1bmN0aW9uIG4odCl7bGV0IGU9MjYuODEqdC8oMTk2MCt0KS0uNTM7cmV0dXJuIGU8MiYmKGUrPS4xNSooMi1lKSksZT4yMC4xJiYoZSs9LjIyKihlLTIwLjEpKSxlfWZ1bmN0aW9uIG8odCl7cmV0dXJuIHQ8MiYmKHQ9KHQtLjMpLy44NSksdD4yMC4xJiYodD0odCs0LjQyMikvMS4yMiksKHQrLjUzKS8oMjYuMjgtdCkqMTk2MH1mdW5jdGlvbiBsKGUpe3JldHVybiB0Kk1hdGgubG9nMTAoMSsuMDA0MzcqZSl9ZnVuY3Rpb24gaShlKXtyZXR1cm4oTWF0aC5wb3coMTAsZS90KS0xKS8uMDA0Mzd9ZnVuY3Rpb24gaCh0LGUsYSxyLHMpe2NvbnN0IG49cigwKSxvPXIoYS8yKSxsPUFycmF5LmZyb20oe2xlbmd0aDp0fSwoKCk9PkFycmF5KGUvMisxKS5maWxsKDApKSksaT1hL2U7Zm9yKGxldCBlPTA7ZTx0O2UrKyl7Y29uc3QgYT1zKG4rZS90KihvLW4pKSxyPU1hdGguZmxvb3IoYS9pKSxoPXIqaSxjPShhLWgpLygocisxKSppLWgpO2xbZV1bcl09MS1jLGxbZV1bcisxXT1jfXJldHVybiBsfWZ1bmN0aW9uIGModCxlKXtjb25zdCBhPWUubGVuZ3RoLHI9RmxvYXQzMkFycmF5LmZyb20oe2xlbmd0aDphfSwoKCk9PjApKTtmb3IobGV0IHM9MDtzPGE7cysrKWZvcihsZXQgYT0wO2E8dC5sZW5ndGg7YSsrKXJbc10rPXRbYV0qZVtzXVthXTtyZXR1cm4gcn1mdW5jdGlvbiB1KHQsZSxhLHIpe3N3aXRjaCh0aGlzLmJ1ZmZlclNpemU9dCx0aGlzLnNhbXBsZVJhdGU9ZSx0aGlzLmJhbmR3aWR0aD0yL3QqKGUvMiksdGhpcy5zaW5UYWJsZT1uZXcgRmxvYXQzMkFycmF5KHQpLHRoaXMuY29zVGFibGU9bmV3IEZsb2F0MzJBcnJheSh0KSx0aGlzLndpbmRvd1ZhbHVlcz1uZXcgRmxvYXQzMkFycmF5KHQpLHRoaXMucmV2ZXJzZVRhYmxlPW5ldyBVaW50MzJBcnJheSh0KSx0aGlzLnBlYWtCYW5kPTAsdGhpcy5wZWFrPTAsYSl7Y2FzZSJiYXJ0bGV0dCI6Zm9yKGxldCBlPTA7ZTx0O2UrKyl0aGlzLndpbmRvd1ZhbHVlc1tlXT0yLyh0LTEpKigodC0xKS8yLU1hdGguYWJzKGUtKHQtMSkvMikpO2JyZWFrO2Nhc2UiYmFydGxldHRIYW5uIjpmb3IobGV0IGU9MDtlPHQ7ZSsrKXRoaXMud2luZG93VmFsdWVzW2VdPS42Mi0uNDgqTWF0aC5hYnMoZS8odC0xKS0uNSktLjM4Kk1hdGguY29zKDIqTWF0aC5QSSplLyh0LTEpKTticmVhaztjYXNlImJsYWNrbWFuIjpyPXJ8fC4xNjtmb3IobGV0IGU9MDtlPHQ7ZSsrKXRoaXMud2luZG93VmFsdWVzW2VdPSgxLXIpLzItLjUqTWF0aC5jb3MoMipNYXRoLlBJKmUvKHQtMSkpK3IvMipNYXRoLmNvcyg0Kk1hdGguUEkqZS8odC0xKSk7YnJlYWs7Y2FzZSJjb3NpbmUiOmZvcihsZXQgZT0wO2U8dDtlKyspdGhpcy53aW5kb3dWYWx1ZXNbZV09TWF0aC5jb3MoTWF0aC5QSSplLyh0LTEpLU1hdGguUEkvMik7YnJlYWs7Y2FzZSJnYXVzcyI6cj1yfHwuMjU7Zm9yKGxldCBlPTA7ZTx0O2UrKyl0aGlzLndpbmRvd1ZhbHVlc1tlXT1NYXRoLnBvdyhNYXRoLkUsLS41Kk1hdGgucG93KChlLSh0LTEpLzIpLyhyKih0LTEpLzIpLDIpKTticmVhaztjYXNlImhhbW1pbmciOmZvcihsZXQgZT0wO2U8dDtlKyspdGhpcy53aW5kb3dWYWx1ZXNbZV09LjU0LS40NipNYXRoLmNvcygyKk1hdGguUEkqZS8odC0xKSk7YnJlYWs7Y2FzZSJoYW5uIjpjYXNlIHZvaWQgMDpmb3IobGV0IGU9MDtlPHQ7ZSsrKXRoaXMud2luZG93VmFsdWVzW2VdPS41KigxLU1hdGguY29zKDIqTWF0aC5QSSplLyh0LTEpKSk7YnJlYWs7Y2FzZSJsYW5jem96Ijpmb3IobGV0IGU9MDtlPHQ7ZSsrKXRoaXMud2luZG93VmFsdWVzW2VdPU1hdGguc2luKE1hdGguUEkqKDIqZS8odC0xKS0xKSkvKE1hdGguUEkqKDIqZS8odC0xKS0xKSk7YnJlYWs7Y2FzZSJyZWN0YW5ndWxhciI6Zm9yKGxldCBlPTA7ZTx0O2UrKyl0aGlzLndpbmRvd1ZhbHVlc1tlXT0xO2JyZWFrO2Nhc2UidHJpYW5ndWxhciI6Zm9yKGxldCBlPTA7ZTx0O2UrKyl0aGlzLndpbmRvd1ZhbHVlc1tlXT0yL3QqKHQvMi1NYXRoLmFicyhlLSh0LTEpLzIpKTticmVhaztkZWZhdWx0OnRocm93IEVycm9yKCJObyBzdWNoIHdpbmRvdyBmdW5jdGlvbiAnIithKyInIil9bGV0IHM9MSxuPXQ+PjE7Zm9yKDtzPHQ7KXtmb3IobGV0IHQ9MDt0PHM7dCsrKXRoaXMucmV2ZXJzZVRhYmxlW3Qrc109dGhpcy5yZXZlcnNlVGFibGVbdF0rbjtzPDw9MSxuPj49MX1mb3IobGV0IGU9MDtlPHQ7ZSsrKXRoaXMuc2luVGFibGVbZV09TWF0aC5zaW4oLU1hdGguUEkvZSksdGhpcy5jb3NUYWJsZVtlXT1NYXRoLmNvcygtTWF0aC5QSS9lKTt0aGlzLmNhbGN1bGF0ZVNwZWN0cnVtPWZ1bmN0aW9uKHQpe2NvbnN0IGU9dGhpcy5idWZmZXJTaXplLGE9dGhpcy5jb3NUYWJsZSxyPXRoaXMuc2luVGFibGUscz10aGlzLnJldmVyc2VUYWJsZSxuPW5ldyBGbG9hdDMyQXJyYXkoZSksbz1uZXcgRmxvYXQzMkFycmF5KGUpLGw9Mi90aGlzLmJ1ZmZlclNpemUsaT1NYXRoLnNxcnQsaD1uZXcgRmxvYXQzMkFycmF5KGUvMik7bGV0IGMsdSxmO2NvbnN0IE09TWF0aC5mbG9vcihNYXRoLmxvZyhlKS9NYXRoLkxOMik7aWYoTWF0aC5wb3coMixNKSE9PWUpdGhyb3ciSW52YWxpZCBidWZmZXIgc2l6ZSwgbXVzdCBiZSBhIHBvd2VyIG9mIDIuIjtpZihlIT09dC5sZW5ndGgpdGhyb3ciU3VwcGxpZWQgYnVmZmVyIGlzIG5vdCB0aGUgc2FtZSBzaXplIGFzIGRlZmluZWQgRkZULiBGRlQgU2l6ZTogIitlKyIgQnVmZmVyIFNpemU6ICIrdC5sZW5ndGg7bGV0IHcsYixkLHAsZyxtLGsseSxUPTE7Zm9yKGxldCBhPTA7YTxlO2ErKyluW2FdPXRbc1thXV0qdGhpcy53aW5kb3dWYWx1ZXNbc1thXV0sb1thXT0wO2Zvcig7VDxlOyl7dz1hW1RdLGI9cltUXSxkPTEscD0wO2ZvcihsZXQgdD0wO3Q8VDt0Kyspe2xldCBhPXQ7Zm9yKDthPGU7KWc9YStULG09ZCpuW2ddLXAqb1tnXSxrPWQqb1tnXStwKm5bZ10sbltnXT1uW2FdLW0sb1tnXT1vW2FdLWssblthXSs9bSxvW2FdKz1rLGErPVQ8PDE7eT1kLGQ9eSp3LXAqYixwPXkqYitwKnd9VDw8PTF9Zm9yKGxldCB0PTAsYT1lLzI7dDxhO3QrKyljPW5bdF0sdT1vW3RdLGY9bCppKGMqYyt1KnUpLGY+dGhpcy5wZWFrJiYodGhpcy5wZWFrQmFuZD10LHRoaXMucGVhaz1mKSxoW3RdPWY7cmV0dXJuIGh9fWxldCBmPW51bGw7c2VsZi5vbm1lc3NhZ2U9ZnVuY3Rpb24odCl7Y29uc3R7dHlwZTpNLGlkOncsYXVkaW9EYXRhOmIsb3B0aW9uczpkfT10LmRhdGE7aWYoImNhbGN1bGF0ZUZyZXF1ZW5jaWVzIj09PU0pdHJ5e2NvbnN0IHQ9ZnVuY3Rpb24odCxNKXtjb25zdHtzdGFydFRpbWU6dyxlbmRUaW1lOmIsc2FtcGxlUmF0ZTpkLGZmdFNhbXBsZXM6cCx3aW5kb3dGdW5jOmcsYWxwaGE6bSxub3ZlcmxhcDprLHNjYWxlOnksZ2FpbkRCOlQscmFuZ2VEQjpGLHNwbGl0Q2hhbm5lbHM6SX09TSxWPU1hdGguZmxvb3IodypkKSxBPU1hdGguZmxvb3IoYipkKSxQPUk/dC5sZW5ndGg6MTtmJiZmLmJ1ZmZlclNpemU9PT1wfHwoZj1uZXcgdShwLGQsZyxtfHwuMTYpKTtjb25zdCBTPWZ1bmN0aW9uKHQsYyx1LGYpe3N3aXRjaCh0KXtjYXNlIm1lbCI6cmV0dXJuIGgoYyx1LGYsZSxhKTtjYXNlImxvZ2FyaXRobWljIjpyZXR1cm4gaChjLHUsZixyLHMpO2Nhc2UiYmFyayI6cmV0dXJuIGgoYyx1LGYsbixvKTtjYXNlImVyYiI6cmV0dXJuIGgoYyx1LGYsbCxpKTtkZWZhdWx0OnJldHVybiBudWxsfX0oeSxwLzIscCxkKTtsZXQgej1rfHxNYXRoLm1heCgwLE1hdGgucm91bmQoLjUqcCkpO2NvbnN0IHY9LjUqcDt6PU1hdGgubWluKHosdik7Y29uc3QgQj1NYXRoLm1heCg2NCwuMjUqcCkscT1NYXRoLm1heChCLHAteikseD1bXTtmb3IobGV0IGU9MDtlPFA7ZSsrKXtjb25zdCBhPXRbZV0scj1bXTtmb3IobGV0IHQ9Vjt0K3A8QTt0Kz1xKXtjb25zdCBlPWEuc2xpY2UodCx0K3ApO2xldCBzPWYuY2FsY3VsYXRlU3BlY3RydW0oZSk7UyYmKHM9YyhzLFMpKTtjb25zdCBuPW5ldyBVaW50OEFycmF5KHMubGVuZ3RoKSxvPVQrRjtmb3IobGV0IHQ9MDt0PHMubGVuZ3RoO3QrKyl7Y29uc3QgZT1zW3RdPjFlLTEyP3NbdF06MWUtMTIsYT0yMCpNYXRoLmxvZzEwKGUpO25bdF09YTwtbz8wOmE+LVQ/MjU1Ok1hdGgucm91bmQoKGErVCkvRioyNTUpfXIucHVzaChuKX14LnB1c2gocil9cmV0dXJuIHh9KGIsZCksTT17dHlwZToiZnJlcXVlbmNpZXNSZXN1bHQiLGlkOncscmVzdWx0OnR9O3NlbGYucG9zdE1lc3NhZ2UoTSl9Y2F0Y2godCl7Y29uc3QgZT17dHlwZToiZnJlcXVlbmNpZXNSZXN1bHQiLGlkOncsZXJyb3I6dCBpbnN0YW5jZW9mIEVycm9yP3QubWVzc2FnZTpTdHJpbmcodCl9O3NlbGYucG9zdE1lc3NhZ2UoZSl9fX0oKTsKLy8jIHNvdXJjZU1hcHBpbmdVUkw9c3BlY3Ryb2dyYW0td29ya2VyLmpzLm1hcAoK");class W extends s{static create(e){return new W(e||{})}constructor(e){var t,s;super(e),this.segments=new Map,this.buffer=null,this.currentPosition=0,this.pixelsPerSecond=0,this.isRendering=!1,this.renderTimeout=null,this.fft=null,this.progressiveLoadTimeout=null,this.isProgressiveLoading=!1,this.nextProgressiveSegmentTime=0,this.worker=null,this.workerPromises=new Map,this.qualityUpdateTimeout=null,this._onWrapperClick=e=>{const t=this.wrapper.getBoundingClientRect(),s=(e.clientX-t.left)/t.width;this.emit("click",s)},this.container="string"==typeof e.container?document.querySelector(e.container):e.container,this.colorMap=function(e="roseus"){if(e&&"string"!=typeof e){if(e.length<256)throw new Error("Colormap must contain 256 elements");for(let t=0;t<e.length;t++)if(4!==e[t].length)throw new Error("ColorMap entries must contain 4 values");return e}const t=b[e];if(!t)throw Error("No such colormap '"+e+"'");return t()}(e.colorMap),this.fftSamples=e.fftSamples||512,this.height=e.height||200,this.noverlap=e.noverlap||null,this.windowFunc=e.windowFunc||"hann",this.alpha=e.alpha,this.frequencyMin=e.frequencyMin||0,this.frequencyMax=e.frequencyMax||0,this.gainDB=null!==(t=e.gainDB)&&void 0!==t?t:20,this.rangeDB=null!==(s=e.rangeDB)&&void 0!==s?s:80,this.scale=e.scale||"mel",this.windowSize=e.windowSize||30,this.bufferSize=e.bufferSize||5e3,this.progressiveLoading=!0===e.progressiveLoading,this.useWebWorker=!0===e.useWebWorker&&"undefined"!=typeof window,this.numMelFilters=this.fftSamples/2,this.numLogFilters=this.fftSamples/2,this.numBarkFilters=this.fftSamples/2,this.numErbFilters=this.fftSamples/2,this.createWrapper(),this.createCanvas(),this.useWebWorker&&this.initializeWorker()}initializeWorker(){if("undefined"!=typeof window&&"undefined"!=typeof Worker)try{this.worker=new Z,this.worker.onmessage=e=>{const{type:t,id:s,result:i,error:r}=e.data;if("frequenciesResult"===t){const e=this.workerPromises.get(s);e&&(this.workerPromises.delete(s),r?e.reject(new Error(r)):e.resolve(i))}},this.worker.onerror=e=>{console.warn("Spectrogram worker error, falling back to main thread:",e),this.worker=null}}catch(e){console.warn("Failed to initialize worker, falling back to main thread:",e),this.worker=null}else console.warn("Worker not available in this environment, using main thread calculation")}onInit(){this.wrapper||this.createWrapper(),this.canvasContainer||this.createCanvas(),this.container=this.wavesurfer.getWrapper(),this.container.appendChild(this.wrapper),this.wavesurfer.options.fillParent&&Object.assign(this.wrapper.style,{width:"100%",overflowX:"hidden",overflowY:"hidden"}),this.subscriptions.push(this.wavesurfer.on("timeupdate",(e=>{this.updatePosition(e)}))),this.subscriptions.push(this.wavesurfer.on("scroll",(()=>{this.handleScroll()}))),this.subscriptions.push(this.wavesurfer.on("redraw",(()=>this.handleRedraw()))),this.subscriptions.push(this.wavesurfer.on("ready",(()=>{const e=this.wavesurfer.getDecodedData();e&&this.render(e)}))),this.wavesurfer.getDecodedData()&&setTimeout((()=>{this.render(this.wavesurfer.getDecodedData())}),0)}createWrapper(){var e,t;this.wrapper=r("div",{style:{display:"block",position:"relative",userSelect:"none"}}),this.options.labels&&(this.labelsEl=r("canvas",{part:"spec-labels",style:{position:"absolute",zIndex:9,width:"55px",height:"100%"}},this.wrapper)),this._onWrapperClick=(e=this.wrapper,t=this.emit.bind(this),s=>{const i=e.getBoundingClientRect(),r=s.clientX-i.left,n=i.width;t("click",r/n)}),this.wrapper.addEventListener("click",this._onWrapperClick)}createCanvas(){this.canvasContainer=r("div",{style:{position:"absolute",left:0,top:0,width:"100%",height:"100%",zIndex:4}},this.wrapper)}handleRedraw(){const e=this.pixelsPerSecond;this.pixelsPerSecond=this.getPixelsPerSecond(),e!==this.pixelsPerSecond&&this.segments.size>0&&this.updateSegmentPositions(e,this.pixelsPerSecond),this.scheduleRender()}updateSegmentPositions(e,t){for(const e of this.segments.values())if(e.startPixel=e.startTime*t,e.endPixel=e.endTime*t,e.canvas){const t=e.endPixel-e.startPixel;e.canvas.style.left=`${e.startPixel}px`,e.canvas.style.width=`${t}px`}const s=t/e;(s<.5||s>2)&&this.scheduleSegmentQualityUpdate()}scheduleSegmentQualityUpdate(){this.qualityUpdateTimeout&&clearTimeout(this.qualityUpdateTimeout),this.qualityUpdateTimeout=window.setTimeout((()=>{this.updateVisibleSegmentQuality()}),500)}updateVisibleSegmentQuality(){return e(this,void 0,void 0,(function*(){var e;if(!this.buffer)return;const t=null===(e=this.wavesurfer)||void 0===e?void 0:e.getWrapper();if(!t)return;const s=this.getScrollLeft(t),i=this.getViewportWidth(t),r=this.getPixelsPerSecond(),n=s/r,o=(s+i)/r,l=Array.from(this.segments.values()).filter((e=>e.startTime<o&&e.endTime>n));if(0!==l.length)for(const e of l)e.canvas&&(yield this.renderSegment(e))}))}getScrollLeft(e){var t;if(e.scrollLeft)return e.scrollLeft;if(null===(t=e.parentElement)||void 0===t?void 0:t.scrollLeft)return e.parentElement.scrollLeft;if(document.documentElement.scrollLeft)return document.documentElement.scrollLeft;if(document.body.scrollLeft)return document.body.scrollLeft;if(window.scrollX)return window.scrollX;if(window.pageXOffset)return window.pageXOffset;let s=e.parentElement;for(;s;){const e=window.getComputedStyle(s);if(("scroll"===e.overflowX||"auto"===e.overflowX)&&s.scrollLeft>0)return s.scrollLeft;s=s.parentElement}return 0}getViewportWidth(e){var t,s;const i=e.offsetWidth||e.clientWidth,r=(null===(t=e.parentElement)||void 0===t?void 0:t.offsetWidth)||(null===(s=e.parentElement)||void 0===s?void 0:s.clientWidth),n=window.innerWidth;return r&&r<i?r:Math.min(i||800,.8*n)}handleScroll(){var e,t;const s=null===(e=this.wavesurfer)||void 0===e?void 0:e.getWrapper();let i=0;if(null==s?void 0:s.scrollLeft)i=s.scrollLeft;else if(null===(t=null==s?void 0:s.parentElement)||void 0===t?void 0:t.scrollLeft)i=s.parentElement.scrollLeft;else if(document.documentElement.scrollLeft||document.body.scrollLeft)i=document.documentElement.scrollLeft||document.body.scrollLeft;else if(window.scrollX||window.pageXOffset)i=window.scrollX||window.pageXOffset;else if(s){let e=s.parentElement;for(;e&&0===i;){const t=window.getComputedStyle(e);if(("scroll"===t.overflowX||"auto"===t.overflowX)&&e.scrollLeft>0){i=e.scrollLeft;break}e=e.parentElement}}this.getPixelsPerSecond(),this.scheduleRender()}updatePosition(e){this.currentPosition=e,this.scheduleRender()}scheduleRender(){this.renderTimeout&&clearTimeout(this.renderTimeout),this.renderTimeout=window.setTimeout((()=>{this.renderVisibleWindow()}),16)}renderVisibleWindow(){return e(this,void 0,void 0,(function*(){var e;if(!this.isRendering&&this.buffer){this.isRendering=!0;try{const t=null===(e=this.wavesurfer)||void 0===e?void 0:e.getWrapper();if(!t)return;const s=this.getScrollLeft(t),i=this.getViewportWidth(t),r=this.getPixelsPerSecond(),n=(this.buffer.duration,s/r),o=(s+i)/r,l=o-n;let a=Math.min(2,.5*l);l>30&&(console.warn(`⚠️ Large visible duration: ${l.toFixed(1)}s - limiting buffer`),a=1);const h=Math.max(0,n-a),c=Math.min(this.buffer.duration,o+a);yield this.generateSegments(h,c)}finally{this.isRendering=!1}}}))}generateSegments(t,s){return e(this,void 0,void 0,(function*(){if(!this.buffer)return;const e=this.getPixelsPerSecond(),i=this.getWidth(),r=this.buffer.duration,n=this.isProgressiveLoading&&s-t<=35,o=!n&&r*e<=i&&r<=60;let l,a;if(n?(a=s-t,l=a*e):o?(l=i,a=r):(l=15e3,a=l/e),this.segments.size>0)for(const[e,t]of this.segments);const h=this.findUncoveredTimeRanges(t,s,a);if(0!==h.length){for(const t of h)for(let s=t.start;s<t.end;s+=a){const r=s,n=Math.min(s+a,t.end,this.buffer.duration),l=`${Math.floor(10*r)}_${Math.floor(10*n)}`;if(this.segments.has(l))continue;performance.now();const h=yield this.calculateFrequencies(r,n);if(performance.now(),h&&h.length>0){const t={startTime:r,endTime:n,startPixel:o?0:r*e,endPixel:o?i:n*e,frequencies:h};this.segments.set(l,t),performance.now(),yield this.renderSegment(t),performance.now(),this.emitProgress()}}this.isProgressiveLoading||this.startProgressiveLoading()}}))}findUncoveredTimeRanges(e,t,s){const i=Array.from(this.segments.values()).sort(((e,t)=>e.startTime-t.startTime)),r=[];let n=e;for(const e of i){if(n<e.startTime&&n<t){const s=Math.min(e.startTime,t);r.push({start:n,end:s})}if(n=Math.max(n,e.endTime),n>=t)break}return n<t&&r.push({start:n,end:t}),r}startProgressiveLoading(){!this.isProgressiveLoading&&this.buffer&&this.progressiveLoading&&(this.isProgressiveLoading=!0,this.nextProgressiveSegmentTime=0,this.progressiveLoadTimeout=window.setTimeout((()=>{this.progressiveLoadNextSegment()}),1e3))}progressiveLoadNextSegment(){return e(this,void 0,void 0,(function*(){if(!this.buffer||!this.isProgressiveLoading)return;const e=this.buffer.duration;if(this.nextProgressiveSegmentTime>=e)return void this._stopProgressiveLoading();const t=this.nextProgressiveSegmentTime,s=Math.min(t+30,e),i=`${Math.floor(10*t)}_${Math.floor(10*s)}`;if(!this.segments.has(i))try{yield this.generateSegments(t,s)}catch(e){return console.warn("Progressive loading failed:",e),void this._stopProgressiveLoading()}this.nextProgressiveSegmentTime=s,this.progressiveLoadTimeout=window.setTimeout((()=>{this.progressiveLoadNextSegment()}),2e3)}))}_stopProgressiveLoading(){this.isProgressiveLoading=!1,this.progressiveLoadTimeout&&(clearTimeout(this.progressiveLoadTimeout),this.progressiveLoadTimeout=null)}getLoadingProgress(){if(!this.buffer)return 0;const e=this.buffer.duration;if(0===e)return 100;if(!this.isProgressiveLoading&&0===this.segments.size)return 0;const t=Math.min(100,this.nextProgressiveSegmentTime/e*100);return!this.isProgressiveLoading&&this.nextProgressiveSegmentTime>=e?100:t}emitProgress(){const e=this.getLoadingProgress()/100;this.emit("progress",e)}calculateFrequencies(t,s){return e(this,void 0,void 0,(function*(){if(!this.buffer)return[];const e=performance.now();if(this.buffer.sampleRate,!this.options.splitChannels||this.buffer.numberOfChannels,this.worker)try{const e=yield this.calculateFrequenciesWithWorker(t,s);performance.now();return e}catch(e){console.warn("Worker calculation failed, falling back to main thread:",e)}return this.calculateFrequenciesMainThread(t,s)}))}calculateFrequenciesWithWorker(t,s){return e(this,void 0,void 0,(function*(){if(!this.buffer||!this.worker)throw new Error("Worker not available");const e=this.buffer.sampleRate,i=this.options.splitChannels?this.buffer.numberOfChannels:1;let r=this.noverlap;if(!r){const i=(s-t)*this.getPixelsPerSecond(),n=Math.floor(t*e),o=(Math.floor(s*e)-n)/i;r=Math.max(0,Math.round(this.fftSamples-o))}const n=[];for(let e=0;e<i;e++)n.push(this.buffer.getChannelData(e));const o=`${Date.now()}_${Math.random()}`,l=new Promise(((e,t)=>{this.workerPromises.set(o,{resolve:e,reject:t}),setTimeout((()=>{this.workerPromises.has(o)&&(this.workerPromises.delete(o),t(new Error("Worker timeout")))}),3e4)}));return this.worker.postMessage({type:"calculateFrequencies",id:o,audioData:n,options:{startTime:t,endTime:s,sampleRate:e,fftSamples:this.fftSamples,windowFunc:this.windowFunc,alpha:this.alpha,noverlap:r,scale:this.scale,gainDB:this.gainDB,rangeDB:this.rangeDB,splitChannels:this.options.splitChannels||!1}}),l}))}calculateFrequenciesMainThread(t,s){return e(this,void 0,void 0,(function*(){if(!this.buffer)return[];const e=this.buffer.sampleRate,i=Math.floor(t*e),r=Math.floor(s*e),n=this.options.splitChannels?this.buffer.numberOfChannels:1;this.fft||(this.fft=new w(this.fftSamples,e,this.windowFunc,this.alpha));let o=this.noverlap;if(!o){const e=(r-i)/((s-t)*this.getPixelsPerSecond());o=Math.max(0,Math.round(this.fftSamples-e))}const l=.5*this.fftSamples;o=Math.min(o,l);const a=Math.max(64,.25*this.fftSamples),h=Math.max(a,this.fftSamples-o),c=[];performance.now();for(let t=0;t<n;t++){const s=this.buffer.getChannelData(t),n=[];for(let t=i;t+this.fftSamples<r;t+=h){const i=s.slice(t,t+this.fftSamples);let r=this.fft.calculateSpectrum(i);const o=this.getFilterBank(e);o&&(r=g(r,o));const l=new Uint8Array(r.length),a=this.gainDB+this.rangeDB;for(let e=0;e<r.length;e++){const t=r[e]>1e-12?r[e]:1e-12,s=20*Math.log10(t);s<-a?l[e]=0:s>-this.gainDB?l[e]=255:l[e]=Math.round((s+this.gainDB)/this.rangeDB*255)}n.push(l)}c.push(n)}return performance.now(),c}))}renderSegment(t){return e(this,void 0,void 0,(function*(){var e;const s=t.endPixel-t.startPixel,i=this.height*t.frequencies.length,r=document.createElement("canvas");r.width=Math.round(s),r.height=Math.round(i),r.style.position="absolute",r.style.left=`${t.startPixel}px`,r.style.top="0",r.style.width=`${s}px`,r.style.height=`${i}px`;const n=r.getContext("2d");if(!n)return;const o=(null===(e=this.buffer)||void 0===e?void 0:e.sampleRate)?this.buffer.sampleRate/2:0,l=this.frequencyMin,a=this.frequencyMax||o;for(let e=0;e<t.frequencies.length;e++)yield this.renderChannelToCanvas(t.frequencies[e],n,s,this.height,e*this.height,o,l,a);t.canvas=r,this.canvasContainer.appendChild(r)}))}renderChannelToCanvas(t,s,i,r,n,o,l,a){return e(this,void 0,void 0,(function*(){if(0===t.length)return;const e=t[0].length,h=new ImageData(t.length,e),c=h.data;for(let s=0;s<t.length;s++){const i=t[s];for(let r=0;r<e;r++){const n=Math.min(255,Math.max(0,i[r])),o=this.colorMap[n],l=4*((e-r-1)*t.length+s);c[l]=255*o[0],c[l+1]=255*o[1],c[l+2]=255*o[2],c[l+3]=255*o[3]}}const d=m(l,this.scale)/m(o,this.scale),u=m(a,this.scale)/m(o,this.scale),f=Math.min(1,u),p=r*f/u,g=n+r*(1-f/u),b=Math.round(e*(1-f)),w=Math.round(e*(f-d)),v=yield createImageBitmap(h,0,b,t.length,w);s.drawImage(v,0,g,i,p),"close"in v&&v.close()}))}clearAllSegments(){for(const e of this.segments.values())e.canvas&&e.canvas.remove();this.segments.clear()}getFilterBank(e){const t=this.fftSamples/2;return function(e,t,s,i){switch(e){case"mel":return p(t,s,i,o,l);case"logarithmic":return p(t,s,i,a,h);case"bark":return p(t,s,i,c,d);case"erb":return p(t,s,i,u,f);default:return null}}(this.scale,t,this.fftSamples,e)}freqType(e){return e>=1e3?(e/1e3).toFixed(1):Math.round(e)}unitType(e){return e>=1e3?"kHz":"Hz"}getLabelFrequency(e,t){const s=m(this.frequencyMin,this.scale);return function(e,t){switch(t){case"mel":return l(e);case"logarithmic":return h(e);case"bark":return d(e);case"erb":return f(e);default:return e}}(s+e/t*(m(this.frequencyMax,this.scale)-s),this.scale)}loadLabels(e,t,s,i,r,n,o,l,a){e=e||"rgba(68,68,68,0)",t=t||"12px",s=s||"12px",i=i||"Helvetica",r=r||"#fff",n=n||"#fff",o=o||"center";const h=this.height||512,c=h/256*5;this.frequencyMin;const d=this.labelsEl.getContext("2d"),u=window.devicePixelRatio;if(this.labelsEl.height=this.height*a*u,this.labelsEl.width=55*u,d.scale(u,u),d)for(let l=0;l<a;l++){let a;for(d.fillStyle=e,d.fillRect(0,l*h,55,(1+l)*h),d.fill(),a=0;a<=c;a++){d.textAlign=o,d.textBaseline="middle";const e=this.getLabelFrequency(a,c),u=this.freqType(e),f=this.unitType(e),m=16;let p=(1+l)*h-a/c*h;p=Math.min(Math.max(p,l*h+10),(1+l)*h-10),d.fillStyle=n,d.font=s+" "+i,d.fillText(f,m+24,p),d.fillStyle=r,d.font=t+" "+i,d.fillText(u.toString(),m,p)}}}render(t){return e(this,void 0,void 0,(function*(){this.buffer=t,this.pixelsPerSecond=this.getPixelsPerSecond(),this.frequencyMax=this.frequencyMax||t.sampleRate/2;const e=this.options.splitChannels?t.numberOfChannels:1;this.wrapper.style.height=this.height*e+"px",this.clearAllSegments(),this.nextProgressiveSegmentTime=0,this.options.labels&&this.loadLabels(this.options.labelsBackground,"12px","12px","",this.options.labelsColor,this.options.labelsHzColor||this.options.labelsColor,"center","#specLabels",e),this.scheduleRender(),this.emit("ready")}))}destroy(){this.unAll(),this.renderTimeout&&(clearTimeout(this.renderTimeout),this.renderTimeout=null),this.qualityUpdateTimeout&&(clearTimeout(this.qualityUpdateTimeout),this.qualityUpdateTimeout=null),this.stopProgressiveLoading(),this.nextProgressiveSegmentTime=0,this.worker&&(this.worker.terminate(),this.worker=null);for(const[e,t]of this.workerPromises)t.reject(new Error("Plugin destroyed"));this.workerPromises.clear(),this.clearAllSegments(),this.canvasContainer&&(this.canvasContainer.remove(),this.canvasContainer=null),this.wrapper&&(this.wrapper.remove(),this.wrapper=null),this.labelsEl&&(this.labelsEl.remove(),this.labelsEl=null),this.container=null,this.buffer=null,this.fft=null,this.isRendering=!1,this.currentPosition=0,this.pixelsPerSecond=0,super.destroy()}getWidth(){var e,t;return(null===(t=null===(e=this.wavesurfer)||void 0===e?void 0:e.getWrapper())||void 0===t?void 0:t.offsetWidth)||0}getPixelsPerSecond(){var e;const t=null===(e=this.wavesurfer)||void 0===e?void 0:e.options.minPxPerSec;if(t&&t>0)return t;if(this.buffer){const e=this.getWidth(),t=e>0?e/this.buffer.duration:100;return Math.max(t,50)}return 50}stopProgressiveLoading(){this.isProgressiveLoading=!1,this.progressiveLoadTimeout&&(clearTimeout(this.progressiveLoadTimeout),this.progressiveLoadTimeout=null)}restartProgressiveLoading(){this.stopProgressiveLoading(),this.nextProgressiveSegmentTime=0,this.progressiveLoading&&this.startProgressiveLoading()}}export{W as default};
|