source: gs3-extensions/mars-src/trunk/src/wavesurfer-plugins/spectrogram.js@ 36964

Last change on this file since 36964 was 34405, checked in by davidb, 4 years ago

Location for some bespoke plugin work to fits in with wavesurfer

File size: 22.6 KB
Line 
1/**
2 * Calculate FFT - Based on https://github.com/corbanbrook/dsp.js
3 */
4/* eslint-disable complexity, no-redeclare, no-var, one-var */
5const FFT = function(bufferSize, sampleRate, windowFunc, alpha) {
6 this.bufferSize = bufferSize;
7 this.sampleRate = sampleRate;
8 this.bandwidth = (2 / bufferSize) * (sampleRate / 2);
9
10 this.sinTable = new Float32Array(bufferSize);
11 this.cosTable = new Float32Array(bufferSize);
12 this.windowValues = new Float32Array(bufferSize);
13 this.reverseTable = new Uint32Array(bufferSize);
14
15 this.peakBand = 0;
16 this.peak = 0;
17
18 var i;
19 switch (windowFunc) {
20 case 'bartlett':
21 for (i = 0; i < bufferSize; i++) {
22 this.windowValues[i] =
23 (2 / (bufferSize - 1)) *
24 ((bufferSize - 1) / 2 - Math.abs(i - (bufferSize - 1) / 2));
25 }
26 break;
27 case 'bartlettHann':
28 for (i = 0; i < bufferSize; i++) {
29 this.windowValues[i] =
30 0.62 -
31 0.48 * Math.abs(i / (bufferSize - 1) - 0.5) -
32 0.38 * Math.cos((Math.PI * 2 * i) / (bufferSize - 1));
33 }
34 break;
35 case 'blackman':
36 alpha = alpha || 0.16;
37 for (i = 0; i < bufferSize; i++) {
38 this.windowValues[i] =
39 (1 - alpha) / 2 -
40 0.5 * Math.cos((Math.PI * 2 * i) / (bufferSize - 1)) +
41 (alpha / 2) *
42 Math.cos((4 * Math.PI * i) / (bufferSize - 1));
43 }
44 break;
45 case 'cosine':
46 for (i = 0; i < bufferSize; i++) {
47 this.windowValues[i] = Math.cos(
48 (Math.PI * i) / (bufferSize - 1) - Math.PI / 2
49 );
50 }
51 break;
52 case 'gauss':
53 alpha = alpha || 0.25;
54 for (i = 0; i < bufferSize; i++) {
55 this.windowValues[i] = Math.pow(
56 Math.E,
57 -0.5 *
58 Math.pow(
59 (i - (bufferSize - 1) / 2) /
60 ((alpha * (bufferSize - 1)) / 2),
61 2
62 )
63 );
64 }
65 break;
66 case 'hamming':
67 for (i = 0; i < bufferSize; i++) {
68 this.windowValues[i] =
69 0.54 -
70 0.46 * Math.cos((Math.PI * 2 * i) / (bufferSize - 1));
71 }
72 break;
73 case 'hann':
74 case undefined:
75 for (i = 0; i < bufferSize; i++) {
76 this.windowValues[i] =
77 0.5 * (1 - Math.cos((Math.PI * 2 * i) / (bufferSize - 1)));
78 }
79 break;
80 case 'lanczoz':
81 for (i = 0; i < bufferSize; i++) {
82 this.windowValues[i] =
83 Math.sin(Math.PI * ((2 * i) / (bufferSize - 1) - 1)) /
84 (Math.PI * ((2 * i) / (bufferSize - 1) - 1));
85 }
86 break;
87 case 'rectangular':
88 for (i = 0; i < bufferSize; i++) {
89 this.windowValues[i] = 1;
90 }
91 break;
92 case 'triangular':
93 for (i = 0; i < bufferSize; i++) {
94 this.windowValues[i] =
95 (2 / bufferSize) *
96 (bufferSize / 2 - Math.abs(i - (bufferSize - 1) / 2));
97 }
98 break;
99 default:
100 throw Error("No such window function '" + windowFunc + "'");
101 }
102
103 var limit = 1;
104 var bit = bufferSize >> 1;
105 var i;
106
107 while (limit < bufferSize) {
108 for (i = 0; i < limit; i++) {
109 this.reverseTable[i + limit] = this.reverseTable[i] + bit;
110 }
111
112 limit = limit << 1;
113 bit = bit >> 1;
114 }
115
116 for (i = 0; i < bufferSize; i++) {
117 this.sinTable[i] = Math.sin(-Math.PI / i);
118 this.cosTable[i] = Math.cos(-Math.PI / i);
119 }
120
121 this.calculateSpectrum = function(buffer) {
122 // Locally scope variables for speed up
123 var bufferSize = this.bufferSize,
124 cosTable = this.cosTable,
125 sinTable = this.sinTable,
126 reverseTable = this.reverseTable,
127 real = new Float32Array(bufferSize),
128 imag = new Float32Array(bufferSize),
129 bSi = 2 / this.bufferSize,
130 sqrt = Math.sqrt,
131 rval,
132 ival,
133 mag,
134 spectrum = new Float32Array(bufferSize / 2);
135
136 var k = Math.floor(Math.log(bufferSize) / Math.LN2);
137
138 if (Math.pow(2, k) !== bufferSize) {
139 throw 'Invalid buffer size, must be a power of 2.';
140 }
141 if (bufferSize !== buffer.length) {
142 throw 'Supplied buffer is not the same size as defined FFT. FFT Size: ' +
143 bufferSize +
144 ' Buffer Size: ' +
145 buffer.length;
146 }
147
148 var halfSize = 1,
149 phaseShiftStepReal,
150 phaseShiftStepImag,
151 currentPhaseShiftReal,
152 currentPhaseShiftImag,
153 off,
154 tr,
155 ti,
156 tmpReal;
157
158 for (var i = 0; i < bufferSize; i++) {
159 real[i] =
160 buffer[reverseTable[i]] * this.windowValues[reverseTable[i]];
161 imag[i] = 0;
162 }
163
164 while (halfSize < bufferSize) {
165 phaseShiftStepReal = cosTable[halfSize];
166 phaseShiftStepImag = sinTable[halfSize];
167
168 currentPhaseShiftReal = 1;
169 currentPhaseShiftImag = 0;
170
171 for (var fftStep = 0; fftStep < halfSize; fftStep++) {
172 var i = fftStep;
173
174 while (i < bufferSize) {
175 off = i + halfSize;
176 tr =
177 currentPhaseShiftReal * real[off] -
178 currentPhaseShiftImag * imag[off];
179 ti =
180 currentPhaseShiftReal * imag[off] +
181 currentPhaseShiftImag * real[off];
182
183 real[off] = real[i] - tr;
184 imag[off] = imag[i] - ti;
185 real[i] += tr;
186 imag[i] += ti;
187
188 i += halfSize << 1;
189 }
190
191 tmpReal = currentPhaseShiftReal;
192 currentPhaseShiftReal =
193 tmpReal * phaseShiftStepReal -
194 currentPhaseShiftImag * phaseShiftStepImag;
195 currentPhaseShiftImag =
196 tmpReal * phaseShiftStepImag +
197 currentPhaseShiftImag * phaseShiftStepReal;
198 }
199
200 halfSize = halfSize << 1;
201 }
202
203 for (var i = 0, N = bufferSize / 2; i < N; i++) {
204 rval = real[i];
205 ival = imag[i];
206 mag = bSi * sqrt(rval * rval + ival * ival);
207
208 if (mag > this.peak) {
209 this.peakBand = i;
210 this.peak = mag;
211 }
212 spectrum[i] = mag;
213 }
214 return spectrum;
215 };
216};
217/* eslint-enable complexity, no-redeclare, no-var, one-var */
218
219/**
220 * @typedef {Object} SpectrogramPluginParams
221 * @property {string|HTMLElement} container Selector of element or element in
222 * which to render
223 * @property {number} fftSamples=512 Number of samples to fetch to FFT. Must be
224 * a power of 2.
225 * @property {boolean} labels Set to true to display frequency labels.
226 * @property {number} noverlap Size of the overlapping window. Must be <
227 * fftSamples. Auto deduced from canvas size by default.
228 * @property {string} windowFunc='hann' The window function to be used. One of
229 * these: `'bartlett'`, `'bartlettHann'`, `'blackman'`, `'cosine'`, `'gauss'`,
230 * `'hamming'`, `'hann'`, `'lanczoz'`, `'rectangular'`, `'triangular'`
231 * @property {?number} alpha Some window functions have this extra value.
232 * (Between 0 and 1)
233 * @property {number} pixelRatio=wavesurfer.params.pixelRatio to control the
234 * size of the spectrogram in relation with its canvas. 1 = Draw on the whole
235 * canvas. 2 = Draw on a quarter (1/2 the length and 1/2 the width)
236 * @property {?boolean} deferInit Set to true to manually call
237 * `initPlugin('spectrogram')`
238 * @property {?number[][]} colorMap A 256 long array of 3- or 4-element arrays.
239 * Each entry should contain a float between 0 and 1 and specify
240 * r, g, b, and (optionally) alpha.
241 */
242
243/**
244 * Render a spectrogram visualisation of the audio.
245 *
246 * @implements {PluginClass}
247 * @extends {Observer}
248 * @example
249 * // es6
250 * import SpectrogramPlugin from 'wavesurfer.spectrogram.js';
251 *
252 * // commonjs
253 * var SpectrogramPlugin = require('wavesurfer.spectrogram.js');
254 *
255 * // if you are using <script> tags
256 * var SpectrogramPlugin = window.WaveSurfer.spectrogram;
257 *
258 * // ... initialising wavesurfer with the plugin
259 * var wavesurfer = WaveSurfer.create({
260 * // wavesurfer options ...
261 * plugins: [
262 * SpectrogramPlugin.create({
263 * // plugin options ...
264 * })
265 * ]
266 * });
267 */
268export default class SpectrogramPlugin {
269 /**
270 * Spectrogram plugin definition factory
271 *
272 * This function must be used to create a plugin definition which can be
273 * used by wavesurfer to correctly instantiate the plugin.
274 *
275 * @param {SpectrogramPluginParams} params Parameters used to initialise the plugin
276 * @return {PluginDefinition} An object representing the plugin.
277 */
278 static create(params) {
279 return {
280 name: 'spectrogram',
281 deferInit: params && params.deferInit ? params.deferInit : false,
282 params: params,
283 staticProps: {
284 FFT: FFT
285 },
286 instance: SpectrogramPlugin
287 };
288 }
289
290 constructor(params, ws) {
291 this.params = params;
292 this.wavesurfer = ws;
293 this.util = ws.util;
294
295 this.frequenciesDataUrl = params.frequenciesDataUrl;
296 this._onScroll = e => {
297 this.updateScroll(e);
298 };
299 this._onRender = () => {
300 this.render();
301 };
302 this._onWrapperClick = e => {
303 this._wrapperClickHandler(e);
304 };
305 this._onReady = () => {
306 const drawer = (this.drawer = ws.drawer);
307
308 this.container =
309 'string' == typeof params.container
310 ? document.querySelector(params.container)
311 : params.container;
312
313 if (!this.container) {
314 throw Error('No container for WaveSurfer spectrogram');
315 }
316 if (params.colorMap) {
317 if (params.colorMap.length < 256) {
318 throw new Error('Colormap must contain 256 elements');
319 }
320 for (let i = 0; i < params.colorMap.length; i++) {
321 const cmEntry = params.colorMap[i];
322 if ((cmEntry.length !== 4) && (cmEntry.length !== 3)) {
323 throw new Error(
324 'ColorMap entries must contain 3 (rgb) or 4 (rgba) values'
325 );
326 }
327 }
328 this.colorMap = params.colorMap;
329 } else {
330 this.colorMap = [];
331 for (let i = 0; i < 256; i++) {
332 const val = (255 - i) / 256;
333 this.colorMap.push([val, val, val, 1]);
334 }
335 }
336 this.width = drawer.width;
337 this.pixelRatio = this.params.pixelRatio || ws.params.pixelRatio;
338 this.fftSamples =
339 this.params.fftSamples || ws.params.fftSamples || 512;
340 this.height = this.fftSamples / 2;
341 this.noverlap = params.noverlap;
342 this.windowFunc = params.windowFunc;
343 this.alpha = params.alpha;
344
345 this.createWrapper();
346 this.createCanvas();
347 this.render();
348
349 drawer.wrapper.addEventListener('scroll', this._onScroll);
350 ws.on('redraw', this._onRender);
351 };
352 }
353
354 init() {
355 // Check if wavesurfer is ready
356 if (this.wavesurfer.isReady) {
357 this._onReady();
358 } else {
359 this.wavesurfer.once('ready', this._onReady);
360 }
361 }
362
363 destroy() {
364 this.unAll();
365 this.wavesurfer.un('ready', this._onReady);
366 this.wavesurfer.un('redraw', this._onRender);
367 this.drawer && this.drawer.wrapper.removeEventListener('scroll', this._onScroll);
368 this.wavesurfer = null;
369 this.util = null;
370 this.params = null;
371 if (this.wrapper) {
372 this.wrapper.removeEventListener('click', this._onWrapperClick);
373 this.wrapper.parentNode.removeChild(this.wrapper);
374 this.wrapper = null;
375 }
376 }
377
378 createWrapper() {
379 const prevSpectrogram = this.container.querySelector('spectrogram');
380 if (prevSpectrogram) {
381 this.container.removeChild(prevSpectrogram);
382 }
383 const wsParams = this.wavesurfer.params;
384 this.wrapper = document.createElement('spectrogram');
385 // if labels are active
386 if (this.params.labels) {
387 const labelsEl = (this.labelsEl = document.createElement('canvas'));
388 labelsEl.classList.add('spec-labels');
389 this.drawer.style(labelsEl, {
390 left: 0,
391 position: 'absolute',
392 zIndex: 9,
393 height: `${this.height / this.pixelRatio}px`,
394 width: `${55 / this.pixelRatio}px`
395 });
396 this.wrapper.appendChild(labelsEl);
397 this.loadLabels(
398 'rgba(68,68,68,0.5)',
399 '12px',
400 '10px',
401 '',
402 '#fff',
403 '#f7f7f7',
404 'center',
405 '#specLabels'
406 );
407 }
408
409 this.drawer.style(this.wrapper, {
410 display: 'block',
411 position: 'relative',
412 userSelect: 'none',
413 webkitUserSelect: 'none',
414 height: `${this.height / this.pixelRatio}px`
415 });
416
417 if (wsParams.fillParent || wsParams.scrollParent) {
418 this.drawer.style(this.wrapper, {
419 width: '100%',
420 overflowX: 'hidden',
421 overflowY: 'hidden'
422 });
423 }
424 this.container.appendChild(this.wrapper);
425
426 this.wrapper.addEventListener('click', this._onWrapperClick);
427 }
428
429 _wrapperClickHandler(event) {
430 event.preventDefault();
431 const relX = 'offsetX' in event ? event.offsetX : event.layerX;
432 this.fireEvent('click', relX / this.width || 0);
433 }
434
435 createCanvas() {
436 const canvas = (this.canvas = this.wrapper.appendChild(
437 document.createElement('canvas')
438 ));
439
440 this.spectrCc = canvas.getContext('2d');
441
442 this.util.style(canvas, {
443 position: 'absolute',
444 zIndex: 4
445 });
446 }
447
448 render() {
449 this.updateCanvasStyle();
450
451 if (this.frequenciesDataUrl) {
452 this.loadFrequenciesData(this.frequenciesDataUrl);
453 } else {
454 this.getFrequencies(this.drawSpectrogram);
455 }
456 }
457
458 updateCanvasStyle() {
459 const width = Math.round(this.width / this.pixelRatio) + 'px';
460 this.canvas.width = this.width;
461 this.canvas.height = this.height;
462 this.canvas.style.width = width;
463 }
464
465 drawSpectrogram(frequenciesData, my) {
466 console.log("spec ....spec.length = " + frequenciesData.length)
467 console.log(frequenciesData);
468 const spectrCc = my.spectrCc;
469 const length = my.wavesurfer.backend.getDuration();
470 const height = my.height;
471 const pixels = my.resample(frequenciesData);
472 const heightFactor = my.buffer ? 2 / my.buffer.numberOfChannels : 1;
473 let i;
474 let j;
475
476 for (i = 0; i < pixels.length; i++) {
477 for (j = 0; j < pixels[i].length; j++) {
478 const colorMap = my.colorMap[pixels[i][j]];
479 if (colorMap.length == 4) {
480 my.spectrCc.fillStyle =
481 'rgba(' +
482 colorMap[0] * 256 +
483 ', ' +
484 colorMap[1] * 256 +
485 ', ' +
486 colorMap[2] * 256 +
487 ',' +
488 colorMap[3] +
489 ')';
490 }
491 else {
492 my.spectrCc.fillStyle =
493 'rgb(' +
494 colorMap[0] * 256 +
495 ', ' +
496 colorMap[1] * 256 +
497 ', ' +
498 colorMap[2] * 256 +
499 ')';
500 }
501
502 my.spectrCc.fillRect(
503 i,
504 height - j * heightFactor,
505 1,
506 heightFactor
507 );
508 }
509 }
510 }
511
512 getFrequencies(callback) {
513 const fftSamples = this.fftSamples;
514 const buffer = (this.buffer = this.wavesurfer.backend.buffer);
515 const channelOne = buffer.getChannelData(0);
516 const bufferLength = buffer.length;
517 const sampleRate = buffer.sampleRate;
518 const frequencies = [];
519
520 if (!buffer) {
521 this.fireEvent('error', 'Web Audio buffer is not available');
522 return;
523 }
524
525 let noverlap = this.noverlap;
526 if (!noverlap) {
527 const uniqueSamplesPerPx = buffer.length / this.canvas.width;
528 noverlap = Math.max(0, Math.round(fftSamples - uniqueSamplesPerPx));
529 }
530
531 const fft = new FFT(
532 fftSamples,
533 sampleRate,
534 this.windowFunc,
535 this.alpha
536 );
537 const maxSlicesCount = Math.floor(
538 bufferLength / (fftSamples - noverlap)
539 );
540 let currentOffset = 0;
541
542 while (currentOffset + fftSamples < channelOne.length) {
543 const segment = channelOne.slice(
544 currentOffset,
545 currentOffset + fftSamples
546 );
547 const spectrum = fft.calculateSpectrum(segment);
548 const array = new Uint8Array(fftSamples / 2);
549 let j;
550 for (j = 0; j < fftSamples / 2; j++) {
551 array[j] = Math.max(-255, Math.log10(spectrum[j]) * 45);
552 }
553 frequencies.push(array);
554 currentOffset += fftSamples - noverlap;
555 }
556 callback(frequencies, this);
557 }
558
559 loadFrequenciesData(url) {
560 const request = this.util.fetchFile({ url: url });
561
562 request.on('success', data =>
563 this.drawSpectrogram(JSON.parse(data), this)
564 );
565 request.on('error', e => this.fireEvent('error', e));
566
567 return request;
568 }
569
570 freqType(freq) {
571 return freq >= 1000 ? (freq / 1000).toFixed(1) : Math.round(freq);
572 }
573
574 unitType(freq) {
575 return freq >= 1000 ? 'KHz' : 'Hz';
576 }
577
578 loadLabels(
579 bgFill,
580 fontSizeFreq,
581 fontSizeUnit,
582 fontType,
583 textColorFreq,
584 textColorUnit,
585 textAlign,
586 container
587 ) {
588 const frequenciesHeight = this.height;
589 bgFill = bgFill || 'rgba(68,68,68,0)';
590 fontSizeFreq = fontSizeFreq || '12px';
591 fontSizeUnit = fontSizeUnit || '10px';
592 fontType = fontType || 'Helvetica';
593 textColorFreq = textColorFreq || '#fff';
594 textColorUnit = textColorUnit || '#fff';
595 textAlign = textAlign || 'center';
596 container = container || '#specLabels';
597 const bgWidth = 55;
598 const getMaxY = frequenciesHeight || 512;
599 const labelIndex = 5 * (getMaxY / 256);
600 const freqStart = 0;
601 const step =
602 (this.wavesurfer.backend.ac.sampleRate / 2 - freqStart) /
603 labelIndex;
604
605 // prepare canvas element for labels
606 const ctx = this.labelsEl.getContext('2d');
607 this.labelsEl.height = this.height;
608 this.labelsEl.width = bgWidth;
609
610 // fill background
611 ctx.fillStyle = bgFill;
612 ctx.fillRect(0, 0, bgWidth, getMaxY);
613 ctx.fill();
614 let i;
615
616 // render labels
617 for (i = 0; i <= labelIndex; i++) {
618 ctx.textAlign = textAlign;
619 ctx.textBaseline = 'middle';
620
621 const freq = freqStart + step * i;
622 const index = Math.round(
623 (freq / (this.sampleRate / 2)) * this.fftSamples
624 );
625 const label = this.freqType(freq);
626 const units = this.unitType(freq);
627 const yLabelOffset = 2;
628 const x = 16;
629 let y;
630
631 if (i == 0) {
632 y = getMaxY + i - 10;
633 // unit label
634 ctx.fillStyle = textColorUnit;
635 ctx.font = fontSizeUnit + ' ' + fontType;
636 ctx.fillText(units, x + 24, y);
637 // freq label
638 ctx.fillStyle = textColorFreq;
639 ctx.font = fontSizeFreq + ' ' + fontType;
640 ctx.fillText(label, x, y);
641 } else {
642 y = getMaxY - i * 50 + yLabelOffset;
643 // unit label
644 ctx.fillStyle = textColorUnit;
645 ctx.font = fontSizeUnit + ' ' + fontType;
646 ctx.fillText(units, x + 24, y);
647 // freq label
648 ctx.fillStyle = textColorFreq;
649 ctx.font = fontSizeFreq + ' ' + fontType;
650 ctx.fillText(label, x, y);
651 }
652 }
653 }
654
655 updateScroll(e) {
656 if (this.wrapper) {
657 this.wrapper.scrollLeft = e.target.scrollLeft;
658 }
659 }
660
661 resample(oldMatrix) {
662 const columnsNumber = this.width;
663 const newMatrix = [];
664
665 const oldPiece = 1 / oldMatrix.length;
666 const newPiece = 1 / columnsNumber;
667 let i;
668
669 for (i = 0; i < columnsNumber; i++) {
670 const column = new Array(oldMatrix[0].length);
671 let j;
672
673 for (j = 0; j < oldMatrix.length; j++) {
674 const oldStart = j * oldPiece;
675 const oldEnd = oldStart + oldPiece;
676 const newStart = i * newPiece;
677 const newEnd = newStart + newPiece;
678
679 const overlap =
680 oldEnd <= newStart || newEnd <= oldStart
681 ? 0
682 : Math.min(
683 Math.max(oldEnd, newStart),
684 Math.max(newEnd, oldStart)
685 ) -
686 Math.max(
687 Math.min(oldEnd, newStart),
688 Math.min(newEnd, oldStart)
689 );
690 let k;
691 /* eslint-disable max-depth */
692 if (overlap > 0) {
693 for (k = 0; k < oldMatrix[0].length; k++) {
694 if (column[k] == null) {
695 column[k] = 0;
696 }
697 column[k] += (overlap / newPiece) * oldMatrix[j][k];
698 }
699 }
700 /* eslint-enable max-depth */
701 }
702
703 const intColumn = new Uint8Array(oldMatrix[0].length);
704 let m;
705
706 for (m = 0; m < oldMatrix[0].length; m++) {
707 intColumn[m] = column[m];
708 }
709
710 newMatrix.push(intColumn);
711 }
712
713 return newMatrix;
714 }
715}
Note: See TracBrowser for help on using the repository browser.