- 1 :
class BeatDetect {
- 2 :
- 3 :
- 4 :
/** @summary <h1>Beat detection library</h1>
- 5 :
* @author Arthur Beaulieu
- 6 :
* @since November 2020
- 7 :
* @description <blockquote>This library provides an audio analyser to retrieve a track beat per minute value. It is
- 8 :
* also made to guess the time offset before the first significant sound (relative to the BPM), and the time offset of
- 9 :
* the estimated first bar. All its parameters can be updated when constructed, so it can adapt to any type of audio
- 10 :
* input. The analysis method is based on the work of Joe Sullivan and José M. Pérez, and is describe in the
- 11 :
* <code>README.md</code> file of its repository. Remarks and pull requests are welcome!</blockquote>
- 12 :
* @param {object} [options] - The configuration object of this library
- 13 :
* @param {boolean} [options.log=false] - Log debug information during the analysis process
- 14 :
* @param {boolean} [options.perf=false] - Log ellapsed time of each analysis step
- 15 :
* @param {number} [options.sampleRate=44100] - The sample rate to use during the audio analysis. Can be changed its setter
- 16 :
* @param {boolean} [options.round=false] - Allow the output rounding to remove floating point
- 17 :
* @param {number} [options.float=8] - The floating precision for the output. Disabled if round is at true
- 18 :
* @param {number} [options.lowPassFreq=150] - The low pass filter cut frequency
- 19 :
* @param {number} [options.highPassFreq=100] - The high pass filter cut frequency
- 20 :
* @param {number[]} [options.bpmRange=[90, 180]] - The BPM range to output the result in
- 21 :
* @param {number} [options.timeSignature=4] - The analysed audio time signature **/
- 22 :
constructor(options = {}) {
- 23 :
// Attach Web Audio API components to the window
- 24 :
window.AudioContext = window.AudioContext || window.webkitAudioContext;
- 25 :
window.OfflineContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
- 26 :
// Ensure that client browser supports the Web Audio API, return otherwise
- 27 :
if (!window.AudioContext || !window.OfflineContext) {
- 28 :
console.error(`BeatDetect.ERROR : Your browser doesn't support the WebAudio API.`);
- 29 :
return; // Stop right here the BeatDetect.js component construction
- 30 :
}
- 31 :
/** @public
- 32 :
* @member {string} - The BeatDetect version number **/
- 33 :
this.VERSION = '1.0.0';
- 34 :
/** @private
- 35 :
* @member {boolean} - Log debug information in the console when set to true **/
- 36 :
this._log = options.log || false;
- 37 :
/* ---- Automation beat deterination internals ---- */
- 38 :
/** @private
- 39 :
* @member {boolean} - Log elapsed times during the analysis in the console when set to true **/
- 40 :
this._perf = options.perf || false;
- 41 :
/** @private
- 42 :
* @member {number} - The sample rate used for analysis. Must match the analysed audio sample rate **/
- 43 :
this._sampleRate = options.sampleRate || 44100;
- 44 :
/** @private
- 45 :
* @member {boolean} - Remove any floating point from output when set to true **/
- 46 :
this._round = options.round || false;
- 47 :
/** @private
- 48 :
* @member {number} - The number of floating point for the output **/
- 49 :
this._float = options.float || 8;
- 50 :
/** @private
- 51 :
* @member {number} - The low pass filter cut frequency **/
- 52 :
this._lowPassFreq = options.lowPassFreq || 150;
- 53 :
/** @private
- 54 :
* @member {number} - The high pass filter cut frequency **/
- 55 :
this._highPassFreq = options.highPassFreq || 100;
- 56 :
/** @private
- 57 :
* @member {array} - The BPM range to display the output in **/
- 58 :
this._bpmRange = options.bpmRange || [90, 180];
- 59 :
/** @private
- 60 :
* @member {number} - The studied track time signature **/
- 61 :
this._timeSignature = options.timeSignature || 4;
- 62 :
/* ------ Manual beat deterination internals ------ */
- 63 :
/** @private
- 64 :
* @member {number} - The amount of time a click is trigerred to compute BPM **/
- 65 :
this.count = 0;
- 66 :
/** @private
- 67 :
* @member {object} - Contains timestamp used to determine manual BPM **/
- 68 :
this._ts = {
- 69 :
current: 0,
- 70 :
previous: 0,
- 71 :
first: 0
- 72 :
};
- 73 :
/** @private
- 74 :
* @member {number} - Reset tap timeout ID **/
- 75 :
this._tapResetId = -1;
- 76 :
}
- 77 :
- 78 :
- 79 :
/* --------------------------------------------------------------------------------------------------------------- */
- 80 :
/* --------------------------------------------- PUBLIC METHOD ------------------------------------------------ */
- 81 :
/* --------------------------------------------------------------------------------------------------------------- */
- 82 :
- 83 :
- 84 :
/** @method
- 85 :
* @name getBeatInfo
- 86 :
* @public
- 87 :
* @memberof BeatDetect
- 88 :
* @description <blockquote>Perform a beat detection on a given track and return the analysis result trhough the
- 89 :
* Promise resolution. Any exception will be thrown in the Promise catch method.</blockquote>
- 90 :
* @param {object} options - The beat detection option
- 91 :
* @param {string} options.url - The url to the audio file to analyse
- 92 :
* @param {string} [options.name] - The track name, only useful for logging
- 93 :
* @returns {promise} A Promise that is resolved when analysis is done, of will be rejected otherwise **/
- 94 :
getBeatInfo(options) {
- 95 :
// Performances mark to compute execution duration
- 96 :
options.perf = {
- 97 :
m0: performance.now(), // Start beat detection
- 98 :
m1: 0, // Fetch track done
- 99 :
m2: 0, // Offline context rendered
- 100 :
m3: 0 // Bpm processing done
- 101 :
};
- 102 :
// In order ; fetch track, decode its buffer, process it and send back BPM info
- 103 :
return new Promise((resolve, reject) => {
- 104 :
this._fetchRawTrack(options)
- 105 :
.then(this._buildOfflineCtx.bind(this))
- 106 :
.then(this._processRenderedBuffer.bind(this))
- 107 :
.then(resolve).catch(reject);
- 108 :
});
- 109 :
}
- 110 :
- 111 :
- 112 :
/* --------------------------------------------------------------------------------------------------------------- */
- 113 :
/* ---------------------------------------- OVERALL LOGIC METHODS --------------------------------------------- */
- 114 :
/* --------------------------------------------------------------------------------------------------------------- */
- 115 :
- 116 :
- 117 :
/** @method
- 118 :
* @name _fetchRawTrack
- 119 :
* @private
- 120 :
* @memberof BeatDetect
- 121 :
* @description <blockquote>This method will perform a fetch on the given URL to retrieve the track to analyse.</blockquote>
- 122 :
* @param {object} options - The option object sent to the <code>getBeatInfo</code> method, augmented with performance marks
- 123 :
* @returns {promise} A Promise that is resolved when analysis is done, of will be rejected otherwise **/
- 124 :
_fetchRawTrack(options) {
- 125 :
return new Promise((resolve, reject) => {
- 126 :
if (!options) {
- 127 :
reject('BeatDetect.ERROR : No options object sent to _fetchRawTrack method.');
- 128 :
} else if (!options.url || !options.perf || typeof options.url !== 'string' || typeof options.perf !== 'object') {
- 129 :
reject('BeatDetect.ERROR : Options object sent to _fetchRawTrack method is invalid.');
- 130 :
} else {
- 131 :
this._logEvent('log', `Fetch track${options.name ? ' ' + options.name : ''}.`);
- 132 :
let request = new XMLHttpRequest();
- 133 :
request.open('GET', options.url, true);
- 134 :
request.responseType = 'arraybuffer';
- 135 :
request.onload = () => {
- 136 :
if (request.status == 404) {
- 137 :
reject('BeatDetect.ERROR : 404 File not found.');
- 138 :
}
- 139 :
- 140 :
options.perf.m1 = performance.now();
- 141 :
resolve(Object.assign(request, options));
- 142 :
};
- 143 :
request.onerror = reject;
- 144 :
request.send();
- 145 :
}
- 146 :
});
- 147 :
}
- 148 :
- 149 :
- 150 :
/** @method
- 151 :
* @name _buildOfflineCtx
- 152 :
* @private
- 153 :
* @memberof BeatDetect
- 154 :
* @description <blockquote>This method will build and connect all required nodes to perform the BPM analysis.</blockquote>
- 155 :
* @param {object} options - The option object sent to the <code>_fetchRawTrack</code> method, augmented with track array buffer
- 156 :
* @returns {promise} A Promise that is resolved when analysis is done, of will be rejected otherwise **/
- 157 :
_buildOfflineCtx(options) {
- 158 :
return new Promise((resolve, reject) => {
- 159 :
if (!options) {
- 160 :
reject('BeatDetect.ERROR : No options object sent to _buildOfflineCtx method.');
- 161 :
} else if (!options.response || !options.perf || typeof options.response !== 'object' || typeof options.perf !== 'object') {
- 162 :
reject('BeatDetect.ERROR : Options object sent to _buildOfflineCtx method is invalid.');
- 163 :
} else {
- 164 :
this._logEvent('log', 'Offline rendering of the track.');
- 165 :
// Decode track audio with audio context to later feed the offline context with a buffer
- 166 :
const audioCtx = new AudioContext();
- 167 :
audioCtx.decodeAudioData(options.response, buffer => {
- 168 :
// Define offline context according to the buffer sample rate and duration
- 169 :
const offlineCtx = new window.OfflineContext(2, buffer.duration * this._sampleRate, this._sampleRate);
- 170 :
// Create buffer source from loaded track
- 171 :
const source = offlineCtx.createBufferSource();
- 172 :
source.buffer = buffer;
- 173 :
// Lowpass filter to ignore most frequencies except bass (goal is to retrieve kick impulsions)
- 174 :
const lowpass = offlineCtx.createBiquadFilter();
- 175 :
lowpass.type = 'lowpass';
- 176 :
lowpass.frequency.value = this._lowPassFreq;
- 177 :
lowpass.Q.value = 1;
- 178 :
// Apply a high pass filter to remove the bassline
- 179 :
const highpass = offlineCtx.createBiquadFilter();
- 180 :
highpass.type = 'highpass';
- 181 :
highpass.frequency.value = this._highPassFreq;
- 182 :
highpass.Q.value = 1;
- 183 :
// Chain offline nodes from source to destination with filters among
- 184 :
source.connect(lowpass);
- 185 :
lowpass.connect(highpass);
- 186 :
highpass.connect(offlineCtx.destination);
- 187 :
// Start the source and rendering
- 188 :
source.start(0);
- 189 :
offlineCtx.startRendering();
- 190 :
// Continnue analysis when buffer has been read
- 191 :
offlineCtx.oncomplete = result => {
- 192 :
options.perf.m2 = performance.now();
- 193 :
resolve(Object.assign(result, options));
- 194 :
};
- 195 :
offlineCtx.onerror = reject;
- 196 :
}, err => {
- 197 :
reject(`BeatDetect.ERROR : ${err}`);
- 198 :
});
- 199 :
}
- 200 :
});
- 201 :
}
- 202 :
- 203 :
- 204 :
/** @method
- 205 :
* @name _processRenderedBuffer
- 206 :
* @private
- 207 :
* @memberof BeatDetect
- 208 :
* @description <blockquote>This method will process the audio buffer to extract its peak and guess the track BPM and offset.</blockquote>
- 209 :
* @param {object} options - The option object sent to the <code>_buildOfflineCtx</code> method, augmented with track audio buffer
- 210 :
* @returns {promise} A Promise that is resolved when analysis is done, of will be rejected otherwise **/
- 211 :
_processRenderedBuffer(options) {
- 212 :
return new Promise((resolve, reject) => {
- 213 :
if (!options) {
- 214 :
reject('BeatDetect.ERROR : No options object sent to _processRenderedBuffer method.');
- 215 :
} else if (!options.renderedBuffer || !options.perf || typeof options.renderedBuffer !== 'object' || typeof options.perf !== 'object') {
- 216 :
reject('BeatDetect.ERROR : Options object sent to _processRenderedBuffer method is invalid.');
- 217 :
} else {
- 218 :
this._logEvent('log', 'Collect beat info.');
- 219 :
// Extract PCM data from offline rendered buffer
- 220 :
const dataL = options.renderedBuffer.getChannelData(0);
- 221 :
const dataR = options.renderedBuffer.getChannelData(1);
- 222 :
// Extract most intense peaks, and create intervals between them
- 223 :
const peaks = this._getPeaks([dataL, dataR]);
- 224 :
const groups = this._getIntervals(peaks);
- 225 :
// Sort found intervals by count to get the most accurate one in first position
- 226 :
var top = groups.sort((intA, intB) => {
- 227 :
return intB.count - intA.count;
- 228 :
}).splice(0, 5); // Only keep the 5 best matches
- 229 :
// Build offset and first bar
- 230 :
const offsets = this._getOffsets(dataL, top[0].tempo);
- 231 :
options.perf.m3 = performance.now();
- 232 :
this._logEvent('log', 'Analysis done.');
- 233 :
// Sent BPM info to the caller
- 234 :
resolve(Object.assign({
- 235 :
bpm: top[0].tempo,
- 236 :
offset: this._floatRound(offsets.offset, this._float),
- 237 :
firstBar: this._floatRound(offsets.firstBar, this._float)
- 238 :
}, this._perf ? { // Assign perf key to return object if user requested it
- 239 :
perf: this._getPerfDuration(options.perf)
- 240 :
} : null));
- 241 :
}
- 242 :
});
- 243 :
}
- 244 :
- 245 :
- 246 :
/* --------------------------------------------------------------------------------------------------------------- */
- 247 :
/* ------------------------------------------ BPM GUESS METHODS ----------------------------------------------- */
- 248 :
/* --------------------------------------------------------------------------------------------------------------- */
- 249 :
- 250 :
- 251 :
/** @method
- 252 :
* @name _getPeaks
- 253 :
* @private
- 254 :
* @memberof BeatDetect
- 255 :
* @description <blockquote>This method will extract peak value from given channel data. See implementation for further details.</blockquote>
- 256 :
* @param {array[]} data - Array containg L/R audio data arrays
- 257 :
* @returns {array} An array filled with peaks value **/
- 258 :
_getPeaks(data) {
- 259 :
// What we're going to do here, is to divide up our audio into parts.
- 260 :
// We will then identify, for each part, what the loudest sample is in that part.
- 261 :
// It's implied that that sample would represent the most likely 'beat' within that part.
- 262 :
// Each part 22,050 samples, half fft.
- 263 :
const partSize = this._sampleRate / 2;
- 264 :
const parts = data[0].length / partSize;
- 265 :
let peaks = [];
- 266 :
// Iterate over .5s parts we created
- 267 :
for (let i = 0; i < parts; ++i) {
- 268 :
let max = 0;
- 269 :
// Iterate each byte in the studied part
- 270 :
for (let j = i * partSize; j < (i + 1) * partSize; ++j) {
- 271 :
const volume = Math.max(Math.abs(data[0][j]), Math.abs(data[1][j]));
- 272 :
if (!max || (volume > max.volume)) {
- 273 :
// Save peak at its most intense position
- 274 :
max = {
- 275 :
position: j,
- 276 :
volume: volume
- 277 :
};
- 278 :
}
- 279 :
}
- 280 :
peaks.push(max);
- 281 :
}
- 282 :
// Sort peaks per volume
- 283 :
peaks.sort((a, b) => {
- 284 :
return b.volume - a.volume;
- 285 :
});
- 286 :
// This way we can ignore the less loud half
- 287 :
peaks = peaks.splice(0, peaks.length * 0.5);
- 288 :
// Then sort again by position to retrieve the playback order
- 289 :
peaks.sort((a, b) => {
- 290 :
return a.position - b.position;
- 291 :
});
- 292 :
// Send back peaks
- 293 :
return peaks;
- 294 :
}
- 295 :
- 296 :
- 297 :
/** @method
- 298 :
* @name _getIntervals
- 299 :
* @private
- 300 :
* @memberof BeatDetect
- 301 :
* @description <blockquote>This method will then compute time interval between peak, in order to
- 302 :
* spot the interval that is the most represented. See implementation for further details.</blockquote>
- 303 :
* @param {object[]} peaks - The peaks for a given track. Returned from _getPeaks method
- 304 :
* @returns {array} An array of time intervals **/
- 305 :
_getIntervals(peaks) {
- 306 :
// What we now do is get all of our peaks, and then measure the distance to
- 307 :
// other peaks, to create intervals. Then, based on the distance between
- 308 :
// those peaks (the distance of the intervals) we can calculate the BPM of
- 309 :
// that particular interval.
- 310 :
// The interval that is seen the most should have the BPM that corresponds
- 311 :
// to the track itself.
- 312 :
const groups = [];
- 313 :
// Comparing each peak with the next one to compute an interval group
- 314 :
peaks.forEach((peak, index) => {
- 315 :
for (let i = 1; (index + i) < peaks.length && i < 10; ++i) {
- 316 :
const group = {
- 317 :
tempo: (60 * this._sampleRate) / (peaks[index + i].position - peak.position),
- 318 :
count: 1,
- 319 :
position: peak.position,
- 320 :
peaks: []
- 321 :
};
- 322 :
// Trim to fit tempo range to lower bound
- 323 :
while (group.tempo <= this._bpmRange[0]) {
- 324 :
group.tempo *= 2;
- 325 :
}
- 326 :
// Trim to fit tempo range to upper bound
- 327 :
while (group.tempo > this._bpmRange[1]) {
- 328 :
group.tempo /= 2;
- 329 :
}
- 330 :
// Integer or floating rounding of tempo value
- 331 :
if (this._round === true) { // Integer rounding
- 332 :
group.tempo = Math.round(group.tempo);
- 333 :
} else { // Floating rounding
- 334 :
group.tempo = this._floatRound(group.tempo, this._float);
- 335 :
}
- 336 :
// Test if exists and if so, increment the interval count number
- 337 :
const exists = groups.some(interval => {
- 338 :
if (interval.tempo === group.tempo) {
- 339 :
interval.peaks.push(peak);
- 340 :
++interval.count;
- 341 :
// Notify that group already exists
- 342 :
return true;
- 343 :
}
- 344 :
// Return false if no match
- 345 :
return false;
- 346 :
});
- 347 :
// Insert only if not existing
- 348 :
if (!exists) {
- 349 :
groups.push(group);
- 350 :
}
- 351 :
}
- 352 :
});
- 353 :
- 354 :
return groups;
- 355 :
}
- 356 :
- 357 :
- 358 :
/** @method
- 359 :
* @name _getOffsets
- 360 :
* @private
- 361 :
* @memberof BeatDetect
- 362 :
* @description <blockquote>This method will finally compute time offset from song start to first bar, or first
- 363 :
* significant beat. See implementation for further details.</blockquote>
- 364 :
* @param {object[]} data - Array containg L audio data (no important to stereo this)
- 365 :
* @param {number} bpm - The most credible BPM, computed after the most frequent time interval
- 366 :
* @returns {object} The beat offset and the offset to the first bar **/
- 367 :
_getOffsets(data, bpm) {
- 368 :
// Now we have bpm, we re-calculate peaks for the 30 first seconds.
- 369 :
// Since a peak is at the maximum waveform height, we need to offset its time a little on its left.
- 370 :
// This offset is empiric, and based on a fraction of the BPM duration in time.
- 371 :
// We assume the left offset from the highest volule value is 5% of the bpm time frame
- 372 :
// Once peak are found and sorted, we get offset by taking the most intense peak (which is
- 373 :
// a strong time of the time signature), and use its position to find the smallest time from
- 374 :
// the track start that is relative to the time signature and the strong time found.
- 375 :
// The first bar is the actual first beat that overcome a 20% threshold, it will mostly be
- 376 :
// equal to the BPM offset.
- 377 :
var partSize = this._sampleRate / 2;
- 378 :
var parts = data.length / partSize;
- 379 :
var peaks = [];
- 380 :
// Create peak with little offset on the left to get the very start of the peak
- 381 :
for (let i = 0; i < parts; ++i) {
- 382 :
let max = 0;
- 383 :
for (let j = i * partSize; j < (i + 1) * partSize; ++j) {
- 384 :
const volume = data[j];
- 385 :
if (!max || (volume > max.volume)) {
- 386 :
max = {
- 387 :
position: j - Math.round(((60 / bpm) * 0.05) * this._sampleRate), // Arbitrary offset on the left of the peak about 5% bpm time
- 388 :
volume: volume
- 389 :
};
- 390 :
}
- 391 :
}
- 392 :
peaks.push(max);
- 393 :
}
- 394 :
// Saved peaks ordered by position before any sort manipuplation
- 395 :
const unsortedPeaks = [...peaks]; // Clone array before sorting for first beat matching
- 396 :
// Sort peak per decreasing volumes
- 397 :
peaks.sort((a, b) => {
- 398 :
return b.volume - a.volume;
- 399 :
});
- 400 :
// First peak is the loudest, we assume it is a strong time of the 4/4 time signature
- 401 :
const refOffset = this._getLowestTimeOffset(peaks[0].position, bpm);
- 402 :
let mean = 0;
- 403 :
let divider = 0;
- 404 :
// Find shortest offset
- 405 :
for (let i = 0; i < peaks.length; ++i) {
- 406 :
const offset = this._getLowestTimeOffset(peaks[i].position, bpm);
- 407 :
if (offset - refOffset < 0.05 || refOffset - offset > -0.05) { // Only keep first times to compute mean
- 408 :
mean += offset;
- 409 :
++divider;
- 410 :
}
- 411 :
}
- 412 :
// Find first beat offset
- 413 :
let i = 0; // Try finding the first peak index that is louder than provided threshold (0.02)
- 414 :
while (unsortedPeaks[i].volume < 0.02) { // Threshold is also arbitrary...
- 415 :
++i;
- 416 :
}
- 417 :
// Convert position into time
- 418 :
let firstBar = (unsortedPeaks[i].position / this._sampleRate);
- 419 :
// If matching first bar is before any possible time ellapsed, we set it at computed offset
- 420 :
if (firstBar > (mean / divider) && firstBar < (60 / bpm)) {
- 421 :
firstBar = (mean / divider)
- 422 :
}
- 423 :
// Return both offset and first bar offset
- 424 :
return {
- 425 :
offset: (mean / divider),
- 426 :
firstBar: firstBar
- 427 :
};
- 428 :
}
- 429 :
- 430 :
- 431 :
/** @method
- 432 :
* @name _getLowestTimeOffset
- 433 :
* @private
- 434 :
* @memberof BeatDetect
- 435 :
* @description <blockquote>This method will search for the smallest time in track for a beat ; using
- 436 :
* the estimated bpm, we rewind from time signature to get the closest from the track beginning.
- 437 :
* See implementation for further details.</blockquote>
- 438 :
* @param {object[]} position - The beat position for beat to lower
- 439 :
* @param {number} bpm - The most credible BPM, computed after the most frequent time interval
- 440 :
* @returns {object} The beat offset and the offset to the first bar **/
- 441 :
_getLowestTimeOffset(position, bpm) {
- 442 :
// Here we compute beat time offset using the first spotted peak.
- 443 :
// The lowest means we rewind following the time signature, to find the smallest time
- 444 :
// which is between 0s and the full mesure time (timesignature * tempo)
- 445 :
// Using its sample index and the found bpm
- 446 :
const bpmTime = 60 / bpm;
- 447 :
const firstBeatTime = position / this._sampleRate;
- 448 :
let offset = firstBeatTime;
- 449 :
- 450 :
while (offset >= bpmTime) {
- 451 :
offset -= (bpmTime * this._timeSignature);
- 452 :
}
- 453 :
- 454 :
if (offset < 0) {
- 455 :
while (offset < 0) {
- 456 :
offset += bpmTime;
- 457 :
}
- 458 :
}
- 459 :
- 460 :
return offset;
- 461 :
}
- 462 :
- 463 :
- 464 :
/** @method
- 465 :
* @name _getPerfDuration
- 466 :
* @private
- 467 :
* @memberof BeatDetect
- 468 :
* @description <blockquote>This method will format performance mark to be readable as times</blockquote>
- 469 :
* @param {object[]} perf - The performance mark to format
- 470 :
* @returns {object} The ellapsed times for different beat detection steps **/
- 471 :
_getPerfDuration(perf) {
- 472 :
// Convert performance mark into ellapsed seconds
- 473 :
return {
- 474 :
total: (perf.m3 - perf.m0) / 1000,
- 475 :
fetch: (perf.m1 - perf.m0) / 1000,
- 476 :
render: (perf.m2 - perf.m1) / 1000,
- 477 :
process: (perf.m3 - perf.m2) / 1000
- 478 :
}
- 479 :
}
- 480 :
- 481 :
- 482 :
/* --------------------------------------------------------------------------------------------------------------- */
- 483 :
/* ------------------------------------------- BPM TAP METHODS ------------------------------------------------ */
- 484 :
/* --------------------------------------------------------------------------------------------------------------- */
- 485 :
- 486 :
- 487 :
/** @method
- 488 :
* @name tapBpm
- 489 :
* @public
- 490 :
* @memberof BeatDetect
- 491 :
* @description <blockquote>Providing a DOM element and a callback to manually determine a bpm, using a click.
- 492 :
* After 5 seconds, the result will be reset.</blockquote>
- 493 :
* @param {objects} options - Manual bpm determinitation options
- 494 :
* @param {object} options.element - The DOM element to listen to
- 495 :
* @param {number} options.precision - The floating point for result
- 496 :
* @param {function} options.callback - The callback function to call each click **/
- 497 :
tapBpm(options) {
- 498 :
options.element.addEventListener('click', this._tapBpm.bind(this, options), false);
- 499 :
}
- 500 :
- 501 :
- 502 :
/** @method
- 503 :
* @name _tapBpm
- 504 :
* @private
- 505 :
* @memberof BeatDetect
- 506 :
* @description <blockquote>Internal method to determine manual BPM</blockquote>
- 507 :
* @param {object} options - The internal options object
- 508 :
* @param {number} precision - The floating point for result
- 509 :
* @param {function} callback - The callback function to call each click **/
- 510 :
_tapBpm(options) {
- 511 :
window.clearTimeout(this._tapResetId);
- 512 :
- 513 :
this._ts.current = Date.now();
- 514 :
// Store the first timestamp of the tap sequence on first click
- 515 :
if (this._ts.first === 0) {
- 516 :
this._ts.first = this._ts.current;
- 517 :
}
- 518 :
- 519 :
if (this._ts.previous !== 0) {
- 520 :
let bpm = 60000 * this.count / (this._ts.current - this._ts.first);
- 521 :
if (options.precision) {
- 522 :
bpm = this._floatRound(bpm, options.precision);
- 523 :
}
- 524 :
options.callback(bpm);
- 525 :
}
- 526 :
- 527 :
// Store the old timestamp
- 528 :
this._ts.previous = this._ts.current;
- 529 :
++this.count;
- 530 :
- 531 :
this._tapResetId = window.setTimeout(() => {
- 532 :
this.count = 0;
- 533 :
this._ts.current = 0;
- 534 :
this._ts.previous = 0;
- 535 :
this._ts.first = 0;
- 536 :
options.callback('--');
- 537 :
}, 5000);
- 538 :
}
- 539 :
- 540 :
- 541 :
/* --------------------------------------------------------------------------------------------------------------- */
- 542 :
/* -------------------------------------------- UTIL METHODS -------------------------------------------------- */
- 543 :
/* --------------------------------------------------------------------------------------------------------------- */
- 544 :
- 545 :
- 546 :
/** @method
- 547 :
* @name _logEvent
- 548 :
* @private
- 549 :
* @memberof BeatDetect
- 550 :
* @description <blockquote>This method will display a given console output if the logging is allowed.</blockquote>
- 551 :
* @param {string} level - The console method to call in info, log, warn, error, trace etc.
- 552 :
* @param {string} string - The text to display in the console **/
- 553 :
_logEvent(level, string) {
- 554 :
if (this._log === true) {
- 555 :
console[level](`BeatDetect : ${string}`);
- 556 :
}
- 557 :
}
- 558 :
- 559 :
- 560 :
/** @method
- 561 :
* @name _floatRound
- 562 :
* @private
- 563 :
* @memberof BeatDetect
- 564 :
* @description <blockquote>This method will return a rounded floating value to a given precision.</blockquote>
- 565 :
* @param {number} value - The value to round at a given floating point
- 566 :
* @param {number} precision - The amount of numbers after the floating point
- 567 :
* @returns {number} The rounded value with its given floating point **/
- 568 :
_floatRound(value, precision) {
- 569 :
const multiplier = Math.pow(10, precision || 0);
- 570 :
return Math.round(value * multiplier) / multiplier;
- 571 :
}
- 572 :
- 573 :
- 574 :
/* --------------------------------------------------------------------------------------------------------------- */
- 575 :
/* -------------------------------------------- SETTERS METHODS ----------------------------------------------- */
- 576 :
/* --------------------------------------------------------------------------------------------------------------- */
- 577 :
- 578 :
- 579 :
/** Set sample rate for analysis.
- 580 :
* @param {number} sampleRate **/
- 581 :
set sampleRate(sampleRate) {
- 582 :
this._sampleRate = sampleRate;
- 583 :
}
- 584 :
- 585 :
- 586 :
/** Set logging in console.
- 587 :
* @param {boolean} log **/
- 588 :
set log(log) {
- 589 :
this._log = log;
- 590 :
}
- 591 :
- 592 :
- 593 :
/** Set performance timings in console.
- 594 :
* @param {boolean} perf **/
- 595 :
set perf(perf) {
- 596 :
this._perf = perf;
- 597 :
}
- 598 :
- 599 :
- 600 :
/** Set output rounding.
- 601 :
* @param {boolean} round **/
- 602 :
set round(round) {
- 603 :
this._round = round;
- 604 :
}
- 605 :
- 606 :
- 607 :
/** Set the output floating precision.
- 608 :
* @param {number} round **/
- 609 :
set float(float) {
- 610 :
this._float = float;
- 611 :
}
- 612 :
- 613 :
- 614 :
/** Set the low pass filter cut frequency.
- 615 :
* @param {number} round **/
- 616 :
set lowPassFreq(lowPassFreq) {
- 617 :
this._lowPassFreq = lowPassFreq;
- 618 :
}
- 619 :
- 620 :
- 621 :
/** Set the high pass filter cut frequency.
- 622 :
* @param {number} round **/
- 623 :
set highPassFreq(highPassFreq) {
- 624 :
this._highPassFreq = highPassFreq;
- 625 :
}
- 626 :
- 627 :
- 628 :
}
- 629 :
- 630 :
- 631 :
export default BeatDetect;