UNPKG

@creenv/audio

Version:

load audio from different sources (file, microphone, soundcloud...), audio analysis (beat detection, frequency...)

323 lines (283 loc) 14.1 kB
/** * @license MIT * @author Baptiste Crespt <baptiste.crespy@gmail.com> * * This class provides a flexible analysis of an audio stream. Because we want to save as much computational power as we can, * this algorithm is fully customizable so that only parts required in the options are computed and returned * * * References * * BeatDetection algorithm * @author gamdev.net <http://gamedev.net/> * <http://archive.gamedev.net/archive/reference/programming/features/beatdetection/> * * Beat Detection Using JavaScript and the Web Audio API * @author Joe Sullivan <https://twitter.com/itsjoesullivan> * <http://joesul.li/van/beat-detection-using-web-audio/> * * _______ * * The beat detection algorithm implemented in this analyser is slightly different from the ones described in the paper, * however the papers offer a solid basis if you're interested in beat detection algorithms. */ /*import AppConfig from '../config/app.config'; import AnalyserConfig from '../config/analyser.config'; import { AudioData } from '../audiostream/audio-data'; import { AudioAnalysedData, AudioAnalysedDataForVisualization, Peak } from './audio-analysed-data'; import { EASINGS } from '../utility/easings';*/ import DEFAULT_OPTIONS from "./options"; import EASINGS from "@creenv/easings"; import AudioStream from "./stream"; import AudioAnalysedData from "./audio-analysed-data"; import Peak from "./peak"; import deepmerge from "deepmerge"; class AudioAnalyser { /** * creates an analyser to compute and get the analysed data such as peak deteciton, signal energy, energy average * * @param {AudioStream} stream the stream, created from a source, that will be analysed * @param {object} options options for the peak detection algorithms * @param {boolean} mergeOptions if set to true, a deepmerge will be performed between the options and the default options * if set to false, the @param options will be used directly */ constructor (bufferSize, options = {}, mergeOptions = true) { // object deep merge if (mergeOptions) this.options = deepmerge(DEFAULT_OPTIONS, options); else this.options = options; this.checkOptions(this.options); this.bufferSize = bufferSize; this.data = new AudioAnalysedData(this.bufferSize, this.options.multibandPeakDetection.options.bands); this.iterations = 0; } /** * Analyse the data provided by the AudioStream * * @param {AudioData} audioData Data provided by the AudioStream.getAudioData() * @param {number} deltaTime time elapsed since last aked analysis * @param {number} currentTimer absolute timer used to store the data by timers * * @return {AudioAnalysedData} data processed by the analyser */ analyse (audioData, deltaTime, currentTimer) { /** * This method looks horrible but the logical tests are useful to save CPU usage * if all the analysis are not required by the visualizer */ this.iterations++; this.data.setTimedomainData(audioData.timedomainData); this.data.setFrequenciesData(audioData.frequencyData); // Peak detection let returns = this.options.returns; if (returns.energy || returns.energyAverage || returns.energyHistory || returns.peak || returns.peakHistory) { // we first compute the energy and push it to the energy history this.data.pushNewEnergy(this.computeEnergy(this.data.getTimedomainData()), deltaTime, this.options.peakDetection.options.energyPersistence); if (returns.energyAverage || returns.peak || returns.peakHistory) { this.data.setEnergyAverage(this.computeLocalEnergyAverage(this.data.getEnergyHistory())); if (returns.peak || returns.peakHistory) { this.computePeakDetection(this.data.getEnergy(), this.data.getEnergyAverage(), this.data.peak, this.data.peakHistory, currentTimer, this.options.peakDetection.options.threshold, this.options.peakDetection.options.peakPersistency, this.options.peakDetection.options.ignoreTime, EASINGS.linear); } } } if (returns.multibandEnergy || returns.multibandEnergyAverage || returns.multibandEnergyHistory || returns.multibandPeak || returns.multibandPeakHistory) { this.data.pushNewMultibandEnergy(this.computeMultibandEnergy(audioData.frequencyData, this.options.multibandPeakDetection.options.bands), deltaTime, this.options.multibandPeakDetection.options.energyPersistence); if (returns.multibandEnergyAverage || returns.multibandPeak || returns.multibandPeakHistory) { this.data.setMultibandEnergyAverage(this.computeMultibandLocalEnergyAverage(this.data.getMultibandEnergyHistory())); if (returns.multibandPeakHistory || returns.multibandPeak) { this.computeMultibandPeakDetection(this.data.getMultibandEnergy(), this.data.getMultibandEnergyAverage(), this.data.multibandPeak, this.data.multibandPeakHistory, currentTimer, this.options.multibandPeakDetection.options.threshold, this.options.multibandPeakDetection.options.peakPersistency, this.options.multibandPeakDetection.options.ignoreTime, EASINGS.linear); } } } return this.data; } /** * This function check if the config is set correctly. It only shows an error if needed. * @param {object} options the object that needs to be checked */ checkOptions (options) { // we first check if some values are correct if (options.multibandPeakDetection.enabled) { if( !(options.multibandPeakDetection.options.bands && (options.multibandPeakDetection.options.bands & (options.multibandPeakDetection.options.bands - 1)) === 0) ) console.error(`The number of bands for the multiband detection algorithm must be a pow of 2.`); } if (options.returns.peakHistory != options.returns.multibandPeakHistory) console.error(`for optimisations reasons, the peak history and multiband peak history must have the same value`); // we check if the return values can be returned if (!options.multibandPeakDetection.enabled) { if (options.returns.multibandPeak) console.error(`The multiband peak can't be computed if the multiband peak detection algorithm is disabled.`); if (options.returns.multibandPeakHistory) console.error(`The multiband peak history can't be computed if the multiband peak detection algorithm is disabled.`); } if (!options.peakDetection.enabled) { if (options.returns.peak) console.error(`The peak can't be computed if the peak detection algorithm is disabled.`); if (options.returns.peakHistory) console.error(`The peak history can't be computed if the peak detection algorithm is disabled.`); } } /** * @return {AudioAnalysedData} Full analysed data */ getAnalysedData () { return this.data; } /** * Computes the energy of a signal * * @param {Uint8Array} timedomainData The timedomain data of the signal * @return {number} The energy of the timedomain data */ computeEnergy (timedomainData) { let energy = 0; for( let i = 0; i < timedomainData.length; i++ ) energy+= Math.abs(timedomainData[i] - 128); return energy/timedomainData.length; } /** * Computes the local average of the energy history * * @param {Array} energyHistory * * @return {number} the average energy of the history */ computeLocalEnergyAverage (energyHistory) { return energyHistory.reduce((a,b) => a+b, 0) / energyHistory.length; } /** * Uses the @param peak to check if a peak has been detected recently and updates its values, if not it checks * if a peak is detected * * @param {number} energy energy of the moment * @param {number} energyAverage average of the last energies * @param {Peak} peak Informations on the peak * @param {Array} peakHistory History of the recorded peaks * @param {*} currentTimer the absolute timer on the current loop * @param {number} threshold the higher this value is, the harder the peak has to hit to be detected * @param {number} peakPersistency the time it takes for the peak valeu to go from 1 to 0 * @param {number} ignoreTime time when a peak can't be detected after a detection * @param {*} interpolationFunction [0;1] => [0;1] */ computePeakDetection (energy, energyAverage, peak, peakHistory, currentTimer, threshold, peakPersistency, ignoreTime, interpolationFunction) { // if a peak has already been detected if (peak.timer != null) { // if a peak has been detected recently, we can't detect a new peak during ignoreTime // we only decrease the value of the peak if (currentTimer - peak.timer <= ignoreTime) { // we decrease the value of the peak if it's not 0 if (peak.value > 0.0) { peak.value = this.peakInterpolation(currentTimer, peak.timer, peakPersistency, interpolationFunction); } } else { // we try a peak detection if (energy / energyAverage > threshold) { // we have a peak let detectedPeak = new Peak(1.0, currentTimer, energy); if( this.options.returns.peakHistory ) peakHistory.push( detectedPeak ); peak.copy( detectedPeak ); } else if (peak.value > 0.0) { // if the detected peak is still not to 0 we decrease its value peak.value = this.peakInterpolation(currentTimer, peak.timer, peakPersistency, interpolationFunction); } } } else { // we try a peak detection if (energy / energyAverage > threshold) { // we have a peak let detectedPeak = new Peak(1.0, currentTimer, energy); if (this.options.returns.peakHistory) peakHistory.push(detectedPeak); peak.copy(detectedPeak); } } } /** * @param {Uint8Array} frequencyData the frequencies data * @param {number} nbBands Number of bands * * @return {Array} the array energy of each band * @private */ computeMultibandEnergy (frequencyData, nbBands) { let fSize = frequencyData.length, bandsEnergy = new Array(nbBands); // we parse each band for (let band = 0; band < nbBands; band++) { let firstIndex = this.bandInterpolation(band / nbBands) * fSize, lastIndex = this.bandInterpolation((band+1) / nbBands) * fSize, bandEnergy = 0; // for each band we parse the frequencies for (let f = firstIndex; f < lastIndex; f++) bandEnergy+= frequencyData[f]; bandsEnergy[band] = bandEnergy/(lastIndex-firstIndex); } return bandsEnergy; } /** * @param {Array} energiesHistory The history of each band energy * * @return {Array.<number>} the array of each band energy average */ computeMultibandLocalEnergyAverage (energiesHistory) { let energiesAverage = new Array(this.options.multibandPeakDetection.options.bands); // we init the values for (let i = 0; i < this.options.multibandPeakDetection.options.bands; i++) energiesAverage[i] = 0; // first we go through the history for (let i = 0; i < energiesHistory.length; i++) { // then we go though each band for (let b = 0; b < energiesHistory[i].length; b++) { energiesAverage[b]+= energiesHistory[i][b]; } } for (let i = 0; i < this.options.multibandPeakDetection.options.bands; i++) energiesAverage[i]/= energiesHistory.length; return energiesAverage; } /** * parse each band to see if there is a local peak on each one of them. Uses the computePeakDetection method for such a purpose. * * @param {Array} multibandEnergy array of the computed energy of each band * @param {Array} multibandEnergyAverage array of the computed average local energy of each band * @param {Array} multibandPeak array of each band peak * @param {Array} multibandPeakHistory array of each band peaks history * @param {*} currentTimer absolute timer on the current frame * @param {number} threshold the higher it is, the harder a peak has to hit to be detected * @param {number} peakPersistency time it takes for a peak to go from 1 to 0 * @param {number} ignoreTime time during a peak can't be detected after a detection * @param {*} interpolationFunction [0; 1] => [0; 1] decrease function of the peak */ computeMultibandPeakDetection (multibandEnergy, multibandEnergyAverage, multibandPeak, multibandPeakHistory, currentTimer, threshold, peakPersistency, ignoreTime, interpolationFunction) { let bandsNb = multibandPeak.length; for (let band = 0; band < bandsNb; band++) { this.computePeakDetection(multibandEnergy[band], multibandEnergyAverage[band], multibandPeak[band], multibandPeakHistory[band], currentTimer, threshold, peakPersistency, ignoreTime, interpolationFunction); } } /** * This function must be growing and continue on [0; 1] in this case f(x) = x² * * @param {number} bandposition [0; 1] the band / total of bands * * @return {number} [0; 1] the new position of the band */ bandInterpolation (bandposition) { return bandposition*bandposition; } /** * @param {*} currentTimer The current timer on the frame * @param {*} peakTimer The absolute timer of the peak * @param {*} peakPersistency The time a peak takes to go down to 0 * @param {*} easingFunction Function interpolation * * @return {number} the value of the peak */ peakInterpolation (currentTimer, peakTimer, peakPersistency, easingFunction) { return Math.max( 0.0, easingFunction( 1.0 - (currentTimer - peakTimer) / peakPersistency ) ); } }; export default AudioAnalyser;