spectrogram.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. /**
  2. * Calculate FFT - Based on https://github.com/corbanbrook/dsp.js
  3. */
  4. /* eslint-disable complexity, no-redeclare, no-var, one-var */
  5. const FFT = function(bufferSize, sampleRate, windowFunc, alpha) {
  6. this.bufferSize = bufferSize;
  7. this.sampleRate = sampleRate;
  8. this.bandwidth = 2 / bufferSize * sampleRate / 2;
  9. this.sinTable = new Float32Array(bufferSize);
  10. this.cosTable = new Float32Array(bufferSize);
  11. this.windowValues = new Float32Array(bufferSize);
  12. this.reverseTable = new Uint32Array(bufferSize);
  13. this.peakBand = 0;
  14. this.peak = 0;
  15. switch (windowFunc) {
  16. case 'bartlett' :
  17. for (var i = 0; i<bufferSize; i++) {
  18. this.windowValues[i] = 2 / (bufferSize - 1) * ((bufferSize - 1) / 2 - Math.abs(i - (bufferSize - 1) / 2));
  19. }
  20. break;
  21. case 'bartlettHann' :
  22. for (var i = 0; i<bufferSize; i++) {
  23. this.windowValues[i] = 0.62 - 0.48 * Math.abs(i / (bufferSize - 1) - 0.5) - 0.38 * Math.cos(Math.PI * 2 * i / (bufferSize - 1));
  24. }
  25. break;
  26. case 'blackman' :
  27. alpha = alpha || 0.16;
  28. for (var i = 0; i<bufferSize; i++) {
  29. this.windowValues[i] = (1 - alpha)/2 - 0.5 * Math.cos(Math.PI * 2 * i / (bufferSize - 1)) + alpha/2 * Math.cos(4 * Math.PI * i / (bufferSize - 1));
  30. }
  31. break;
  32. case 'cosine' :
  33. for (var i = 0; i<bufferSize; i++) {
  34. this.windowValues[i] = Math.cos(Math.PI * i / (bufferSize - 1) - Math.PI / 2);
  35. }
  36. break;
  37. case 'gauss' :
  38. alpha = alpha || 0.25;
  39. for (var i = 0; i<bufferSize; i++) {
  40. this.windowValues[i] = Math.pow(Math.E, -0.5 * Math.pow((i - (bufferSize - 1) / 2) / (alpha * (bufferSize - 1) / 2), 2));
  41. }
  42. break;
  43. case 'hamming' :
  44. for (var i = 0; i<bufferSize; i++) {
  45. this.windowValues[i] = 0.54 - 0.46 * Math.cos(Math.PI * 2 * i / (bufferSize - 1));
  46. }
  47. break;
  48. case 'hann' :
  49. case undefined :
  50. for (var i = 0; i<bufferSize; i++) {
  51. this.windowValues[i] = 0.5 * (1 - Math.cos(Math.PI * 2 * i / (bufferSize - 1)));
  52. }
  53. break;
  54. case 'lanczoz' :
  55. for (var i = 0; i<bufferSize; i++) {
  56. this.windowValues[i] = Math.sin(Math.PI * (2 * i / (bufferSize - 1) - 1)) / (Math.PI * (2 * i / (bufferSize - 1) - 1));
  57. }
  58. break;
  59. case 'rectangular' :
  60. for (var i = 0; i<bufferSize; i++) {
  61. this.windowValues[i] = 1;
  62. }
  63. break;
  64. case 'triangular' :
  65. for (var i = 0; i<bufferSize; i++) {
  66. this.windowValues[i] = 2 / bufferSize * (bufferSize / 2 - Math.abs(i - (bufferSize - 1) / 2));
  67. }
  68. break;
  69. default:
  70. throw Error('No such window function \'' + windowFunc + '\'');
  71. }
  72. var limit = 1;
  73. var bit = bufferSize >> 1;
  74. var i;
  75. while (limit < bufferSize) {
  76. for (i = 0; i < limit; i++) {
  77. this.reverseTable[i + limit] = this.reverseTable[i] + bit;
  78. }
  79. limit = limit << 1;
  80. bit = bit >> 1;
  81. }
  82. for (i = 0; i < bufferSize; i++) {
  83. this.sinTable[i] = Math.sin(-Math.PI/i);
  84. this.cosTable[i] = Math.cos(-Math.PI/i);
  85. }
  86. this.calculateSpectrum = function(buffer) {
  87. // Locally scope variables for speed up
  88. var bufferSize = this.bufferSize,
  89. cosTable = this.cosTable,
  90. sinTable = this.sinTable,
  91. reverseTable = this.reverseTable,
  92. real = new Float32Array(bufferSize),
  93. imag = new Float32Array(bufferSize),
  94. bSi = 2 / this.bufferSize,
  95. sqrt = Math.sqrt,
  96. rval,
  97. ival,
  98. mag,
  99. spectrum = new Float32Array(bufferSize / 2);
  100. var k = Math.floor(Math.log(bufferSize) / Math.LN2);
  101. if (Math.pow(2, k) !== bufferSize) {
  102. throw 'Invalid buffer size, must be a power of 2.';
  103. }
  104. if (bufferSize !== buffer.length) {
  105. throw 'Supplied buffer is not the same size as defined FFT. FFT Size: ' + bufferSize + ' Buffer Size: ' + buffer.length;
  106. }
  107. var halfSize = 1,
  108. phaseShiftStepReal,
  109. phaseShiftStepImag,
  110. currentPhaseShiftReal,
  111. currentPhaseShiftImag,
  112. off,
  113. tr,
  114. ti,
  115. tmpReal;
  116. for (var i = 0; i < bufferSize; i++) {
  117. real[i] = buffer[reverseTable[i]] * this.windowValues[reverseTable[i]];
  118. imag[i] = 0;
  119. }
  120. while (halfSize < bufferSize) {
  121. phaseShiftStepReal = cosTable[halfSize];
  122. phaseShiftStepImag = sinTable[halfSize];
  123. currentPhaseShiftReal = 1;
  124. currentPhaseShiftImag = 0;
  125. for (var fftStep = 0; fftStep < halfSize; fftStep++) {
  126. var i = fftStep;
  127. while (i < bufferSize) {
  128. off = i + halfSize;
  129. tr = (currentPhaseShiftReal * real[off]) - (currentPhaseShiftImag * imag[off]);
  130. ti = (currentPhaseShiftReal * imag[off]) + (currentPhaseShiftImag * real[off]);
  131. real[off] = real[i] - tr;
  132. imag[off] = imag[i] - ti;
  133. real[i] += tr;
  134. imag[i] += ti;
  135. i += halfSize << 1;
  136. }
  137. tmpReal = currentPhaseShiftReal;
  138. currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) - (currentPhaseShiftImag * phaseShiftStepImag);
  139. currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) + (currentPhaseShiftImag * phaseShiftStepReal);
  140. }
  141. halfSize = halfSize << 1;
  142. }
  143. for (var i = 0, N = bufferSize / 2; i < N; i++) {
  144. rval = real[i];
  145. ival = imag[i];
  146. mag = bSi * sqrt(rval * rval + ival * ival);
  147. if (mag > this.peak) {
  148. this.peakBand = i;
  149. this.peak = mag;
  150. }
  151. spectrum[i] = mag;
  152. }
  153. return spectrum;
  154. };
  155. };
  156. /* eslint-enable complexity, no-redeclare, no-var, one-var */
  157. /**
  158. * @typedef {Object} SpectrogramPluginParams
  159. * @property {string|HTMLElement} container Selector of element or element in
  160. * which to render
  161. * @property {number} fftSamples=512 number of samples to fetch to FFT. Must be
  162. * a pwer of 2.
  163. * @property {number} noverlap Size of the overlapping window. Must be <
  164. * fftSamples. Auto deduced from canvas size by default.
  165. * @property {string} windowFunc='hann' The window function to be used. One of
  166. * these: `'bartlett'`, `'bartlettHann'`, `'blackman'`, `'cosine'`, `'gauss'`,
  167. * `'hamming'`, `'hann'`, `'lanczoz'`, `'rectangular'`, `'triangular'`
  168. * @property {?number} alpha Some window functions have this extra value.
  169. * (Between 0 and 1)
  170. * @property {number} pixelRatio=wavesurfer.params.pixelRatio to control the
  171. * size of the spectrogram in relation with its canvas. 1 = Draw on the whole
  172. * canvas. 2 = Draw on a quarter (1/2 the length and 1/2 the width)
  173. * @property {?boolean} deferInit Set to true to manually call
  174. * `initPlugin('spectrogram')`
  175. */
  176. /**
  177. * Render a spectrogram visualisation of the audio.
  178. *
  179. * @implements {PluginClass}
  180. * @extends {Observer}
  181. * @example
  182. * // es6
  183. * import SpectrogramPlugin from 'wavesurfer.spectrogram.js';
  184. *
  185. * // commonjs
  186. * var SpectrogramPlugin = require('wavesurfer.spectrogram.js');
  187. *
  188. * // if you are using <script> tags
  189. * var SpectrogramPlugin = window.WaveSurfer.spectrogram;
  190. *
  191. * // ... initialising wavesurfer with the plugin
  192. * var wavesurfer = WaveSurfer.create({
  193. * // wavesurfer options ...
  194. * plugins: [
  195. * SpectrogramPlugin.create({
  196. * // plugin options ...
  197. * })
  198. * ]
  199. * });
  200. */
  201. export default class SpectrogramPlugin {
  202. /**
  203. * Spectrogram plugin definition factory
  204. *
  205. * This function must be used to create a plugin definition which can be
  206. * used by wavesurfer to correctly instantiate the plugin.
  207. *
  208. * @param {SpectrogramPluginParams} params parameters use to initialise the plugin
  209. * @return {PluginDefinition} an object representing the plugin
  210. */
  211. static create(params) {
  212. return {
  213. name: 'spectrogram',
  214. deferInit: params && params.deferInit ? params.deferInit : false,
  215. params: params,
  216. staticProps: {
  217. FFT: FFT
  218. },
  219. instance: SpectrogramPlugin
  220. };
  221. }
  222. constructor(params, ws) {
  223. this.params = params;
  224. this.wavesurfer = ws;
  225. this.util = ws.util;
  226. this.frequenciesDataUrl = params.frequenciesDataUrl;
  227. this._onScroll = e => {
  228. this.updateScroll(e);
  229. };
  230. this._onReady = () => {
  231. const drawer = this.drawer = ws.drawer;
  232. this.container = 'string' == typeof params.container ?
  233. document.querySelector(params.container) : params.container;
  234. if (!this.container) {
  235. throw Error('No container for WaveSurfer spectrogram');
  236. }
  237. this.width = drawer.width;
  238. this.pixelRatio = this.params.pixelRatio || ws.params.pixelRatio;
  239. this.fftSamples = this.params.fftSamples || ws.params.fftSamples || 512;
  240. this.height = this.fftSamples / 2;
  241. this.noverlap = params.noverlap;
  242. this.windowFunc = params.windowFunc;
  243. this.alpha = params.alpha;
  244. this.createWrapper();
  245. this.createCanvas();
  246. this.render();
  247. drawer.wrapper.addEventListener('scroll', this._onScroll);
  248. ws.on('redraw', () => this.render());
  249. };
  250. }
  251. init() {
  252. // Check if ws is ready
  253. if (this.wavesurfer.isReady) {
  254. this._onReady();
  255. }
  256. this.wavesurfer.on('ready', this._onReady);
  257. }
  258. destroy() {
  259. this.unAll();
  260. this.wavesurfer.un('ready', this._onReady);
  261. this.drawer.wrapper.removeEventListener('scroll', this._onScroll);
  262. this.wavesurfer = null;
  263. this.util = null;
  264. this.params = null;
  265. if (this.wrapper) {
  266. this.wrapper.parentNode.removeChild(this.wrapper);
  267. this.wrapper = null;
  268. }
  269. }
  270. createWrapper() {
  271. const prevSpectrogram = this.container.querySelector('spectrogram');
  272. if (prevSpectrogram) {
  273. this.container.removeChild(prevSpectrogram);
  274. }
  275. const wsParams = this.wavesurfer.params;
  276. this.wrapper = document.createElement('spectrogram');
  277. // if labels are active
  278. if (this.params.labels) {
  279. const labelsEl = this.labelsEl = document.createElement('canvas');
  280. labelsEl.classList.add('spec-labels');
  281. this.drawer.style(labelsEl, {
  282. left: 0,
  283. position: 'absolute',
  284. zIndex: 9,
  285. height: `${this.height / this.pixelRatio}px`,
  286. width: `${55 / this.pixelRatio}px`
  287. });
  288. this.wrapper.appendChild(labelsEl);
  289. // can be customized in next version
  290. this.loadLabels('rgba(68,68,68,0.5)', '12px', '10px', '', '#fff', '#f7f7f7', 'center', '#specLabels');
  291. }
  292. this.drawer.style(this.wrapper, {
  293. display: 'block',
  294. position: 'relative',
  295. userSelect: 'none',
  296. webkitUserSelect: 'none',
  297. height: this.height + 'px'
  298. });
  299. if (wsParams.fillParent || wsParams.scrollParent) {
  300. this.drawer.style(this.wrapper, {
  301. width: '100%',
  302. overflowX: 'hidden',
  303. overflowY: 'hidden'
  304. });
  305. }
  306. this.container.appendChild(this.wrapper);
  307. this.wrapper.addEventListener('click', e => {
  308. e.preventDefault();
  309. const relX = 'offsetX' in e ? e.offsetX : e.layerX;
  310. this.fireEvent('click', (relX / this.scrollWidth) || 0);
  311. });
  312. }
  313. createCanvas() {
  314. const canvas = this.canvas = this.wrapper.appendChild(
  315. document.createElement('canvas')
  316. );
  317. this.spectrCc = canvas.getContext('2d');
  318. this.util.style(canvas, {
  319. position: 'absolute',
  320. zIndex: 4
  321. });
  322. }
  323. render() {
  324. this.updateCanvasStyle();
  325. if (this.frequenciesDataUrl) {
  326. this.loadFrequenciesData(this.frequenciesDataUrl);
  327. } else {
  328. this.getFrequencies(this.drawSpectrogram);
  329. }
  330. }
  331. updateCanvasStyle() {
  332. const width = Math.round(this.width / this.pixelRatio) + 'px';
  333. this.canvas.width = this.width;
  334. this.canvas.height = this.height;
  335. this.canvas.style.width = width;
  336. }
  337. drawSpectrogram(frequenciesData, my) {
  338. const spectrCc = my.spectrCc;
  339. const length = my.wavesurfer.backend.getDuration();
  340. const height = my.height;
  341. const pixels = my.resample(frequenciesData);
  342. const heightFactor = my.buffer ? 2 / my.buffer.numberOfChannels : 1;
  343. let i;
  344. let j;
  345. for (i = 0; i < pixels.length; i++) {
  346. for (j = 0; j < pixels[i].length; j++) {
  347. const colorValue = 255 - pixels[i][j];
  348. my.spectrCc.fillStyle = 'rgb(' + colorValue + ', ' + colorValue + ', ' + colorValue + ')';
  349. my.spectrCc.fillRect(i, height - j * heightFactor, 1, heightFactor);
  350. }
  351. }
  352. }
  353. getFrequencies(callback) {
  354. const fftSamples = this.fftSamples;
  355. const buffer = this.buffer = this.wavesurfer.backend.buffer;
  356. const channelOne = buffer.getChannelData(0);
  357. const bufferLength = buffer.length;
  358. const sampleRate = buffer.sampleRate;
  359. const frequencies = [];
  360. if (!buffer) {
  361. this.fireEvent('error', 'Web Audio buffer is not available');
  362. return;
  363. }
  364. let noverlap = this.noverlap;
  365. if (!noverlap) {
  366. const uniqueSamplesPerPx = buffer.length / this.canvas.width;
  367. noverlap = Math.max(0, Math.round(fftSamples - uniqueSamplesPerPx));
  368. }
  369. const fft = new FFT(fftSamples, sampleRate, this.windowFunc, this.alpha);
  370. const maxSlicesCount = Math.floor(bufferLength / (fftSamples - noverlap));
  371. let currentOffset = 0;
  372. while (currentOffset + fftSamples < channelOne.length) {
  373. const segment = channelOne.slice(currentOffset, currentOffset + fftSamples);
  374. const spectrum = fft.calculateSpectrum(segment);
  375. const array = new Uint8Array(fftSamples / 2);
  376. let j;
  377. for (j = 0; j < fftSamples / 2; j++) {
  378. array[j] = Math.max(-255, Math.log10(spectrum[j]) * 45);
  379. }
  380. frequencies.push(array);
  381. currentOffset += (fftSamples - noverlap);
  382. }
  383. callback(frequencies, this);
  384. }
  385. loadFrequenciesData(url) {
  386. const ajax = this.util.ajax({ url: url });
  387. ajax.on('success', data => this.drawSpectrogram(JSON.parse(data), this));
  388. ajax.on('error', e => this.fireEvent('error', 'XHR error: ' + e.target.statusText));
  389. return ajax;
  390. }
  391. freqType(freq) {
  392. return (freq >= 1000 ? (freq / 1000).toFixed(1) : Math.round(freq));
  393. }
  394. unitType(freq) {
  395. return (freq >= 1000 ? 'KHz' : 'Hz');
  396. }
  397. loadLabels(bgFill, fontSizeFreq, fontSizeUnit, fontType, textColorFreq, textColorUnit, textAlign, container) {
  398. const frequenciesHeight = this.height;
  399. bgFill = bgFill || 'rgba(68,68,68,0)';
  400. fontSizeFreq = fontSizeFreq || '12px';
  401. fontSizeUnit = fontSizeUnit || '10px';
  402. fontType = fontType || 'Helvetica';
  403. textColorFreq = textColorFreq || '#fff';
  404. textColorUnit = textColorUnit || '#fff';
  405. textAlign = textAlign || 'center';
  406. container = container || '#specLabels';
  407. const getMaxY = frequenciesHeight || 512;
  408. const labelIndex = 5 * (getMaxY / 256);
  409. const freqStart = 0;
  410. const step = ((this.wavesurfer.backend.ac.sampleRate / 2) - freqStart) / labelIndex;
  411. const ctx = this.labelsEl.getContext('2d');
  412. this.labelsEl.height = this.height;
  413. this.labelsEl.width = 55;
  414. ctx.fillStyle = bgFill;
  415. ctx.fillRect(0, 0, 55, getMaxY);
  416. ctx.fill();
  417. let i;
  418. for (i = 0; i <= labelIndex; i++) {
  419. ctx.textAlign = textAlign;
  420. ctx.textBaseline = 'middle';
  421. const freq = freqStart + (step * i);
  422. const index = Math.round(freq / (this.sampleRate / 2) * this.fftSamples);
  423. const label = this.freqType(freq);
  424. const units = this.unitType(freq);
  425. const x = 16;
  426. const yLabelOffset = 2;
  427. if (i == 0) {
  428. ctx.fillStyle = textColorUnit;
  429. ctx.font = fontSizeUnit + ' ' + fontType;
  430. ctx.fillText(units, x + 24, getMaxY + i - 10);
  431. ctx.fillStyle = textColorFreq;
  432. ctx.font = fontSizeFreq + ' ' + fontType;
  433. ctx.fillText(label, x, getMaxY + i - 10);
  434. } else {
  435. ctx.fillStyle = textColorUnit;
  436. ctx.font = fontSizeUnit + ' ' + fontType;
  437. ctx.fillText(units, x + 24, getMaxY - i * 50 + yLabelOffset);
  438. ctx.fillStyle = textColorFreq;
  439. ctx.font = fontSizeFreq + ' ' + fontType;
  440. ctx.fillText(label, x, getMaxY - i * 50 + yLabelOffset);
  441. }
  442. }
  443. }
  444. updateScroll(e) {
  445. if (this.wrapper) {
  446. this.wrapper.scrollLeft = e.target.scrollLeft;
  447. }
  448. }
  449. resample(oldMatrix) {
  450. const columnsNumber = this.width;
  451. const newMatrix = [];
  452. const oldPiece = 1 / oldMatrix.length;
  453. const newPiece = 1 / columnsNumber;
  454. let i;
  455. for (i = 0; i < columnsNumber; i++) {
  456. const column = new Array(oldMatrix[0].length);
  457. let j;
  458. for (j = 0; j < oldMatrix.length; j++) {
  459. const oldStart = j * oldPiece;
  460. const oldEnd = oldStart + oldPiece;
  461. const newStart = i * newPiece;
  462. const newEnd = newStart + newPiece;
  463. const overlap = (oldEnd <= newStart || newEnd <= oldStart) ?
  464. 0 :
  465. Math.min(Math.max(oldEnd, newStart), Math.max(newEnd, oldStart)) -
  466. Math.max(Math.min(oldEnd, newStart), Math.min(newEnd, oldStart));
  467. let k;
  468. /* eslint-disable max-depth */
  469. if (overlap > 0) {
  470. for (k = 0; k < oldMatrix[0].length; k++) {
  471. if (column[k] == null) {
  472. column[k] = 0;
  473. }
  474. column[k] += (overlap / newPiece) * oldMatrix[j][k];
  475. }
  476. }
  477. /* eslint-enable max-depth */
  478. }
  479. const intColumn = new Uint8Array(oldMatrix[0].length);
  480. let m;
  481. for (m = 0; m < oldMatrix[0].length; m++) {
  482. intColumn[m] = column[m];
  483. }
  484. newMatrix.push(intColumn);
  485. }
  486. return newMatrix;
  487. }
  488. }