meyda
Version:
Real-time feature extraction for the web audio api
205 lines (204 loc) • 7.6 kB
JavaScript
import { __spreadArray } from "tslib";
import * as windowing from "./windowing";
var 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) {
var c = [];
for (var 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) {
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;
}
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, 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;
}
export function hzToOctaves(freq, A440) {
return Math.log2((16 * freq) / A440);
}
export 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); }); });
}
export 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); });
}
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");
}
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); });
}