UNPKG

meyda

Version:

Real-time feature extraction for the web audio api

273 lines (219 loc) 6.81 kB
import * as windowing from "./windowing"; let windows = {}; export function isPowerOfTwo(num) { while (num % 2 === 0 && num > 1) { num /= 2; } return num === 1; } export function error(message) { throw new Error("Meyda: " + message); } export function pointwiseBufferMult(a, b) { let c: number[] = []; for (let i = 0; i < Math.min(a.length, b.length); i++) { c[i] = a[i] * b[i]; } return c; } export function applyWindow(signal, windowname) { if (windowname !== "rect") { if (windowname === "" || !windowname) windowname = "hanning"; if (!windows[windowname]) windows[windowname] = {}; if (!windows[windowname][signal.length]) { try { windows[windowname][signal.length] = windowing[windowname]( signal.length ); } catch (e) { throw new Error("Invalid windowing function"); } } signal = pointwiseBufferMult(signal, windows[windowname][signal.length]); } return signal; } export function createBarkScale(length, sampleRate, bufferSize): Float32Array { let barkScale = new Float32Array(length); for (var i = 0; i < barkScale.length; i++) { barkScale[i] = (i * sampleRate) / bufferSize; barkScale[i] = 13 * Math.atan(barkScale[i] / 1315.8) + 3.5 * Math.atan(Math.pow(barkScale[i] / 7518, 2)); } return barkScale; } export function typedToArray(t) { // utility to convert typed arrays to normal arrays return Array.prototype.slice.call(t); } export function arrayToTyped(t) { // utility to convert arrays to typed F32 arrays return Float32Array.from(t); } export function _normalize(num, range) { return num / range; } export function normalize(a, range) { return a.map(function (n) { return _normalize(n, range); }); } export function normalizeToOne(a) { var max = Math.max.apply(null, a); return a.map(function (n) { return n / max; }); } export function mean(a) { return ( a.reduce(function (prev, cur) { return prev + cur; }) / a.length ); } function _melToFreq(melValue) { var freqValue = 700 * (Math.exp(melValue / 1125) - 1); return freqValue; } function _freqToMel(freqValue) { var melValue = 1125 * Math.log(1 + freqValue / 700); return melValue; } export function melToFreq(mV) { return _melToFreq(mV); } export function freqToMel(fV) { return _freqToMel(fV); } export function createMelFilterBank( numFilters: number, sampleRate: number, bufferSize: number ): number[][] { //the +2 is the upper and lower limits let melValues = new Float32Array(numFilters + 2); let melValuesInFreq = new Float32Array(numFilters + 2); //Generate limits in Hz - from 0 to the nyquist. let lowerLimitFreq = 0; let upperLimitFreq = sampleRate / 2; //Convert the limits to Mel let lowerLimitMel = _freqToMel(lowerLimitFreq); let upperLimitMel = _freqToMel(upperLimitFreq); //Find the range let range = upperLimitMel - lowerLimitMel; //Find the range as part of the linear interpolation let valueToAdd = range / (numFilters + 1); let fftBinsOfFreq: number[] = new Array(numFilters + 2); for (let i = 0; i < melValues.length; i++) { // Initialising the mel frequencies // They're a linear interpolation between the lower and upper limits. melValues[i] = i * valueToAdd; // Convert back to Hz melValuesInFreq[i] = _melToFreq(melValues[i]); // Find the corresponding bins fftBinsOfFreq[i] = Math.floor( ((bufferSize + 1) * melValuesInFreq[i]) / sampleRate ); } var filterBank: number[][] = new Array(numFilters); for (let j = 0; j < filterBank.length; j++) { // Create a two dimensional array of size numFilters * (buffersize/2)+1 // pre-populating the arrays with 0s. filterBank[j] = new Array(bufferSize / 2 + 1).fill(0); //creating the lower and upper slopes for each bin for (let i = fftBinsOfFreq[j]; i < fftBinsOfFreq[j + 1]; i++) { filterBank[j][i] = (i - fftBinsOfFreq[j]) / (fftBinsOfFreq[j + 1] - fftBinsOfFreq[j]); } for (let i = fftBinsOfFreq[j + 1]; i < fftBinsOfFreq[j + 2]; i++) { filterBank[j][i] = (fftBinsOfFreq[j + 2] - i) / (fftBinsOfFreq[j + 2] - fftBinsOfFreq[j + 1]); } } return filterBank; } export function hzToOctaves(freq, A440) { return Math.log2((16 * freq) / A440); } export function normalizeByColumn(a) { var emptyRow = a[0].map(() => 0); var colDenominators = a .reduce((acc, row) => { row.forEach((cell, j) => { acc[j] += Math.pow(cell, 2); }); return acc; }, emptyRow) .map(Math.sqrt); return a.map((row, i) => row.map((v, j) => v / (colDenominators[j] || 1))); } export function createChromaFilterBank( numFilters, sampleRate, bufferSize, centerOctave = 5, octaveWidth = 2, baseC = true, A440 = 440 ) { var numOutputBins = Math.floor(bufferSize / 2) + 1; var frequencyBins = new Array(bufferSize) .fill(0) .map( (_, i) => numFilters * hzToOctaves((sampleRate * i) / bufferSize, A440) ); // Set a value for the 0 Hz bin that is 1.5 octaves below bin 1 // (so chroma is 50% rotated from bin 1, and bin width is broad) frequencyBins[0] = frequencyBins[1] - 1.5 * numFilters; var binWidthBins = frequencyBins .slice(1) .map((v, i) => Math.max(v - frequencyBins[i]), 1) .concat([1]); var halfNumFilters = Math.round(numFilters / 2); var filterPeaks = new Array(numFilters) .fill(0) .map((_, i) => frequencyBins.map( (frq) => ((10 * numFilters + halfNumFilters + frq - i) % numFilters) - halfNumFilters ) ); var weights = filterPeaks.map((row, i) => row.map((_, j) => Math.exp(-0.5 * Math.pow((2 * filterPeaks[i][j]) / binWidthBins[j], 2)) ) ); weights = normalizeByColumn(weights); if (octaveWidth) { var octaveWeights = frequencyBins.map((v) => Math.exp( -0.5 * Math.pow((v / numFilters - centerOctave) / octaveWidth, 2) ) ); weights = weights.map((row) => row.map((cell, j) => cell * octaveWeights[j]) ); } if (baseC) { weights = [...weights.slice(3), ...weights.slice(0, 3)]; } return weights.map((row) => row.slice(0, numOutputBins)); } export function frame(buffer, frameLength, hopLength) { if (buffer.length < frameLength) { throw new Error("Buffer is too short for frame length"); } if (hopLength < 1) { throw new Error("Hop length cannot be less that 1"); } if (frameLength < 1) { throw new Error("Frame length cannot be less that 1"); } const numFrames = 1 + Math.floor((buffer.length - frameLength) / hopLength); return new Array(numFrames) .fill(0) .map((_, i) => buffer.slice(i * hopLength, i * hopLength + frameLength)); }