wavesurfer.js 7.0.0-alpha.8 → 7.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +90 -18
  2. package/dist/base-plugin.d.ts +6 -4
  3. package/dist/base-plugin.js +9 -3
  4. package/dist/decoder.d.ts +8 -7
  5. package/dist/decoder.js +43 -38
  6. package/dist/event-emitter.d.ts +16 -10
  7. package/dist/event-emitter.js +38 -16
  8. package/dist/fetcher.d.ts +4 -3
  9. package/dist/fetcher.js +5 -15
  10. package/dist/player.d.ts +31 -11
  11. package/dist/player.js +58 -31
  12. package/dist/plugins/envelope.d.ts +71 -0
  13. package/dist/plugins/envelope.js +347 -0
  14. package/dist/plugins/envelope.min.js +1 -0
  15. package/dist/plugins/minimap.d.ts +39 -0
  16. package/dist/plugins/minimap.js +117 -0
  17. package/dist/plugins/minimap.min.js +1 -0
  18. package/dist/plugins/multitrack.d.ts +117 -0
  19. package/dist/plugins/multitrack.js +507 -0
  20. package/dist/plugins/multitrack.min.js +1 -0
  21. package/dist/plugins/record.d.ts +26 -0
  22. package/dist/plugins/record.js +126 -0
  23. package/dist/plugins/record.min.js +1 -0
  24. package/dist/plugins/regions.d.ts +83 -43
  25. package/dist/plugins/regions.js +362 -190
  26. package/dist/plugins/regions.min.js +1 -0
  27. package/dist/plugins/spectrogram.d.ts +69 -0
  28. package/dist/plugins/spectrogram.js +340 -0
  29. package/dist/plugins/spectrogram.min.js +1 -0
  30. package/dist/plugins/timeline.d.ts +26 -9
  31. package/dist/plugins/timeline.js +65 -22
  32. package/dist/plugins/timeline.min.js +1 -0
  33. package/dist/renderer.d.ts +29 -13
  34. package/dist/renderer.js +280 -161
  35. package/dist/timer.d.ts +1 -1
  36. package/dist/wavesurfer.d.ts +151 -0
  37. package/dist/wavesurfer.js +241 -0
  38. package/dist/wavesurfer.min.js +1 -1
  39. package/package.json +54 -27
  40. package/dist/index.d.ts +0 -117
  41. package/dist/index.js +0 -227
  42. package/dist/player-webaudio.d.ts +0 -8
  43. package/dist/player-webaudio.js +0 -32
  44. package/dist/react/useWavesurfer.d.ts +0 -5
  45. package/dist/react/useWavesurfer.js +0 -20
  46. package/dist/wavesurfer.Regions.min.js +0 -1
  47. package/dist/wavesurfer.Timeline.min.js +0 -1
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Spectrogram plugin
3
+ *
4
+ * Render a spectrogram visualisation of the audio.
5
+ *
6
+ * @author Pavel Denisov (https://github.com/akreal)
7
+ * @see https://github.com/wavesurfer-js/wavesurfer.js/pull/337
8
+ *
9
+ * @example
10
+ * // ... initialising wavesurfer with the plugin
11
+ * var wavesurfer = WaveSurfer.create({
12
+ * // wavesurfer options ...
13
+ * plugins: [
14
+ * SpectrogramPlugin.create({
15
+ * // plugin options ...
16
+ * })
17
+ * ]
18
+ * });
19
+ */
20
+ import BasePlugin from '../base-plugin.js';
21
+ export type SpectrogramPluginOptions = {
22
+ /** Selector of element or element in which to render */
23
+ container: string | HTMLElement;
24
+ /** Number of samples to fetch to FFT. Must be a power of 2. */
25
+ fftSamples?: number;
26
+ /** Height of the spectrogram view in CSS pixels */
27
+ height?: number;
28
+ /** Set to true to display frequency labels. */
29
+ labels?: boolean;
30
+ /** Size of the overlapping window. Must be < fftSamples. Auto deduced from canvas size by default. */
31
+ noverlap?: number;
32
+ /** The window function to be used. */
33
+ windowFunc?: 'bartlett' | 'bartlettHann' | 'blackman' | 'cosine' | 'gauss' | 'hamming' | 'hann' | 'lanczoz' | 'rectangular' | 'triangular';
34
+ /** Some window functions have this extra value. (Between 0 and 1) */
35
+ alpha?: number;
36
+ /** Min frequency to scale spectrogram. */
37
+ frequencyMin?: number;
38
+ /** Max frequency to scale spectrogram. Set this to samplerate/2 to draw whole range of spectrogram. */
39
+ frequencyMax?: number;
40
+ /**
41
+ * A 256 long array of 4-element arrays. Each entry should contain a float between 0 and 1 and specify r, g, b, and alpha.
42
+ * Each entry should contain a float between 0 and 1 and specify r, g, b, and alpha.
43
+ */
44
+ colorMap?: number[][];
45
+ };
46
+ export type SpectrogramPluginEvents = {
47
+ ready: [];
48
+ click: [relativeX: number];
49
+ };
50
+ export default class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramPluginOptions> {
51
+ static create(options?: SpectrogramPluginOptions): SpectrogramPlugin;
52
+ utils: {
53
+ style: (el: HTMLElement, styles: Record<string, string>) => CSSStyleDeclaration & Record<string, string>;
54
+ };
55
+ constructor(options: SpectrogramPluginOptions);
56
+ onInit(): void;
57
+ destroy(): void;
58
+ createWrapper(): void;
59
+ _wrapperClickHandler(event: any): void;
60
+ createCanvas(): void;
61
+ render(): void;
62
+ drawSpectrogram: (frequenciesData: any) => void;
63
+ getFrequencies(callback: any): void;
64
+ loadFrequenciesData(url: any): Promise<void>;
65
+ freqType(freq: any): string | number;
66
+ unitType(freq: any): "KHz" | "Hz";
67
+ loadLabels(bgFill: any, fontSizeFreq: any, fontSizeUnit: any, fontType: any, textColorFreq: any, textColorUnit: any, textAlign: any, container: any): void;
68
+ resample(oldMatrix: any): Uint8Array[];
69
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Spectrogram plugin
3
+ *
4
+ * Render a spectrogram visualisation of the audio.
5
+ *
6
+ * @author Pavel Denisov (https://github.com/akreal)
7
+ * @see https://github.com/wavesurfer-js/wavesurfer.js/pull/337
8
+ *
9
+ * @example
10
+ * // ... initialising wavesurfer with the plugin
11
+ * var wavesurfer = WaveSurfer.create({
12
+ * // wavesurfer options ...
13
+ * plugins: [
14
+ * SpectrogramPlugin.create({
15
+ * // plugin options ...
16
+ * })
17
+ * ]
18
+ * });
19
+ */
20
+ // @ts-nocheck
21
+ import BasePlugin from '../base-plugin.js';
22
+ import FFT from './spectrogram-fft.mjs';
23
+ export default class SpectrogramPlugin extends BasePlugin {
24
+ static create(options) {
25
+ return new SpectrogramPlugin(options || {});
26
+ }
27
+ constructor(options) {
28
+ super(options);
29
+ this.utils = {
30
+ style: (el, styles) => Object.assign(el.style, styles),
31
+ };
32
+ this.drawSpectrogram = (frequenciesData) => {
33
+ if (!isNaN(frequenciesData[0][0])) {
34
+ // data is 1ch [sample, freq] format
35
+ // to [channel, sample, freq] format
36
+ frequenciesData = [frequenciesData];
37
+ }
38
+ const spectrCc = this.spectrCc;
39
+ const height = this.height;
40
+ const width = this.width;
41
+ const freqFrom = this.buffer.sampleRate / 2;
42
+ const freqMin = this.frequencyMin;
43
+ const freqMax = this.frequencyMax;
44
+ if (!spectrCc) {
45
+ return;
46
+ }
47
+ for (let c = 0; c < frequenciesData.length; c++) {
48
+ // for each channel
49
+ const pixels = this.resample(frequenciesData[c]);
50
+ const imageData = new ImageData(width, height);
51
+ for (let i = 0; i < pixels.length; i++) {
52
+ for (let j = 0; j < pixels[i].length; j++) {
53
+ const colorMap = this.colorMap[pixels[i][j]];
54
+ const redIndex = ((height - j) * width + i) * 4;
55
+ imageData.data[redIndex] = colorMap[0] * 255;
56
+ imageData.data[redIndex + 1] = colorMap[1] * 255;
57
+ imageData.data[redIndex + 2] = colorMap[2] * 255;
58
+ imageData.data[redIndex + 3] = colorMap[3] * 255;
59
+ }
60
+ }
61
+ // scale and stack spectrograms
62
+ createImageBitmap(imageData).then((renderer) => {
63
+ spectrCc.drawImage(renderer, 0, height * (1 - freqMax / freqFrom), // source x, y
64
+ width, (height * (freqMax - freqMin)) / freqFrom, // source width, height
65
+ 0, height * c, // destination x, y
66
+ width, height);
67
+ });
68
+ }
69
+ this.emit('ready');
70
+ };
71
+ this.frequenciesDataUrl = options.frequenciesDataUrl;
72
+ this.container =
73
+ 'string' == typeof options.container ? document.querySelector(options.container) : options.container;
74
+ if (options.colorMap) {
75
+ if (options.colorMap.length < 256) {
76
+ throw new Error('Colormap must contain 256 elements');
77
+ }
78
+ for (let i = 0; i < options.colorMap.length; i++) {
79
+ const cmEntry = options.colorMap[i];
80
+ if (cmEntry.length !== 4) {
81
+ throw new Error('ColorMap entries must contain 4 values');
82
+ }
83
+ }
84
+ this.colorMap = options.colorMap;
85
+ }
86
+ else {
87
+ this.colorMap = [];
88
+ for (let i = 0; i < 256; i++) {
89
+ const val = (255 - i) / 256;
90
+ this.colorMap.push([val, val, val, 1]);
91
+ }
92
+ }
93
+ this.fftSamples = options.fftSamples || 512;
94
+ this.height = options.height || this.fftSamples / 2;
95
+ this.noverlap = options.noverlap;
96
+ this.windowFunc = options.windowFunc;
97
+ this.alpha = options.alpha;
98
+ this.channels = 1;
99
+ // Getting file's original samplerate is difficult(#1248).
100
+ // So set 12kHz default to render like wavesurfer.js 5.x.
101
+ this.frequencyMin = options.frequencyMin || 0;
102
+ this.frequencyMax = options.frequencyMax || 0;
103
+ this.createWrapper();
104
+ this.createCanvas();
105
+ }
106
+ onInit() {
107
+ this.container = this.container || this.wavesurfer.getWrapper();
108
+ this.container.appendChild(this.wrapper);
109
+ if (this.wavesurfer.options.fillParent) {
110
+ this.utils.style(this.wrapper, {
111
+ width: '100%',
112
+ overflowX: 'hidden',
113
+ overflowY: 'hidden',
114
+ });
115
+ }
116
+ this.width = this.wavesurfer.getWrapper().offsetWidth;
117
+ this.subscriptions.push(this.wavesurfer.on('redraw', () => this.render()));
118
+ }
119
+ destroy() {
120
+ this.unAll();
121
+ this.wavesurfer.un('ready', this._onReady);
122
+ this.wavesurfer.un('redraw', this._onRender);
123
+ this.wavesurfer = null;
124
+ this.util = null;
125
+ this.options = null;
126
+ if (this.wrapper) {
127
+ this.wrapper.remove();
128
+ this.wrapper = null;
129
+ }
130
+ }
131
+ createWrapper() {
132
+ this.wrapper = document.createElement('div');
133
+ this.utils.style(this.wrapper, {
134
+ display: 'block',
135
+ position: 'relative',
136
+ userSelect: 'none',
137
+ webkitUserSelect: 'none',
138
+ height: `${this.height * this.channels}px`,
139
+ });
140
+ // if labels are active
141
+ if (this.options.labels) {
142
+ const labelsEl = document.createElement('canvas');
143
+ labelsEl.classList.add('spec-labels');
144
+ this.utils.style(labelsEl, {
145
+ position: 'absolute',
146
+ zIndex: 9,
147
+ width: '55px',
148
+ height: '100%',
149
+ });
150
+ this.wrapper.appendChild(labelsEl);
151
+ this.labelsEl = labelsEl;
152
+ }
153
+ this.wrapper.addEventListener('click', this._onWrapperClick);
154
+ }
155
+ _wrapperClickHandler(event) {
156
+ event.preventDefault();
157
+ const relX = 'offsetX' in event ? event.offsetX : event.layerX;
158
+ this.emit('click', relX / this.width || 0);
159
+ }
160
+ createCanvas() {
161
+ const canvas = document.createElement('canvas');
162
+ this.wrapper.appendChild(canvas);
163
+ this.spectrCc = canvas.getContext('2d');
164
+ this.utils.style(canvas, {
165
+ position: 'absolute',
166
+ left: 0,
167
+ top: 0,
168
+ width: '100%',
169
+ height: '100%',
170
+ zIndex: 4,
171
+ });
172
+ this.canvas = canvas;
173
+ }
174
+ render() {
175
+ this.canvas.width = this.width;
176
+ this.canvas.height = this.height;
177
+ if (this.frequenciesDataUrl) {
178
+ this.loadFrequenciesData(this.frequenciesDataUrl);
179
+ }
180
+ else {
181
+ this.getFrequencies(this.drawSpectrogram);
182
+ }
183
+ this.loadLabels('rgba(68,68,68,0.5)', '12px', '10px', '', '#fff', '#f7f7f7', 'center', '#specLabels');
184
+ }
185
+ getFrequencies(callback) {
186
+ const fftSamples = this.fftSamples;
187
+ const buffer = this.wavesurfer.getDecodedData();
188
+ const channels = this.channels;
189
+ this.frequencyMax = this.frequencyMax || buffer.sampleRate / 2;
190
+ if (!buffer)
191
+ return;
192
+ this.buffer = buffer;
193
+ // This may differ from file samplerate. Browser resamples audio.
194
+ const sampleRate = buffer.sampleRate;
195
+ const frequencies = [];
196
+ let noverlap = this.noverlap;
197
+ if (!noverlap) {
198
+ const uniqueSamplesPerPx = buffer.length / this.canvas.width;
199
+ noverlap = Math.max(0, Math.round(fftSamples - uniqueSamplesPerPx));
200
+ }
201
+ const fft = new FFT(fftSamples, sampleRate, this.windowFunc, this.alpha);
202
+ for (let c = 0; c < channels; c++) {
203
+ // for each channel
204
+ const channelData = buffer.getChannelData(c);
205
+ const channelFreq = [];
206
+ let currentOffset = 0;
207
+ while (currentOffset + fftSamples < channelData.length) {
208
+ const segment = channelData.slice(currentOffset, currentOffset + fftSamples);
209
+ const spectrum = fft.calculateSpectrum(segment);
210
+ const array = new Uint8Array(fftSamples / 2);
211
+ let j;
212
+ for (j = 0; j < fftSamples / 2; j++) {
213
+ array[j] = Math.max(-255, Math.log10(spectrum[j]) * 45);
214
+ }
215
+ channelFreq.push(array);
216
+ // channelFreq: [sample, freq]
217
+ currentOffset += fftSamples - noverlap;
218
+ }
219
+ frequencies.push(channelFreq);
220
+ // frequencies: [channel, sample, freq]
221
+ }
222
+ callback(frequencies, this);
223
+ }
224
+ loadFrequenciesData(url) {
225
+ return fetch(url)
226
+ .then((response) => response.json())
227
+ .then(this.drawSpectrogram);
228
+ }
229
+ freqType(freq) {
230
+ return freq >= 1000 ? (freq / 1000).toFixed(1) : Math.round(freq);
231
+ }
232
+ unitType(freq) {
233
+ return freq >= 1000 ? 'KHz' : 'Hz';
234
+ }
235
+ loadLabels(bgFill, fontSizeFreq, fontSizeUnit, fontType, textColorFreq, textColorUnit, textAlign, container) {
236
+ const frequenciesHeight = this.height;
237
+ bgFill = bgFill || 'rgba(68,68,68,0)';
238
+ fontSizeFreq = fontSizeFreq || '12px';
239
+ fontSizeUnit = fontSizeUnit || '10px';
240
+ fontType = fontType || 'Helvetica';
241
+ textColorFreq = textColorFreq || '#fff';
242
+ textColorUnit = textColorUnit || '#fff';
243
+ textAlign = textAlign || 'center';
244
+ container = container || '#specLabels';
245
+ const bgWidth = 55;
246
+ const getMaxY = frequenciesHeight || 512;
247
+ const labelIndex = 5 * (getMaxY / 256);
248
+ const freqStart = this.frequencyMin;
249
+ const step = (this.frequencyMax - freqStart) / labelIndex;
250
+ // prepare canvas element for labels
251
+ const ctx = this.labelsEl.getContext('2d');
252
+ const dispScale = window.devicePixelRatio;
253
+ this.labelsEl.height = this.height * this.channels * dispScale;
254
+ this.labelsEl.width = bgWidth * dispScale;
255
+ ctx.scale(dispScale, dispScale);
256
+ if (!ctx) {
257
+ return;
258
+ }
259
+ for (let c = 0; c < this.channels; c++) {
260
+ // for each channel
261
+ // fill background
262
+ ctx.fillStyle = bgFill;
263
+ ctx.fillRect(0, c * getMaxY, bgWidth, (1 + c) * getMaxY);
264
+ ctx.fill();
265
+ let i;
266
+ // render labels
267
+ for (i = 0; i <= labelIndex; i++) {
268
+ ctx.textAlign = textAlign;
269
+ ctx.textBaseline = 'middle';
270
+ const freq = freqStart + step * i;
271
+ const label = this.freqType(freq);
272
+ const units = this.unitType(freq);
273
+ const yLabelOffset = 2;
274
+ const x = 16;
275
+ let y;
276
+ if (i == 0) {
277
+ y = (1 + c) * getMaxY + i - 10;
278
+ // unit label
279
+ ctx.fillStyle = textColorUnit;
280
+ ctx.font = fontSizeUnit + ' ' + fontType;
281
+ ctx.fillText(units, x + 24, y);
282
+ // freq label
283
+ ctx.fillStyle = textColorFreq;
284
+ ctx.font = fontSizeFreq + ' ' + fontType;
285
+ ctx.fillText(label, x, y);
286
+ }
287
+ else {
288
+ y = (1 + c) * getMaxY - i * 50 + yLabelOffset;
289
+ // unit label
290
+ ctx.fillStyle = textColorUnit;
291
+ ctx.font = fontSizeUnit + ' ' + fontType;
292
+ ctx.fillText(units, x + 24, y);
293
+ // freq label
294
+ ctx.fillStyle = textColorFreq;
295
+ ctx.font = fontSizeFreq + ' ' + fontType;
296
+ ctx.fillText(label, x, y);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ resample(oldMatrix) {
302
+ const columnsNumber = this.width;
303
+ const newMatrix = [];
304
+ const oldPiece = 1 / oldMatrix.length;
305
+ const newPiece = 1 / columnsNumber;
306
+ let i;
307
+ for (i = 0; i < columnsNumber; i++) {
308
+ const column = new Array(oldMatrix[0].length);
309
+ let j;
310
+ for (j = 0; j < oldMatrix.length; j++) {
311
+ const oldStart = j * oldPiece;
312
+ const oldEnd = oldStart + oldPiece;
313
+ const newStart = i * newPiece;
314
+ const newEnd = newStart + newPiece;
315
+ const overlap = oldEnd <= newStart || newEnd <= oldStart
316
+ ? 0
317
+ : Math.min(Math.max(oldEnd, newStart), Math.max(newEnd, oldStart)) -
318
+ Math.max(Math.min(oldEnd, newStart), Math.min(newEnd, oldStart));
319
+ let k;
320
+ /* eslint-disable max-depth */
321
+ if (overlap > 0) {
322
+ for (k = 0; k < oldMatrix[0].length; k++) {
323
+ if (column[k] == null) {
324
+ column[k] = 0;
325
+ }
326
+ column[k] += (overlap / newPiece) * oldMatrix[j][k];
327
+ }
328
+ }
329
+ /* eslint-enable max-depth */
330
+ }
331
+ const intColumn = new Uint8Array(oldMatrix[0].length);
332
+ let m;
333
+ for (m = 0; m < oldMatrix[0].length; m++) {
334
+ intColumn[m] = column[m];
335
+ }
336
+ newMatrix.push(intColumn);
337
+ }
338
+ return newMatrix;
339
+ }
340
+ }
@@ -0,0 +1 @@
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Spectrogram=e():t.Spectrogram=e()}(WaveSurfer,(()=>(()=>{"use strict";var t={d:(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>r});const s=class{constructor(){this.listeners={}}on(t,e){return this.listeners[t]||(this.listeners[t]=new Set),this.listeners[t].add(e),()=>this.un(t,e)}once(t,e){const s=this.on(t,e),i=this.on(t,(()=>{s(),i()}));return s}un(t,e){this.listeners[t]&&(e?this.listeners[t].delete(e):delete this.listeners[t])}unAll(){this.listeners={}}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)))}},i=class extends s{constructor(t){super(),this.subscriptions=[],this.options=t}onInit(){}init(t){this.wavesurfer=t,this.onInit()}destroy(){this.subscriptions.forEach((t=>t()))}};function a(t,e,s,i){switch(this.bufferSize=t,this.sampleRate=e,this.bandwidth=2/t*(e/2),this.sinTable=new Float32Array(t),this.cosTable=new Float32Array(t),this.windowValues=new Float32Array(t),this.reverseTable=new Uint32Array(t),this.peakBand=0,this.peak=0,s){case"bartlett":for(a=0;a<t;a++)this.windowValues[a]=2/(t-1)*((t-1)/2-Math.abs(a-(t-1)/2));break;case"bartlettHann":for(a=0;a<t;a++)this.windowValues[a]=.62-.48*Math.abs(a/(t-1)-.5)-.38*Math.cos(2*Math.PI*a/(t-1));break;case"blackman":for(i=i||.16,a=0;a<t;a++)this.windowValues[a]=(1-i)/2-.5*Math.cos(2*Math.PI*a/(t-1))+i/2*Math.cos(4*Math.PI*a/(t-1));break;case"cosine":for(a=0;a<t;a++)this.windowValues[a]=Math.cos(Math.PI*a/(t-1)-Math.PI/2);break;case"gauss":for(i=i||.25,a=0;a<t;a++)this.windowValues[a]=Math.pow(Math.E,-.5*Math.pow((a-(t-1)/2)/(i*(t-1)/2),2));break;case"hamming":for(a=0;a<t;a++)this.windowValues[a]=.54-.46*Math.cos(2*Math.PI*a/(t-1));break;case"hann":case void 0:for(a=0;a<t;a++)this.windowValues[a]=.5*(1-Math.cos(2*Math.PI*a/(t-1)));break;case"lanczoz":for(a=0;a<t;a++)this.windowValues[a]=Math.sin(Math.PI*(2*a/(t-1)-1))/(Math.PI*(2*a/(t-1)-1));break;case"rectangular":for(a=0;a<t;a++)this.windowValues[a]=1;break;case"triangular":for(a=0;a<t;a++)this.windowValues[a]=2/t*(t/2-Math.abs(a-(t-1)/2));break;default:throw Error("No such window function '"+s+"'")}for(var a,r=1,n=t>>1;r<t;){for(a=0;a<r;a++)this.reverseTable[a+r]=this.reverseTable[a]+n;r<<=1,n>>=1}for(a=0;a<t;a++)this.sinTable[a]=Math.sin(-Math.PI/a),this.cosTable[a]=Math.cos(-Math.PI/a);this.calculateSpectrum=function(t){var e,s,i,a=this.bufferSize,r=this.cosTable,n=this.sinTable,h=this.reverseTable,l=new Float32Array(a),o=new Float32Array(a),c=2/this.bufferSize,f=Math.sqrt,p=new Float32Array(a/2),u=Math.floor(Math.log(a)/Math.LN2);if(Math.pow(2,u)!==a)throw"Invalid buffer size, must be a power of 2.";if(a!==t.length)throw"Supplied buffer is not the same size as defined FFT. FFT Size: "+a+" Buffer Size: "+t.length;for(var d,w,M,b,g,m,y,v,x=1,S=0;S<a;S++)l[S]=t[h[S]]*this.windowValues[h[S]],o[S]=0;for(;x<a;){d=r[x],w=n[x],M=1,b=0;for(var k=0;k<x;k++){for(S=k;S<a;)m=M*l[g=S+x]-b*o[g],y=M*o[g]+b*l[g],l[g]=l[S]-m,o[g]=o[S]-y,l[S]+=m,o[S]+=y,S+=x<<1;M=(v=M)*d-b*w,b=v*w+b*d}x<<=1}S=0;for(var q=a/2;S<q;S++)(i=c*f((e=l[S])*e+(s=o[S])*s))>this.peak&&(this.peakBand=S,this.peak=i),p[S]=i;return p}}class r extends i{static create(t){return new r(t||{})}constructor(t){if(super(t),this.utils={style:(t,e)=>Object.assign(t.style,e)},this.drawSpectrogram=t=>{isNaN(t[0][0])||(t=[t]);const e=this.spectrCc,s=this.height,i=this.width,a=this.buffer.sampleRate/2,r=this.frequencyMin,n=this.frequencyMax;if(e){for(let h=0;h<t.length;h++){const l=this.resample(t[h]),o=new ImageData(i,s);for(let t=0;t<l.length;t++)for(let e=0;e<l[t].length;e++){const a=this.colorMap[l[t][e]],r=4*((s-e)*i+t);o.data[r]=255*a[0],o.data[r+1]=255*a[1],o.data[r+2]=255*a[2],o.data[r+3]=255*a[3]}createImageBitmap(o).then((t=>{e.drawImage(t,0,s*(1-n/a),i,s*(n-r)/a,0,s*h,i,s)}))}this.emit("ready")}},this.frequenciesDataUrl=t.frequenciesDataUrl,this.container="string"==typeof t.container?document.querySelector(t.container):t.container,t.colorMap){if(t.colorMap.length<256)throw new Error("Colormap must contain 256 elements");for(let e=0;e<t.colorMap.length;e++)if(4!==t.colorMap[e].length)throw new Error("ColorMap entries must contain 4 values");this.colorMap=t.colorMap}else{this.colorMap=[];for(let t=0;t<256;t++){const e=(255-t)/256;this.colorMap.push([e,e,e,1])}}this.fftSamples=t.fftSamples||512,this.height=t.height||this.fftSamples/2,this.noverlap=t.noverlap,this.windowFunc=t.windowFunc,this.alpha=t.alpha,this.channels=1,this.frequencyMin=t.frequencyMin||0,this.frequencyMax=t.frequencyMax||0,this.createWrapper(),this.createCanvas()}onInit(){this.container=this.container||this.wavesurfer.getWrapper(),this.container.appendChild(this.wrapper),this.wavesurfer.options.fillParent&&this.utils.style(this.wrapper,{width:"100%",overflowX:"hidden",overflowY:"hidden"}),this.width=this.wavesurfer.getWrapper().offsetWidth,this.subscriptions.push(this.wavesurfer.on("redraw",(()=>this.render())))}destroy(){this.unAll(),this.wavesurfer.un("ready",this._onReady),this.wavesurfer.un("redraw",this._onRender),this.wavesurfer=null,this.util=null,this.options=null,this.wrapper&&(this.wrapper.remove(),this.wrapper=null)}createWrapper(){if(this.wrapper=document.createElement("div"),this.utils.style(this.wrapper,{display:"block",position:"relative",userSelect:"none",webkitUserSelect:"none",height:this.height*this.channels+"px"}),this.options.labels){const t=document.createElement("canvas");t.classList.add("spec-labels"),this.utils.style(t,{position:"absolute",zIndex:9,width:"55px",height:"100%"}),this.wrapper.appendChild(t),this.labelsEl=t}this.wrapper.addEventListener("click",this._onWrapperClick)}_wrapperClickHandler(t){t.preventDefault();const e="offsetX"in t?t.offsetX:t.layerX;this.emit("click",e/this.width||0)}createCanvas(){const t=document.createElement("canvas");this.wrapper.appendChild(t),this.spectrCc=t.getContext("2d"),this.utils.style(t,{position:"absolute",left:0,top:0,width:"100%",height:"100%",zIndex:4}),this.canvas=t}render(){this.canvas.width=this.width,this.canvas.height=this.height,this.frequenciesDataUrl?this.loadFrequenciesData(this.frequenciesDataUrl):this.getFrequencies(this.drawSpectrogram),this.loadLabels("rgba(68,68,68,0.5)","12px","10px","","#fff","#f7f7f7","center","#specLabels")}getFrequencies(t){const e=this.fftSamples,s=this.wavesurfer.getDecodedData(),i=this.channels;if(this.frequencyMax=this.frequencyMax||s.sampleRate/2,!s)return;this.buffer=s;const r=s.sampleRate,n=[];let h=this.noverlap;if(!h){const t=s.length/this.canvas.width;h=Math.max(0,Math.round(e-t))}const l=new a(e,r,this.windowFunc,this.alpha);for(let t=0;t<i;t++){const i=s.getChannelData(t),a=[];let r=0;for(;r+e<i.length;){const t=i.slice(r,r+e),s=l.calculateSpectrum(t),n=new Uint8Array(e/2);let o;for(o=0;o<e/2;o++)n[o]=Math.max(-255,45*Math.log10(s[o]));a.push(n),r+=e-h}n.push(a)}t(n,this)}loadFrequenciesData(t){return fetch(t).then((t=>t.json())).then(this.drawSpectrogram)}freqType(t){return t>=1e3?(t/1e3).toFixed(1):Math.round(t)}unitType(t){return t>=1e3?"KHz":"Hz"}loadLabels(t,e,s,i,a,r,n,h){t=t||"rgba(68,68,68,0)",e=e||"12px",s=s||"10px",i=i||"Helvetica",a=a||"#fff",r=r||"#fff",n=n||"center",h=h||"#specLabels";const l=this.height||512,o=l/256*5,c=this.frequencyMin,f=(this.frequencyMax-c)/o,p=this.labelsEl.getContext("2d"),u=window.devicePixelRatio;if(this.labelsEl.height=this.height*this.channels*u,this.labelsEl.width=55*u,p.scale(u,u),p)for(let h=0;h<this.channels;h++){let u;for(p.fillStyle=t,p.fillRect(0,h*l,55,(1+h)*l),p.fill(),u=0;u<=o;u++){p.textAlign=n,p.textBaseline="middle";const t=c+f*u,o=this.freqType(t),d=this.unitType(t),w=2,M=16;let b;0==u?(b=(1+h)*l+u-10,p.fillStyle=r,p.font=s+" "+i,p.fillText(d,M+24,b),p.fillStyle=a,p.font=e+" "+i,p.fillText(o,M,b)):(b=(1+h)*l-50*u+w,p.fillStyle=r,p.font=s+" "+i,p.fillText(d,M+24,b),p.fillStyle=a,p.font=e+" "+i,p.fillText(o,M,b))}}}resample(t){const e=this.width,s=[],i=1/t.length,a=1/e;let r;for(r=0;r<e;r++){const e=new Array(t[0].length);let n;for(n=0;n<t.length;n++){const s=n*i,h=s+i,l=r*a,o=l+a,c=h<=l||o<=s?0:Math.min(Math.max(h,l),Math.max(o,s))-Math.max(Math.min(h,l),Math.min(o,s));let f;if(c>0)for(f=0;f<t[0].length;f++)null==e[f]&&(e[f]=0),e[f]+=c/a*t[n][f]}const h=new Uint8Array(t[0].length);let l;for(l=0;l<t[0].length;l++)h[l]=e[l];s.push(h)}return s}}return e.default})()));
@@ -1,28 +1,45 @@
1
+ /**
2
+ * The Timeline plugin adds timestamps and notches under the waveform.
3
+ */
1
4
  import BasePlugin from '../base-plugin.js';
2
- import { WaveSurferPluginParams } from '../index.js';
3
- type TimelinePluginOptions = {
5
+ export type TimelinePluginOptions = {
6
+ /** The height of the timeline in pixels, defaults to 20 */
4
7
  height?: number;
8
+ /** HTML container for the timeline, defaults to wavesufer's container */
9
+ container?: HTMLElement;
10
+ /** Pass 'beforebegin' to insert the timeline on top of the waveform */
11
+ insertPosition?: InsertPosition;
12
+ /** The duration of the timeline in seconds, defaults to wavesurfer's duration */
13
+ duration?: number;
14
+ /** Interval between ticks in seconds */
5
15
  timeInterval?: number;
16
+ /** Interval between numeric labels */
6
17
  primaryLabelInterval?: number;
18
+ /** Interval between secondary numeric labels */
7
19
  secondaryLabelInterval?: number;
20
+ /** Custom inline style to apply to the container */
21
+ style?: Partial<CSSStyleDeclaration> | string;
8
22
  };
9
23
  declare const defaultOptions: {
10
24
  height: number;
11
25
  };
12
- type TimelinePluginEvents = {
13
- ready: void;
26
+ export type TimelinePluginEvents = {
27
+ ready: [];
14
28
  };
15
29
  declare class TimelinePlugin extends BasePlugin<TimelinePluginEvents, TimelinePluginOptions> {
16
- private wrapper;
30
+ private timelineWrapper;
17
31
  protected options: TimelinePluginOptions & typeof defaultOptions;
18
- constructor(params: WaveSurferPluginParams, options: TimelinePluginOptions);
32
+ constructor(options?: TimelinePluginOptions);
33
+ static create(options?: TimelinePluginOptions): TimelinePlugin;
34
+ /** Called by wavesurfer, don't call manually */
35
+ onInit(): void;
19
36
  /** Unmount */
20
37
  destroy(): void;
21
- private initWrapper;
38
+ private initTimelineWrapper;
22
39
  private formatTime;
23
40
  private defaultTimeInterval;
24
- defaultPrimaryLabelInterval(pxPerSec: number): number;
25
- defaultSecondaryLabelInterval(pxPerSec: number): number;
41
+ private defaultPrimaryLabelInterval;
42
+ private defaultSecondaryLabelInterval;
26
43
  private initTimeline;
27
44
  }
28
45
  export default TimelinePlugin;
@@ -1,29 +1,55 @@
1
+ /**
2
+ * The Timeline plugin adds timestamps and notches under the waveform.
3
+ */
1
4
  import BasePlugin from '../base-plugin.js';
2
5
  const defaultOptions = {
3
6
  height: 20,
4
7
  };
5
8
  class TimelinePlugin extends BasePlugin {
6
- constructor(params, options) {
7
- super(params, options);
9
+ constructor(options) {
10
+ super(options || {});
8
11
  this.options = Object.assign({}, defaultOptions, options);
9
- this.wrapper = this.initWrapper();
10
- this.container.appendChild(this.wrapper);
11
- this.subscriptions.push(this.wavesurfer.on('decode', ({ duration }) => {
12
- this.initTimeline(duration);
13
- }));
12
+ this.timelineWrapper = this.initTimelineWrapper();
13
+ }
14
+ static create(options) {
15
+ return new TimelinePlugin(options);
16
+ }
17
+ /** Called by wavesurfer, don't call manually */
18
+ onInit() {
19
+ if (!this.wavesurfer) {
20
+ throw Error('WaveSurfer is not initialized');
21
+ }
22
+ const container = this.options.container ?? this.wavesurfer.getWrapper();
23
+ if (this.options.insertPosition) {
24
+ ;
25
+ (container.firstElementChild || container).insertAdjacentElement(this.options.insertPosition, this.timelineWrapper);
26
+ }
27
+ else {
28
+ container.appendChild(this.timelineWrapper);
29
+ }
30
+ if (this.options.duration) {
31
+ this.initTimeline(this.options.duration);
32
+ }
33
+ else {
34
+ this.subscriptions.push(this.wavesurfer.on('redraw', () => {
35
+ this.initTimeline(this.wavesurfer?.getDuration() || 0);
36
+ }));
37
+ }
14
38
  }
15
39
  /** Unmount */
16
40
  destroy() {
17
- this.wrapper.remove();
41
+ this.timelineWrapper.remove();
18
42
  super.destroy();
19
43
  }
20
- initWrapper() {
21
- return document.createElement('div');
44
+ initTimelineWrapper() {
45
+ const div = document.createElement('div');
46
+ div.setAttribute('part', 'timeline');
47
+ return div;
22
48
  }
23
49
  formatTime(seconds) {
24
50
  if (seconds / 60 > 1) {
25
51
  // calculate minutes and seconds from seconds count
26
- const minutes = Math.round(seconds / 60);
52
+ const minutes = Math.floor(seconds / 60);
27
53
  seconds = Math.round(seconds % 60);
28
54
  const paddedSeconds = `${seconds < 10 ? '0' : ''}${seconds}`;
29
55
  return `${minutes}:${paddedSeconds}`;
@@ -71,36 +97,52 @@ class TimelinePlugin extends BasePlugin {
71
97
  return 2;
72
98
  }
73
99
  initTimeline(duration) {
74
- var _a, _b, _c;
75
- const width = Math.round(this.wrapper.scrollWidth * devicePixelRatio);
76
- const pxPerSec = width / duration;
77
- const timeInterval = (_a = this.options.timeInterval) !== null && _a !== void 0 ? _a : this.defaultTimeInterval(pxPerSec);
78
- const primaryLabelInterval = (_b = this.options.primaryLabelInterval) !== null && _b !== void 0 ? _b : this.defaultPrimaryLabelInterval(pxPerSec);
79
- const secondaryLabelInterval = (_c = this.options.secondaryLabelInterval) !== null && _c !== void 0 ? _c : this.defaultSecondaryLabelInterval(pxPerSec);
100
+ const pxPerSec = this.timelineWrapper.scrollWidth / duration;
101
+ const timeInterval = this.options.timeInterval ?? this.defaultTimeInterval(pxPerSec);
102
+ const primaryLabelInterval = this.options.primaryLabelInterval ?? this.defaultPrimaryLabelInterval(pxPerSec);
103
+ const secondaryLabelInterval = this.options.secondaryLabelInterval ?? this.defaultSecondaryLabelInterval(pxPerSec);
104
+ const isTop = this.options.insertPosition === 'beforebegin';
80
105
  const timeline = document.createElement('div');
81
106
  timeline.setAttribute('style', `
82
107
  height: ${this.options.height}px;
83
108
  overflow: hidden;
84
109
  display: flex;
85
110
  justify-content: space-between;
86
- align-items: flex-end;
111
+ align-items: ${isTop ? 'flex-start' : 'flex-end'};
87
112
  font-size: ${this.options.height / 2}px;
113
+ white-space: nowrap;
88
114
  `);
115
+ if (isTop) {
116
+ const topStyle = `
117
+ position: absolute;
118
+ top: 0;
119
+ left: 0;
120
+ right: 0;
121
+ z-index: 2;
122
+ `;
123
+ timeline.setAttribute('style', timeline.getAttribute('style') + topStyle);
124
+ }
125
+ if (typeof this.options.style === 'string') {
126
+ timeline.setAttribute('style', timeline.getAttribute('style') + this.options.style);
127
+ }
128
+ else if (typeof this.options.style === 'object') {
129
+ Object.assign(timeline.style, this.options.style);
130
+ }
89
131
  const notchEl = document.createElement('div');
90
132
  notchEl.setAttribute('style', `
91
133
  width: 1px;
92
134
  height: 50%;
93
135
  display: flex;
94
136
  flex-direction: column;
95
- justify-content: flex-end;
137
+ justify-content: ${isTop ? 'flex-start' : 'flex-end'};
96
138
  overflow: visible;
97
139
  border-left: 1px solid currentColor;
98
140
  opacity: 0.25;
99
141
  `);
100
142
  for (let i = 0; i < duration; i += timeInterval) {
101
143
  const notch = notchEl.cloneNode();
102
- const isPrimary = i % primaryLabelInterval === 0;
103
- const isSecondary = i % secondaryLabelInterval === 0;
144
+ const isPrimary = (Math.round(i * 100) / 100) % primaryLabelInterval === 0;
145
+ const isSecondary = (Math.round(i * 100) / 100) % secondaryLabelInterval === 0;
104
146
  if (isPrimary || isSecondary) {
105
147
  notch.style.height = '100%';
106
148
  notch.style.textIndent = '3px';
@@ -110,7 +152,8 @@ class TimelinePlugin extends BasePlugin {
110
152
  }
111
153
  timeline.appendChild(notch);
112
154
  }
113
- this.wrapper.appendChild(timeline);
155
+ this.timelineWrapper.innerHTML = '';
156
+ this.timelineWrapper.appendChild(timeline);
114
157
  this.emit('ready');
115
158
  }
116
159
  }
@@ -0,0 +1 @@
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Timeline=e():t.Timeline=e()}(WaveSurfer,(()=>(()=>{"use strict";var t={284:(t,e,i)=>{i.d(e,{Z:()=>r});var n=i(139);class s extends n.Z{constructor(t){super(),this.subscriptions=[],this.options=t}onInit(){}init(t){this.wavesurfer=t,this.onInit()}destroy(){this.subscriptions.forEach((t=>t()))}}const r=s},139:(t,e,i)=>{i.d(e,{Z:()=>n});const n=class{constructor(){this.listeners={}}on(t,e){return this.listeners[t]||(this.listeners[t]=new Set),this.listeners[t].add(e),()=>this.un(t,e)}once(t,e){const i=this.on(t,e),n=this.on(t,(()=>{i(),n()}));return i}un(t,e){this.listeners[t]&&(e?this.listeners[t].delete(e):delete this.listeners[t])}unAll(){this.listeners={}}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)))}}}},e={};function i(n){var s=e[n];if(void 0!==s)return s.exports;var r=e[n]={exports:{}};return t[n](r,r.exports,i),r.exports}i.d=(t,e)=>{for(var n in e)i.o(e,n)&&!i.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var n={};return(()=>{i.d(n,{default:()=>r});var t=i(284);const e={height:20};class s extends t.Z{constructor(t){super(t||{}),this.options=Object.assign({},e,t),this.timelineWrapper=this.initTimelineWrapper()}static create(t){return new s(t)}onInit(){if(!this.wavesurfer)throw Error("WaveSurfer is not initialized");const t=this.options.container??this.wavesurfer.getWrapper();this.options.insertPosition?(t.firstElementChild||t).insertAdjacentElement(this.options.insertPosition,this.timelineWrapper):t.appendChild(this.timelineWrapper),this.options.duration?this.initTimeline(this.options.duration):this.subscriptions.push(this.wavesurfer.on("redraw",(()=>{this.initTimeline(this.wavesurfer?.getDuration()||0)})))}destroy(){this.timelineWrapper.remove(),super.destroy()}initTimelineWrapper(){const t=document.createElement("div");return t.setAttribute("part","timeline"),t}formatTime(t){return t/60>1?`${Math.floor(t/60)}:${(t=Math.round(t%60))<10?"0":""}${t}`:""+Math.round(1e3*t)/1e3}defaultTimeInterval(t){return t>=25?1:5*t>=25?5:15*t>=25?15:60*Math.ceil(.5/t)}defaultPrimaryLabelInterval(t){return t>=25?10:5*t>=25?6:4}defaultSecondaryLabelInterval(t){return t>=25?5:2}initTimeline(t){const e=this.timelineWrapper.scrollWidth/t,i=this.options.timeInterval??this.defaultTimeInterval(e),n=this.options.primaryLabelInterval??this.defaultPrimaryLabelInterval(e),s=this.options.secondaryLabelInterval??this.defaultSecondaryLabelInterval(e),r="beforebegin"===this.options.insertPosition,o=document.createElement("div");if(o.setAttribute("style",`\n height: ${this.options.height}px;\n overflow: hidden;\n display: flex;\n justify-content: space-between;\n align-items: ${r?"flex-start":"flex-end"};\n font-size: ${this.options.height/2}px;\n white-space: nowrap;\n `),r){const t="\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n z-index: 2;\n ";o.setAttribute("style",o.getAttribute("style")+t)}"string"==typeof this.options.style?o.setAttribute("style",o.getAttribute("style")+this.options.style):"object"==typeof this.options.style&&Object.assign(o.style,this.options.style);const l=document.createElement("div");l.setAttribute("style",`\n width: 1px;\n height: 50%;\n display: flex;\n flex-direction: column;\n justify-content: ${r?"flex-start":"flex-end"};\n overflow: visible;\n border-left: 1px solid currentColor;\n opacity: 0.25;\n `);for(let e=0;e<t;e+=i){const t=l.cloneNode(),i=Math.round(100*e)/100%n==0,r=Math.round(100*e)/100%s==0;(i||r)&&(t.style.height="100%",t.style.textIndent="3px",t.textContent=this.formatTime(e),i&&(t.style.opacity="1")),o.appendChild(t)}this.timelineWrapper.innerHTML="",this.timelineWrapper.appendChild(o),this.emit("ready")}}const r=s})(),n.default})()));