wavesurfer.js 5.1.0 → 6.0.2

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 (66) hide show
  1. package/CHANGES.md +47 -0
  2. package/README.md +10 -0
  3. package/dist/plugin/wavesurfer.cursor.js +53 -51
  4. package/dist/plugin/wavesurfer.cursor.js.map +1 -1
  5. package/dist/plugin/wavesurfer.cursor.min.js +2 -2
  6. package/dist/plugin/wavesurfer.cursor.min.js.map +1 -1
  7. package/dist/plugin/wavesurfer.elan.js +10 -7
  8. package/dist/plugin/wavesurfer.elan.js.map +1 -1
  9. package/dist/plugin/wavesurfer.elan.min.js +2 -2
  10. package/dist/plugin/wavesurfer.elan.min.js.map +1 -1
  11. package/dist/plugin/wavesurfer.markers.js +142 -26
  12. package/dist/plugin/wavesurfer.markers.js.map +1 -1
  13. package/dist/plugin/wavesurfer.markers.min.js +2 -2
  14. package/dist/plugin/wavesurfer.markers.min.js.map +1 -1
  15. package/dist/plugin/wavesurfer.mediasession.js +6 -6
  16. package/dist/plugin/wavesurfer.mediasession.js.map +1 -1
  17. package/dist/plugin/wavesurfer.mediasession.min.js +2 -2
  18. package/dist/plugin/wavesurfer.mediasession.min.js.map +1 -1
  19. package/dist/plugin/wavesurfer.microphone.js +5 -5
  20. package/dist/plugin/wavesurfer.microphone.js.map +1 -1
  21. package/dist/plugin/wavesurfer.microphone.min.js +2 -2
  22. package/dist/plugin/wavesurfer.microphone.min.js.map +1 -1
  23. package/dist/plugin/wavesurfer.minimap.js +26 -23
  24. package/dist/plugin/wavesurfer.minimap.js.map +1 -1
  25. package/dist/plugin/wavesurfer.minimap.min.js +2 -2
  26. package/dist/plugin/wavesurfer.minimap.min.js.map +1 -1
  27. package/dist/plugin/wavesurfer.playhead.js +5 -5
  28. package/dist/plugin/wavesurfer.playhead.js.map +1 -1
  29. package/dist/plugin/wavesurfer.playhead.min.js +2 -2
  30. package/dist/plugin/wavesurfer.playhead.min.js.map +1 -1
  31. package/dist/plugin/wavesurfer.regions.js +88 -21
  32. package/dist/plugin/wavesurfer.regions.js.map +1 -1
  33. package/dist/plugin/wavesurfer.regions.min.js +2 -2
  34. package/dist/plugin/wavesurfer.regions.min.js.map +1 -1
  35. package/dist/plugin/wavesurfer.spectrogram.js +71 -58
  36. package/dist/plugin/wavesurfer.spectrogram.js.map +1 -1
  37. package/dist/plugin/wavesurfer.spectrogram.min.js +2 -2
  38. package/dist/plugin/wavesurfer.spectrogram.min.js.map +1 -1
  39. package/dist/plugin/wavesurfer.timeline.js +52 -54
  40. package/dist/plugin/wavesurfer.timeline.js.map +1 -1
  41. package/dist/plugin/wavesurfer.timeline.min.js +2 -2
  42. package/dist/plugin/wavesurfer.timeline.min.js.map +1 -1
  43. package/dist/wavesurfer-html-init.js +6 -6
  44. package/dist/wavesurfer-html-init.js.map +1 -1
  45. package/dist/wavesurfer-html-init.min.js +2 -2
  46. package/dist/wavesurfer-html-init.min.js.map +1 -1
  47. package/dist/wavesurfer.js +302 -133
  48. package/dist/wavesurfer.js.map +1 -1
  49. package/dist/wavesurfer.min.js +2 -2
  50. package/dist/wavesurfer.min.js.map +1 -1
  51. package/package.json +23 -23
  52. package/src/drawer.canvasentry.js +33 -6
  53. package/src/drawer.js +1 -0
  54. package/src/drawer.multicanvas.js +19 -8
  55. package/src/mediaelement.js +1 -1
  56. package/src/plugin/cursor/index.js +68 -70
  57. package/src/plugin/markers/index.js +112 -23
  58. package/src/plugin/minimap/index.js +24 -21
  59. package/src/plugin/regions/index.js +31 -4
  60. package/src/plugin/regions/region.js +45 -8
  61. package/src/plugin/spectrogram/index.js +65 -50
  62. package/src/plugin/timeline/index.js +11 -9
  63. package/src/util/index.js +1 -0
  64. package/src/util/silence-mode.js +35 -0
  65. package/src/wavesurfer.js +54 -23
  66. package/src/webaudio.js +10 -1
@@ -96,16 +96,7 @@ export default class MinimapPlugin {
96
96
  this.drawer = new ws.Drawer(this.params.container, this.params);
97
97
  this.wavesurfer = ws;
98
98
  this.util = ws.util;
99
- /**
100
- * Minimap needs to listen for the `ready` and `waveform-ready` events
101
- * to work with the `MediaElement` backend. The moment the `ready` event
102
- * is called is different (and peaks would not load).
103
- *
104
- * @type {string}
105
- * @see https://github.com/katspaugh/wavesurfer.js/issues/736
106
- */
107
- this.renderEvent =
108
- ws.params.backend === 'MediaElement' ? 'waveform-ready' : 'ready';
99
+ this.renderEvent = 'redraw';
109
100
  this.overviewRegion = null;
110
101
  this.regionsPlugin = this.wavesurfer[this.params.regionsPluginName];
111
102
 
@@ -144,7 +135,8 @@ export default class MinimapPlugin {
144
135
  // event listeners for the overview region
145
136
  this._onScroll = e => {
146
137
  if (!this.draggingOverview) {
147
- this.moveOverviewRegion(e.target.scrollLeft / this.ratio);
138
+ const orientedTarget = this.util.withOrientation(e.target, this.wavesurfer.params.vertical);
139
+ this.moveOverviewRegion(orientedTarget.scrollLeft / this.ratio);
148
140
  }
149
141
  };
150
142
  this._onMouseover = e => {
@@ -245,8 +237,13 @@ export default class MinimapPlugin {
245
237
  createElements() {
246
238
  this.drawer.createElements();
247
239
  if (this.params.showOverview) {
248
- this.overviewRegion = this.util.style(
249
- document.createElement('overview'),
240
+ this.overviewRegion = this.util.withOrientation(
241
+ this.drawer.wrapper.appendChild(document.createElement('overview')),
242
+ this.wavesurfer.params.vertical
243
+ );
244
+
245
+ this.util.style(
246
+ this.overviewRegion,
250
247
  {
251
248
  top: 0,
252
249
  bottom: 0,
@@ -262,7 +259,6 @@ export default class MinimapPlugin {
262
259
  opacity: this.params.overviewOpacity
263
260
  }
264
261
  );
265
- this.drawer.wrapper.appendChild(this.overviewRegion);
266
262
  }
267
263
  }
268
264
 
@@ -307,24 +303,27 @@ export default class MinimapPlugin {
307
303
  }
308
304
 
309
305
  if (this.params.showOverview) {
310
- this.overviewRegion.addEventListener('mousedown', event => {
306
+ this.overviewRegion.domElement.addEventListener('mousedown', e => {
307
+ const event = this.util.withOrientation(e, this.wavesurfer.params.vertical);
311
308
  this.draggingOverview = true;
312
309
  relativePositionX = event.layerX;
313
310
  positionMouseDown.clientX = event.clientX;
314
311
  positionMouseDown.clientY = event.clientY;
315
312
  });
316
313
 
317
- this.drawer.wrapper.addEventListener('mousemove', event => {
314
+ this.drawer.wrapper.addEventListener('mousemove', e => {
318
315
  if (this.draggingOverview) {
316
+ const event = this.util.withOrientation(e, this.wavesurfer.params.vertical);
319
317
  this.moveOverviewRegion(
320
318
  event.clientX -
321
- this.drawer.container.getBoundingClientRect().left -
322
- relativePositionX
319
+ this.drawer.container.getBoundingClientRect().left -
320
+ relativePositionX
323
321
  );
324
322
  }
325
323
  });
326
324
 
327
- this.drawer.wrapper.addEventListener('mouseup', event => {
325
+ this.drawer.wrapper.addEventListener('mouseup', e => {
326
+ const event = this.util.withOrientation(e, this.wavesurfer.params.vertical);
328
327
  if (
329
328
  positionMouseDown.clientX - event.clientX === 0 &&
330
329
  positionMouseDown.clientX - event.clientX === 0
@@ -356,7 +355,9 @@ export default class MinimapPlugin {
356
355
  this.moveOverviewRegion(
357
356
  this.wavesurfer.drawer.wrapper.scrollLeft / this.ratio
358
357
  );
359
- this.overviewRegion.style.width = this.overviewWidth + 'px';
358
+ this.util.style(this.overviewRegion, {
359
+ width: this.overviewWidth + 'px'
360
+ });
360
361
  }
361
362
  }
362
363
 
@@ -372,7 +373,9 @@ export default class MinimapPlugin {
372
373
  this.overviewPosition =
373
374
  this.drawer.container.offsetWidth - this.overviewWidth;
374
375
  }
375
- this.overviewRegion.style.left = this.overviewPosition + 'px';
376
+ this.util.style(this.overviewRegion, {
377
+ left: this.overviewPosition + 'px'
378
+ });
376
379
  if (this.draggingOverview) {
377
380
  this.wavesurfer.drawer.wrapper.scrollLeft =
378
381
  this.overviewPosition * this.ratio;
@@ -10,7 +10,7 @@
10
10
  * @property {?number} snapToGridInterval Snap the regions to a grid of the specified multiples in seconds
11
11
  * @property {?number} snapToGridOffset Shift the snap-to-grid by the specified seconds. May also be negative.
12
12
  * @property {?boolean} deferInit Set to true to manually call
13
- * @property {number[]} maxRegions Maximum number of regions that may be created by the user at one time.
13
+ * @property {number} maxRegions Maximum number of regions that may be created by the user at one time.
14
14
  * `initPlugin('regions')`
15
15
  * @property {function} formatTimeCallback Allows custom formating for region tooltip.
16
16
  * @property {?number} edgeScrollWidth='5% from container edges' Optional width for edgeScroll to start
@@ -129,15 +129,14 @@ export default class RegionsPlugin {
129
129
  this.wavesurfer.Region = Region;
130
130
 
131
131
  // By default, scroll the container if the user drags a region
132
- // within 5% of its edge
132
+ // within 5% (based on its initial size) of its edge
133
133
  const scrollWidthProportion = 0.05;
134
134
  this._onBackendCreated = () => {
135
135
  this.wrapper = this.wavesurfer.drawer.wrapper;
136
136
  this.orientation = this.wavesurfer.drawer.orientation;
137
+ this.defaultEdgeScrollWidth = this.wrapper.clientWidth * scrollWidthProportion;
137
138
  if (this.params.regions) {
138
139
  this.params.regions.forEach(region => {
139
- region.edgeScrollWidth = this.params.edgeScrollWidth ||
140
- this.wrapper.clientWidth * scrollWidthProportion;
141
140
  this.add(region);
142
141
  });
143
142
  }
@@ -171,6 +170,11 @@ export default class RegionsPlugin {
171
170
  destroy() {
172
171
  this.wavesurfer.un('ready', this._onReady);
173
172
  this.wavesurfer.un('backend-created', this._onBackendCreated);
173
+ // Disabling `region-removed' because destroying the plugin calls
174
+ // the Region.remove() method that is also used to remove regions based
175
+ // on user input. This can cause confusion since teardown is not a
176
+ // user event, but would emit `region-removed` as if it was.
177
+ this.wavesurfer.setDisabledEventEmissions(['region-removed']);
174
178
  this.disableDragSelection();
175
179
  this.clear();
176
180
  }
@@ -197,6 +201,16 @@ export default class RegionsPlugin {
197
201
  return null;
198
202
  }
199
203
 
204
+ params = {
205
+ edgeScrollWidth: this.params.edgeScrollWidth || this.defaultEdgeScrollWidth,
206
+ ...params
207
+ };
208
+
209
+ // Take formatTimeCallback from plugin params if not already set
210
+ if (!params.formatTimeCallback && this.params.formatTimeCallback) {
211
+ params = {...params, formatTimeCallback: this.params.formatTimeCallback};
212
+ }
213
+
200
214
  if (!params.minLength && this.regionsMinLength) {
201
215
  params = {...params, minLength: this.regionsMinLength};
202
216
  }
@@ -284,6 +298,19 @@ export default class RegionsPlugin {
284
298
  this.vertical
285
299
  );
286
300
 
301
+ // set the region channel index based on the clicked area
302
+ if (this.wavesurfer.params.splitChannels) {
303
+ const y = (e.touches ? e.touches[0].clientY : e.clientY) - wrapperRect.top;
304
+ const channelCount = this.wavesurfer.backend.buffer != null ? this.wavesurfer.backend.buffer.numberOfChannels : 1;
305
+ const channelHeight = this.wrapper.clientHeight / channelCount;
306
+ const channelIdx = Math.floor(y / channelHeight);
307
+ params.channelIdx = channelIdx;
308
+ const channelColors = this.wavesurfer.params.splitChannelsOptions.channelColors[channelIdx];
309
+ if (channelColors && channelColors.dragColor) {
310
+ params.color = channelColors.dragColor;
311
+ }
312
+ }
313
+
287
314
  drag = true;
288
315
  start = this.wavesurfer.drawer.handleEvent(e, true);
289
316
  region = null;
@@ -62,6 +62,7 @@ export class Region {
62
62
  // select channel ID to set region
63
63
  let channelIdx =
64
64
  params.channelIdx == null ? -1 : parseInt(params.channelIdx);
65
+ this.channelIdx = channelIdx;
65
66
  this.regionHeight = '100%';
66
67
  this.marginTop = '0px';
67
68
 
@@ -87,7 +88,7 @@ export class Region {
87
88
  }
88
89
 
89
90
  /* Update region params. */
90
- update(params) {
91
+ update(params, eventParams) {
91
92
  if (params.start != null) {
92
93
  this.start = Number(params.start);
93
94
  }
@@ -125,7 +126,7 @@ export class Region {
125
126
 
126
127
  this.updateRender();
127
128
  this.fireEvent('update');
128
- this.wavesurfer.fireEvent('region-updated', this);
129
+ this.wavesurfer.fireEvent('region-updated', this, eventParams);
129
130
  }
130
131
 
131
132
  /* Remove a single region. */
@@ -190,7 +191,7 @@ export class Region {
190
191
 
191
192
  this.style(this.element, {
192
193
  position: 'absolute',
193
- zIndex: 2,
194
+ zIndex: 3,
194
195
  height: this.regionHeight,
195
196
  top: this.marginTop
196
197
  });
@@ -708,10 +709,33 @@ export class Region {
708
709
  delta = this.start * -1;
709
710
  }
710
711
 
712
+ const eventParams = {
713
+ direction: this._getDragDirection(delta),
714
+ action: 'drag'
715
+ };
716
+
711
717
  this.update({
712
718
  start: this.start + delta,
713
719
  end: this.end + delta
714
- });
720
+ }, eventParams);
721
+ }
722
+
723
+ /**
724
+ * Returns the direction of dragging region based on delta
725
+ * Negative delta means region is moving to the left
726
+ * Positive - to the right
727
+ * For zero delta the direction is not defined
728
+ * @param {number} delta Drag offset
729
+ * @returns {string|null} Direction 'left', 'right' or null
730
+ */
731
+ _getDragDirection(delta) {
732
+ if (delta < 0) {
733
+ return 'left';
734
+ }
735
+ if (delta > 0) {
736
+ return 'right';
737
+ }
738
+ return null;
715
739
  }
716
740
 
717
741
  /**
@@ -724,13 +748,22 @@ export class Region {
724
748
  */
725
749
  onResize(delta, direction) {
726
750
  const duration = this.wavesurfer.getDuration();
751
+ const eventParams = {
752
+ action: 'resize',
753
+ direction: direction === 'start' ? 'left' : 'right'
754
+ };
755
+
727
756
  if (direction === 'start') {
728
757
  // Check if changing the start by the given delta would result in the region being smaller than minLength
729
- // Ignore cases where we are making the region wider rather than shrinking it
730
758
  if (delta > 0 && this.end - (this.start + delta) < this.minLength) {
731
759
  delta = this.end - this.minLength - this.start;
732
760
  }
733
761
 
762
+ // Check if changing the start by the given delta would result in the region being larger than maxLength
763
+ if (delta < 0 && this.end - (this.start + delta) > this.maxLength) {
764
+ delta = this.end - this.start - this.maxLength;
765
+ }
766
+
734
767
  if (delta < 0 && (this.start + delta) < 0) {
735
768
  delta = this.start * -1;
736
769
  }
@@ -738,14 +771,18 @@ export class Region {
738
771
  this.update({
739
772
  start: Math.min(this.start + delta, this.end),
740
773
  end: Math.max(this.start + delta, this.end)
741
- });
774
+ }, eventParams);
742
775
  } else {
743
776
  // Check if changing the end by the given delta would result in the region being smaller than minLength
744
- // Ignore cases where we are making the region wider rather than shrinking it
745
777
  if (delta < 0 && this.end + delta - this.start < this.minLength) {
746
778
  delta = this.start + this.minLength - this.end;
747
779
  }
748
780
 
781
+ // Check if changing the end by the given delta would result in the region being larger than maxLength
782
+ if (delta > 0 && this.end + delta - this.start > this.maxLength) {
783
+ delta = this.maxLength - (this.end - this.start);
784
+ }
785
+
749
786
  if (delta > 0 && (this.end + delta) > duration) {
750
787
  delta = duration - this.end;
751
788
  }
@@ -753,7 +790,7 @@ export class Region {
753
790
  this.update({
754
791
  start: Math.min(this.end + delta, this.start),
755
792
  end: Math.max(this.end + delta, this.start)
756
- });
793
+ }, eventParams);
757
794
  }
758
795
  }
759
796
 
@@ -8,6 +8,8 @@ import FFT from './fft';
8
8
  * which to render
9
9
  * @property {number} fftSamples=512 Number of samples to fetch to FFT. Must be
10
10
  * a power of 2.
11
+ * @property {boolean} splitChannels=false Render with separate spectrograms for
12
+ * the channels of the audio
11
13
  * @property {boolean} labels Set to true to display frequency labels.
12
14
  * @property {number} noverlap Size of the overlapping window. Must be <
13
15
  * fftSamples. Auto deduced from canvas size by default.
@@ -127,6 +129,8 @@ export default class SpectrogramPlugin {
127
129
  this.noverlap = params.noverlap;
128
130
  this.windowFunc = params.windowFunc;
129
131
  this.alpha = params.alpha;
132
+ this.splitChannels = params.splitChannels;
133
+ this.channels = this.splitChannels ? ws.backend.buffer.numberOfChannels : 1;
130
134
 
131
135
  this.createWrapper();
132
136
  this.createCanvas();
@@ -176,7 +180,7 @@ export default class SpectrogramPlugin {
176
180
  left: 0,
177
181
  position: 'absolute',
178
182
  zIndex: 9,
179
- height: `${this.height / this.pixelRatio}px`,
183
+ height: `${this.height * this.channels / this.pixelRatio}px`,
180
184
  width: `${55 / this.pixelRatio}px`
181
185
  });
182
186
  this.wrapper.appendChild(labelsEl);
@@ -197,7 +201,7 @@ export default class SpectrogramPlugin {
197
201
  position: 'relative',
198
202
  userSelect: 'none',
199
203
  webkitUserSelect: 'none',
200
- height: `${this.height / this.pixelRatio}px`
204
+ height: `${this.height * this.channels / this.pixelRatio}px`
201
205
  });
202
206
 
203
207
  if (wsParams.fillParent || wsParams.scrollParent) {
@@ -244,57 +248,58 @@ export default class SpectrogramPlugin {
244
248
  updateCanvasStyle() {
245
249
  const width = Math.round(this.width / this.pixelRatio) + 'px';
246
250
  this.canvas.width = this.width;
247
- this.canvas.height = this.height;
251
+ this.canvas.height = this.height * this.channels;
248
252
  this.canvas.style.width = width;
249
253
  }
250
254
 
251
255
  drawSpectrogram(frequenciesData, my) {
256
+ if (!isNaN(frequenciesData[0][0])) { // data is 1ch [sample, freq] format
257
+ // to [channel, sample, freq] format
258
+ frequenciesData = [frequenciesData];
259
+ }
260
+
252
261
  const spectrCc = my.spectrCc;
253
262
  const height = my.height;
254
263
  const width = my.width;
255
- const pixels = my.resample(frequenciesData);
256
- const heightFactor = my.buffer ? 2 / my.buffer.numberOfChannels : 1;
257
- if (spectrCc) {
264
+
265
+ if (!spectrCc) {
266
+ return;
267
+ }
268
+
269
+ for (let c = 0; c < frequenciesData.length; c++) { // for each channel
270
+ const pixels = my.resample(frequenciesData[c]);
258
271
  const imageData = spectrCc.createImageData(width, height);
259
- let i;
260
- let j;
261
- let k;
262
272
 
263
- for (i = 0; i < pixels.length; i++) {
264
- for (j = 0; j < pixels[i].length; j++) {
273
+ for (let i = 0; i < pixels.length; i++) {
274
+ for (let j = 0; j < pixels[i].length; j++) {
265
275
  const colorMap = my.colorMap[pixels[i][j]];
266
- /* eslint-disable max-depth */
267
- for (k = 0; k < heightFactor; k++) {
268
- let y = height - j * heightFactor;
269
- if (heightFactor === 2 && k === 1) {
270
- y--;
271
- }
272
- const redIndex = y * (width * 4) + i * 4;
273
- imageData.data[redIndex] = colorMap[0] * 255;
274
- imageData.data[redIndex + 1] = colorMap[1] * 255;
275
- imageData.data[redIndex + 2] = colorMap[2] * 255;
276
- imageData.data[redIndex + 3] = colorMap[3] * 255;
277
- }
278
- /* eslint-enable max-depth */
276
+ const redIndex = ((height - j) * width + i) * 4;
277
+ imageData.data[redIndex] = colorMap[0] * 255;
278
+ imageData.data[redIndex + 1] = colorMap[1] * 255;
279
+ imageData.data[redIndex + 2] = colorMap[2] * 255;
280
+ imageData.data[redIndex + 3] = colorMap[3] * 255;
279
281
  }
280
282
  }
281
- spectrCc.putImageData(imageData, 0, 0);
283
+
284
+ // stack spectrograms
285
+ spectrCc.putImageData(imageData, 0, height * c);
282
286
  }
283
287
  }
284
288
 
285
289
  getFrequencies(callback) {
286
290
  const fftSamples = this.fftSamples;
287
291
  const buffer = (this.buffer = this.wavesurfer.backend.buffer);
288
- const channelOne = buffer.getChannelData(0);
289
- const bufferLength = buffer.length;
290
- const sampleRate = buffer.sampleRate;
291
- const frequencies = [];
292
+ const channels = this.channels;
292
293
 
293
294
  if (!buffer) {
294
295
  this.fireEvent('error', 'Web Audio buffer is not available');
295
296
  return;
296
297
  }
297
298
 
299
+ // This may differ from file samplerate. Browser resamples audio.
300
+ const sampleRate = buffer.sampleRate;
301
+ const frequencies = [];
302
+
298
303
  let noverlap = this.noverlap;
299
304
  if (!noverlap) {
300
305
  const uniqueSamplesPerPx = buffer.length / this.canvas.width;
@@ -307,24 +312,30 @@ export default class SpectrogramPlugin {
307
312
  this.windowFunc,
308
313
  this.alpha
309
314
  );
310
- const maxSlicesCount = Math.floor(
311
- bufferLength / (fftSamples - noverlap)
312
- );
313
- let currentOffset = 0;
314
315
 
315
- while (currentOffset + fftSamples < channelOne.length) {
316
- const segment = channelOne.slice(
317
- currentOffset,
318
- currentOffset + fftSamples
319
- );
320
- const spectrum = fft.calculateSpectrum(segment);
321
- const array = new Uint8Array(fftSamples / 2);
322
- let j;
323
- for (j = 0; j < fftSamples / 2; j++) {
324
- array[j] = Math.max(-255, Math.log10(spectrum[j]) * 45);
316
+ for (let c = 0; c < channels; c++) { // for each channel
317
+ const channelData = buffer.getChannelData(c);
318
+ const channelFreq = [];
319
+ let currentOffset = 0;
320
+
321
+ while (currentOffset + fftSamples < channelData.length) {
322
+ const segment = channelData.slice(
323
+ currentOffset,
324
+ currentOffset + fftSamples
325
+ );
326
+ const spectrum = fft.calculateSpectrum(segment);
327
+ const array = new Uint8Array(fftSamples / 2);
328
+ let j;
329
+ for (j = 0; j < fftSamples / 2; j++) {
330
+ array[j] = Math.max(-255, Math.log10(spectrum[j]) * 45);
331
+ }
332
+ channelFreq.push(array);
333
+ // channelFreq: [sample, freq]
334
+
335
+ currentOffset += fftSamples - noverlap;
325
336
  }
326
- frequencies.push(array);
327
- currentOffset += fftSamples - noverlap;
337
+ frequencies.push(channelFreq);
338
+ // frequencies: [channel, sample, freq]
328
339
  }
329
340
  callback(frequencies, this);
330
341
  }
@@ -377,13 +388,17 @@ export default class SpectrogramPlugin {
377
388
 
378
389
  // prepare canvas element for labels
379
390
  const ctx = this.labelsEl.getContext('2d');
380
- this.labelsEl.height = this.height;
391
+ this.labelsEl.height = this.height * this.channels;
381
392
  this.labelsEl.width = bgWidth;
382
393
 
383
- if (ctx) {
394
+ if (!ctx) {
395
+ return;
396
+ }
397
+
398
+ for (let c = 0; c < this.channels; c++) { // for each channel
384
399
  // fill background
385
400
  ctx.fillStyle = bgFill;
386
- ctx.fillRect(0, 0, bgWidth, getMaxY);
401
+ ctx.fillRect(0, c * getMaxY, bgWidth, (1 + c) * getMaxY);
387
402
  ctx.fill();
388
403
  let i;
389
404
 
@@ -403,7 +418,7 @@ export default class SpectrogramPlugin {
403
418
  let y;
404
419
 
405
420
  if (i == 0) {
406
- y = getMaxY + i - 10;
421
+ y = (1 + c) * getMaxY + i - 10;
407
422
  // unit label
408
423
  ctx.fillStyle = textColorUnit;
409
424
  ctx.font = fontSizeUnit + ' ' + fontType;
@@ -413,7 +428,7 @@ export default class SpectrogramPlugin {
413
428
  ctx.font = fontSizeFreq + ' ' + fontType;
414
429
  ctx.fillText(label, x, y);
415
430
  } else {
416
- y = getMaxY - i * 50 + yLabelOffset;
431
+ y = (1 + c) * getMaxY - i * 50 + yLabelOffset;
417
432
  // unit label
418
433
  ctx.fillStyle = textColorUnit;
419
434
  ctx.font = fontSizeUnit + ' ' + fontType;
@@ -505,18 +505,20 @@ export default class TimelinePlugin {
505
505
 
506
506
  this.canvases.forEach(canvas => {
507
507
  const context = canvas.getContext('2d');
508
- const canvasWidth = context.canvas.width;
508
+ if (context) {
509
+ const canvasWidth = context.canvas.width;
509
510
 
510
- if (xOffset > x + textWidth) {
511
- return;
512
- }
511
+ if (xOffset > x + textWidth) {
512
+ return;
513
+ }
513
514
 
514
- if (xOffset + canvasWidth > x && context) {
515
- textWidth = context.measureText(text).width;
516
- context.fillText(text, x - xOffset, y);
517
- }
515
+ if (xOffset + canvasWidth > x && context) {
516
+ textWidth = context.measureText(text).width;
517
+ context.fillText(text, x - xOffset, y);
518
+ }
518
519
 
519
- xOffset += canvasWidth;
520
+ xOffset += canvasWidth;
521
+ }
520
522
  });
521
523
  }
522
524
 
package/src/util/index.js CHANGED
@@ -11,3 +11,4 @@ export { default as preventClick } from './prevent-click';
11
11
  export { default as fetchFile } from './fetch';
12
12
  export { default as clamp } from './clamp';
13
13
  export { default as withOrientation } from './orientation';
14
+ export { default as ignoreSilenceMode } from './silence-mode';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Ignores device silence mode when using the `WebAudio` backend.
3
+ *
4
+ * Many mobile devices contain a hardware button to mute the ringtone for incoming
5
+ * calls and messages. Unfortunately, on some platforms like iOS, this also mutes
6
+ * wavesurfer's audio when using the `WebAudio` backend. This function creates a
7
+ * temporary `<audio>` element that makes sure the WebAudio backend keeps playing
8
+ * when muting the device ringer.
9
+ *
10
+ * @since 5.2.0
11
+ */
12
+ export default function ignoreSilenceMode() {
13
+ // Set the src to a short bit of url encoded as a silent mp3
14
+ // NOTE The silence MP3 must be high quality, when web audio sounds are played
15
+ // in parallel the web audio sound is mixed to match the bitrate of the html sound
16
+ // 0.01 seconds of silence VBR220-260 Joint Stereo 859B
17
+ const audioData = "data:audio/mpeg;base64,//uQxAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA//////////////////////////////////////////////////////////////////8AAABhTEFNRTMuMTAwA8MAAAAAAAAAABQgJAUHQQAB9AAAAnGMHkkIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//sQxAADgnABGiAAQBCqgCRMAAgEAH///////////////7+n/9FTuQsQH//////2NG0jWUGlio5gLQTOtIoeR2WX////X4s9Atb/JRVCbBUpeRUq//////////////////9RUi0f2jn/+xDECgPCjAEQAABN4AAANIAAAAQVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==";
18
+
19
+ // disable iOS Airplay (setting the attribute in js doesn't work)
20
+ let tmp = document.createElement("div");
21
+ tmp.innerHTML = '<audio x-webkit-airplay="deny"></audio>';
22
+
23
+ let audioSilentMode = tmp.children.item(0);
24
+ audioSilentMode.src = audioData;
25
+ audioSilentMode.preload = "auto";
26
+ audioSilentMode.type = "audio/mpeg";
27
+ audioSilentMode.disableRemotePlayback = true;
28
+
29
+ // play
30
+ audioSilentMode.play();
31
+
32
+ // cleanup
33
+ audioSilentMode.remove();
34
+ tmp.remove();
35
+ }