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