meyda
Version:
Real-time feature extraction for the web audio api
301 lines (291 loc) • 10.9 kB
JavaScript
;
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
function blackman(size) {
var blackmanBuffer = new Float32Array(size);
var coeff1 = (2 * Math.PI) / (size - 1);
var coeff2 = 2 * coeff1;
//According to http://uk.mathworks.com/help/signal/ref/blackman.html
//first half of the window
for (var i = 0; i < size / 2; i++) {
blackmanBuffer[i] =
0.42 - 0.5 * Math.cos(i * coeff1) + 0.08 * Math.cos(i * coeff2);
}
//second half of the window
for (var i = Math.ceil(size / 2); i > 0; i--) {
blackmanBuffer[size - i] = blackmanBuffer[i - 1];
}
return blackmanBuffer;
}
function sine(size) {
var coeff = Math.PI / (size - 1);
var sineBuffer = new Float32Array(size);
for (var i = 0; i < size; i++) {
sineBuffer[i] = Math.sin(coeff * i);
}
return sineBuffer;
}
function hanning(size) {
var hanningBuffer = new Float32Array(size);
for (var i = 0; i < size; i++) {
// According to the R documentation
// http://ugrad.stat.ubc.ca/R/library/e1071/html/hanning.window.html
hanningBuffer[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (size - 1));
}
return hanningBuffer;
}
function hamming(size) {
var hammingBuffer = new Float32Array(size);
for (var i = 0; i < size; i++) {
//According to http://uk.mathworks.com/help/signal/ref/hamming.html
hammingBuffer[i] = 0.54 - 0.46 * Math.cos(2 * Math.PI * (i / size - 1));
}
return hammingBuffer;
}
var windowing = /*#__PURE__*/Object.freeze({
__proto__: null,
blackman: blackman,
hamming: hamming,
hanning: hanning,
sine: sine
});
var windows = {};
function isPowerOfTwo(num) {
while (num % 2 === 0 && num > 1) {
num /= 2;
}
return num === 1;
}
function error(message) {
throw new Error("Meyda: " + message);
}
function pointwiseBufferMult(a, b) {
var c = [];
for (var i = 0; i < Math.min(a.length, b.length); i++) {
c[i] = a[i] * b[i];
}
return c;
}
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;
}
function createBarkScale(length, sampleRate, bufferSize) {
var 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;
}
function typedToArray(t) {
// utility to convert typed arrays to normal arrays
return Array.prototype.slice.call(t);
}
function arrayToTyped(t) {
// utility to convert arrays to typed F32 arrays
return Float32Array.from(t);
}
function _normalize(num, range) {
return num / range;
}
function normalize(a, range) {
return a.map(function (n) {
return _normalize(n, range);
});
}
function normalizeToOne(a) {
var max = Math.max.apply(null, a);
return a.map(function (n) {
return n / max;
});
}
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;
}
function melToFreq(mV) {
return _melToFreq(mV);
}
function freqToMel(fV) {
return _freqToMel(fV);
}
function createMelFilterBank(numFilters, sampleRate, bufferSize) {
//the +2 is the upper and lower limits
var melValues = new Float32Array(numFilters + 2);
var melValuesInFreq = new Float32Array(numFilters + 2);
//Generate limits in Hz - from 0 to the nyquist.
var lowerLimitFreq = 0;
var upperLimitFreq = sampleRate / 2;
//Convert the limits to Mel
var lowerLimitMel = _freqToMel(lowerLimitFreq);
var upperLimitMel = _freqToMel(upperLimitFreq);
//Find the range
var range = upperLimitMel - lowerLimitMel;
//Find the range as part of the linear interpolation
var valueToAdd = range / (numFilters + 1);
var fftBinsOfFreq = new Array(numFilters + 2);
for (var 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 = new Array(numFilters);
for (var 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 (var i = fftBinsOfFreq[j]; i < fftBinsOfFreq[j + 1]; i++) {
filterBank[j][i] =
(i - fftBinsOfFreq[j]) / (fftBinsOfFreq[j + 1] - fftBinsOfFreq[j]);
}
for (var i = fftBinsOfFreq[j + 1]; i < fftBinsOfFreq[j + 2]; i++) {
filterBank[j][i] =
(fftBinsOfFreq[j + 2] - i) /
(fftBinsOfFreq[j + 2] - fftBinsOfFreq[j + 1]);
}
}
return filterBank;
}
function hzToOctaves(freq, A440) {
return Math.log2((16 * freq) / A440);
}
function normalizeByColumn(a) {
var emptyRow = a[0].map(function () { return 0; });
var colDenominators = a
.reduce(function (acc, row) {
row.forEach(function (cell, j) {
acc[j] += Math.pow(cell, 2);
});
return acc;
}, emptyRow)
.map(Math.sqrt);
return a.map(function (row, i) { return row.map(function (v, j) { return v / (colDenominators[j] || 1); }); });
}
function createChromaFilterBank(numFilters, sampleRate, bufferSize, centerOctave, octaveWidth, baseC, A440) {
if (centerOctave === void 0) { centerOctave = 5; }
if (octaveWidth === void 0) { octaveWidth = 2; }
if (baseC === void 0) { baseC = true; }
if (A440 === void 0) { A440 = 440; }
var numOutputBins = Math.floor(bufferSize / 2) + 1;
var frequencyBins = new Array(bufferSize)
.fill(0)
.map(function (_, i) { return 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(function (v, i) { return Math.max(v - frequencyBins[i]); }, 1)
.concat([1]);
var halfNumFilters = Math.round(numFilters / 2);
var filterPeaks = new Array(numFilters)
.fill(0)
.map(function (_, i) {
return frequencyBins.map(function (frq) {
return ((10 * numFilters + halfNumFilters + frq - i) % numFilters) -
halfNumFilters;
});
});
var weights = filterPeaks.map(function (row, i) {
return row.map(function (_, j) {
return Math.exp(-0.5 * Math.pow((2 * filterPeaks[i][j]) / binWidthBins[j], 2));
});
});
weights = normalizeByColumn(weights);
if (octaveWidth) {
var octaveWeights = frequencyBins.map(function (v) {
return Math.exp(-0.5 * Math.pow((v / numFilters - centerOctave) / octaveWidth, 2));
});
weights = weights.map(function (row) {
return row.map(function (cell, j) { return cell * octaveWeights[j]; });
});
}
if (baseC) {
weights = __spreadArray(__spreadArray([], weights.slice(3), true), weights.slice(0, 3), true);
}
return weights.map(function (row) { return row.slice(0, numOutputBins); });
}
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");
}
var numFrames = 1 + Math.floor((buffer.length - frameLength) / hopLength);
return new Array(numFrames)
.fill(0)
.map(function (_, i) { return buffer.slice(i * hopLength, i * hopLength + frameLength); });
}
exports._normalize = _normalize;
exports.applyWindow = applyWindow;
exports.arrayToTyped = arrayToTyped;
exports.createBarkScale = createBarkScale;
exports.createChromaFilterBank = createChromaFilterBank;
exports.createMelFilterBank = createMelFilterBank;
exports.error = error;
exports.frame = frame;
exports.freqToMel = freqToMel;
exports.hzToOctaves = hzToOctaves;
exports.isPowerOfTwo = isPowerOfTwo;
exports.mean = mean;
exports.melToFreq = melToFreq;
exports.normalize = normalize;
exports.normalizeByColumn = normalizeByColumn;
exports.normalizeToOne = normalizeToOne;
exports.pointwiseBufferMult = pointwiseBufferMult;
exports.typedToArray = typedToArray;