src/OnsetDetection.js | API Document

# src/OnsetDetection.js

    /**
     * Spectral flux calculating and peaks finding
     * @class
     */
    export default class OnsetDetection {
        /**
         * Get spectral flux
         * @param {Float32Array} audioData - non-interleaved IEEE 32-bit linear PCM with a nominal range of -1 -> +1 (Web Audio API - Audio Buffer)
         * @param {Object} fft - object with methods for performing FFT
         * @param {Object} [params={}] - parameters
         * @param {Number} [params.bufferSize=2048] - FFT windows size
         * @param {Number} [params.hopSize=441] - spacing of audio frames in samples
         * @return {Array} spectralFlux - the array of spectral flux values
         */
        static calculateSF(audioData, fft, params = {}) {
            if (typeof fft == "undefined") {
                throw new ReferenceError("fft is undefined");
            }
            if (typeof fft.getHammingWindow !== "function" || typeof fft.getSpectrum !== "function") {
                throw new ReferenceError("fft doesn't contain getHammingWindow or getSpectrum methods");
            }
            // Array.fill polyfill
            if (!Array.prototype.fill) {
              Array.prototype.fill = function(value) {
                if (this == null) {
                  throw new TypeError('this is null or not defined');
                }
                var O = Object(this);
                var len = O.length >>> 0;
                var start = arguments[1];
                var relativeStart = start >> 0;
                 var k = relativeStart < 0 ?
                  Math.max(len + relativeStart, 0) :
                  Math.min(relativeStart, len);
                var end = arguments[2];
                var relativeEnd = end === undefined ?
                  len : end >> 0;
                var final = relativeEnd < 0 ?
                  Math.max(len + relativeEnd, 0) :
                  Math.min(relativeEnd, len);
                while (k < final) {
                  O[k] = value;
                  k++;
                }
                return O;
              };
            }
            params.bufferSize = params.bufferSize || 2048;
            //params.samplingRate = params.samplingRate || 44100;
            params.hopSize = params.hopSize || 441;

            const {bufferSize, hopSize} = params;

            let k = Math.floor(Math.log(bufferSize) / Math.LN2);
            if (Math.pow(2, k) !== bufferSize) {
                throw "Invalid buffer size (" + bufferSize + "), must be power of 2";
            }

            const hammWindow = fft.getHammingWindow(bufferSize);
            let spectralFlux = [];
            let spectrumLength = bufferSize / 2 + 1;
            let previousSpectrum = new Array(spectrumLength);
            previousSpectrum.fill(0);
            let im = new Array(bufferSize);

            let length = audioData.length;
            let zerosStart = new Array(bufferSize - hopSize);
            zerosStart.fill(0);
            audioData = zerosStart.concat(audioData);

            let zerosEnd = new Array(bufferSize - (audioData.length % hopSize));
            zerosEnd.fill(0);
            audioData = audioData.concat(zerosEnd);

            for (let wndStart = 0; wndStart < length; wndStart += hopSize) {
                let wndEnd = wndStart + bufferSize;

                let re = [];
                let k = 0;
                for (let i = wndStart; i < wndEnd; i++) {
                    re[k] = hammWindow[k] * audioData[i];
                    k++;
                }
                im.fill(0);

                fft.getSpectrum(re, im);

                let flux = 0;
                for(let j = 0; j < spectrumLength; j++) {
                    let value = re[j] - previousSpectrum[j];
                    flux += value < 0 ? 0 : value;
                }
                spectralFlux.push(flux);

                previousSpectrum = re;
            }

            return spectralFlux;
        }
        /**
         * Normalize data to have a mean of 0 and standard deviation of 1
         * @param {Array} data - data array
         */
        static normalize(data) {
            if (!Array.isArray(data)) {
                throw "Array expected";
            }
            if (data.length == 0) {
                throw "Array is empty";
            }
            let sum = 0;
            let squareSum = 0;
            for (let i = 0; i < data.length; i++) {
                sum += data[i];
                squareSum += data[i] * data[i];
            }
            let mean = sum / data.length;
            let standardDeviation = Math.sqrt( (squareSum - sum * mean) / data.length );
            if (standardDeviation == 0)
                standardDeviation = 1;
            for (let i = 0; i < data.length; i++) {
                data[i] = (data[i] - mean) / standardDeviation;
            }
        }
        /**
         * Finding local maxima in an array
         * @param {Array} spectralFlux - input data
         * @param {Object} [params={}] - parametrs
         * @param {Number} [params.decayRate=0.84] - how quickly previous peaks are forgotten
         * @param {Number} [params.peakFindingWindow=6] - minimum distance between peaks
         * @param {Number} [params.meanWndMultiplier=3] - multiplier for peak finding window
         * @param {Number} [params.peakThreshold=0.35] - minimum value of peaks
         * @return {Array} peaks - array of peak indexes
         */
        static findPeaks(spectralFlux, params = {}) {
            const length = spectralFlux.length;
            const sf = spectralFlux;
            const decayRate = params.decayRate || 0.84;
            const peakFindingWindow = params.peakFindingWindow || 6;
            const meanWndMultiplier = params.meanWndMultiplier || 3;
            const peakThreshold = params.peakThreshold || 0.35;

            let max = 0;
            let av = sf[0];
            let peaks = [];

            for (let i = 0; i < length; i++) {
                av = decayRate * av + (1 - decayRate) * sf[i];
                if (sf[i] < av) continue;

                let wndStart = i - peakFindingWindow;
                let wndEnd = i + peakFindingWindow + 1;

                if (wndStart < 0) wndStart = 0;
                if (wndEnd > length) wndEnd = length;
                if (av < sf[i]) av = sf[i];

                let isMax = true;
                for (let j = wndStart; j < wndEnd; j++) {
                    if (sf[j] > sf[i]) isMax = false;
                }
                if (isMax) {
                    let meanWndStart = i - peakFindingWindow * meanWndMultiplier;
                    let meanWndEnd = i + peakFindingWindow;
                    if (meanWndStart < 0) meanWndStart = 0;
                    if (meanWndEnd > length) meanWndEnd = length;
                    let sum = 0;
                    let count = meanWndEnd - meanWndStart;
                    for (let j = meanWndStart; j < meanWndEnd; j++) {
                        sum += sf[j];
                    }
                    if (sf[i] > sum / count + peakThreshold) {
                        peaks.push(i);
                    }
                }
            }

            if (peaks.length < 2) {
                throw "Fail to find peaks";
            }
            return peaks;
        }
    }