Skip to content

Commit

Permalink
fix: remove useless stable bpm concept
Browse files Browse the repository at this point in the history
  • Loading branch information
dlepaux committed Jun 24, 2023
1 parent e051d74 commit 0142d70
Show file tree
Hide file tree
Showing 4 changed files with 7 additions and 26 deletions.
4 changes: 3 additions & 1 deletion src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export function getBiquadFilters(context: AudioContext | OfflineAudioContext): N
* @param {Record<string, number[]>} data Contain valid peaks
* @param {number} audioSampleRate Audio sample rate
*/
export async function computeBpm(data: ValidPeaks, audioSampleRate: number, minPeaks = consts.minPeaks): Promise<BpmCandidates> {
export async function computeBpm(data: ValidPeaks, audioSampleRate: number): Promise<BpmCandidates> {
const minPeaks = consts.minPeaks;

/**
* Flag to fix Object.keys looping
*/
Expand Down
2 changes: 1 addition & 1 deletion src/generated-processor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default `"use strict";(()=>{var c=(n,s,e)=>new Promise((r,t)=>{var a=l=>{try{i(e.next(l))}catch(d){t(d)}},o=l=>{try{i(e.throw(l))}catch(d){t(d)}},i=l=>l.done?r(l.value):Promise.resolve(l.value).then(a,o);i((e=e.apply(n,s)).next())});var A="realtime-bpm-processor";function m(t){return c(this,arguments,function*(n,s=.2,e=.95,r=.05){let a=e;do if(a-=r,yield n(a))break;while(a>s)})}function v(n=.2,s=.95,e=.05){let r={},t=s;do t-=e,r[t.toString()]=[];while(t>n);return r}function b(n=.2,s=.95,e=.05){let r={},t=s;do t-=e,r[t.toString()]=0;while(t>n);return r}function h(){let s=0,e=new Float32Array(0);function r(){s=0,e=new Float32Array(0)}function t(){return s===4096}function a(){r()}return function(o){t()&&a();let i=new Float32Array(e.length+o.length);return i.set(e,0),i.set(o,e.length),e=i,s+=o.length,{isBufferFull:t(),buffer:e,bufferSize:4096}}}function I(n,s,e=0,r=1e4){let t=[],{length:a}=n;for(let o=e;o<a;o+=1)n[o]>s&&(t.push(o),o+=r);return{peaks:t,threshold:s}}function B(r,t){return c(this,arguments,function*(n,s,e=15){let a=!1,o=.2;if(yield m(i=>c(this,null,function*(){return a?!0:(n[i].length>e&&(a=!0,o=i),!1)})),a&&o){let i=w(n[o]),l=S(s,i);return{bpm:V(l),threshold:o}}return{bpm:[],threshold:o}})}function V(n,s=5){return n.sort((e,r)=>r.count-e.count).splice(0,s)}function w(n){let s=[];for(let e=0;e<n.length;e++)for(let r=0;r<10;r++){let t=n[e],a=e+r,o=n[a]-t;if(!s.some(l=>l.interval===o?(l.count+=1,l.count):!1)){let l={interval:o,count:1};s.push(l)}}return s}function S(n,s){let e=[];for(let r of s){if(r.interval===0)continue;r.interval=Math.abs(r.interval);let t=60/(r.interval/n);for(;t<90;)t*=2;for(;t>180;)t/=2;if(t=Math.round(t),!e.some(o=>o.tempo===t?(o.count+=r.count,o.count):!1)){let o={tempo:t,count:r.count,confidence:0};e.push(o)}}return e}var u={minValidThreshold:()=>.2,validPeaks:()=>v(),nextIndexPeaks:()=>b(),skipIndexes:()=>1,effectiveBufferTime:()=>0},f=class{constructor(){this.options={continuousAnalysis:!1,stabilizationTime:2e4,muteTimeInIndexes:1e4,debug:!1};this.minValidThreshold=u.minValidThreshold();this.validPeaks=u.validPeaks();this.nextIndexPeaks=u.nextIndexPeaks();this.skipIndexes=u.skipIndexes();this.effectiveBufferTime=u.effectiveBufferTime();this.lastTopBpmCandidate=void 0;this.topBpmCandidateCount=0;this.computedStabilizationTimeInSeconds=0;this.updateComputedValues()}setAsyncConfiguration(s){Object.assign(this.options,s),this.updateComputedValues()}updateComputedValues(){this.computedStabilizationTimeInSeconds=this.options.stabilizationTime/1e3}reset(){this.minValidThreshold=u.minValidThreshold(),this.validPeaks=u.validPeaks(),this.nextIndexPeaks=u.nextIndexPeaks(),this.skipIndexes=u.skipIndexes(),this.effectiveBufferTime=u.effectiveBufferTime()}clearValidPeaks(s){return c(this,null,function*(){this.minValidThreshold=Number.parseFloat(s.toFixed(2)),yield m(e=>c(this,null,function*(){return e<s&&(delete this.validPeaks[e],delete this.nextIndexPeaks[e]),!1}))})}analyzeChunck(s,e,r,t){return c(this,null,function*(){this.options.debug&&t({message:"ANALYZE_CHUNK",data:s}),this.effectiveBufferTime+=r;let a=r*this.skipIndexes,o=a-r;yield this.findPeaks(s,r,o,a,t),this.skipIndexes++;let i=yield B(this.validPeaks,e),{threshold:l}=i;if(t({message:"BPM",result:i}),i.bpm.length>0){let d=i.bpm[0].tempo;this.lastTopBpmCandidate===d?this.topBpmCandidateCount++:(this.topBpmCandidateCount=1,this.lastTopBpmCandidate=d)}(this.minValidThreshold<l||this.topBpmCandidateCount>=50)&&(t({message:"BPM_STABLE",result:i}),yield this.clearValidPeaks(l)),this.options.continuousAnalysis&&this.effectiveBufferTime/e>this.computedStabilizationTimeInSeconds&&(this.reset(),t({message:"ANALYZER_RESETED"}))})}findPeaks(s,e,r,t,a){return c(this,null,function*(){yield m(o=>c(this,null,function*(){if(this.nextIndexPeaks[o]>=t)return!1;let i=this.nextIndexPeaks[o]%e,{peaks:l,threshold:d}=I(s,o,i);if(l.length===0)return!1;for(let y of l){let g=r+y;this.nextIndexPeaks[d]=g+this.options.muteTimeInIndexes,this.validPeaks[d].push(g),this.options.debug&&a({message:"VALID_PEAK",data:{threshold:d,index:g}})}return!1}),this.minValidThreshold)})}};var x=class extends AudioWorkletProcessor{constructor(){super();this.realTimeBpmAnalyzer=new f;this.stopped=!1;this.aggregate=h(),this.port.addEventListener("message",this.onMessage.bind(this)),this.port.start()}onMessage(e){e.data.message==="ASYNC_CONFIGURATION"&&(console.log("[processor.onMessage] ASYNC_CONFIGURATION"),this.realTimeBpmAnalyzer.setAsyncConfiguration(e.data.parameters)),e.data.message==="RESET"&&(console.log("[processor.onMessage] RESET"),this.aggregate=h(),this.stopped=!1,this.realTimeBpmAnalyzer.reset()),e.data.message==="STOP"&&(console.log("[processor.onMessage] STOP"),this.aggregate=h(),this.stopped=!0,this.realTimeBpmAnalyzer.reset())}process(e,r,t){let a=e[0][0];if(this.stopped||!a)return!0;let{isBufferFull:o,buffer:i,bufferSize:l}=this.aggregate(a);return o&&this.realTimeBpmAnalyzer.analyzeChunck(i,sampleRate,l,d=>{this.port.postMessage(d)}).catch(d=>{console.error(d)}),!0}};registerProcessor(A,x);var K={};})();
export default `"use strict";(()=>{var c=(a,s,e)=>new Promise((o,t)=>{var l=n=>{try{i(e.next(n))}catch(d){t(d)}},r=n=>{try{i(e.throw(n))}catch(d){t(d)}},i=n=>n.done?o(n.value):Promise.resolve(n.value).then(l,r);i((e=e.apply(a,s)).next())});var A="realtime-bpm-processor";function m(t){return c(this,arguments,function*(a,s=.2,e=.95,o=.05){let l=e;do if(l-=o,yield a(l))break;while(l>s)})}function v(a=.2,s=.95,e=.05){let o={},t=s;do t-=e,o[t.toString()]=[];while(t>a);return o}function b(a=.2,s=.95,e=.05){let o={},t=s;do t-=e,o[t.toString()]=0;while(t>a);return o}function f(){let s=0,e=new Float32Array(0);function o(){s=0,e=new Float32Array(0)}function t(){return s===4096}function l(){o()}return function(r){t()&&l();let i=new Float32Array(e.length+r.length);return i.set(e,0),i.set(r,e.length),e=i,s+=r.length,{isBufferFull:t(),buffer:e,bufferSize:4096}}}function I(a,s,e=0,o=1e4){let t=[],{length:l}=a;for(let r=e;r<l;r+=1)a[r]>s&&(t.push(r),r+=o);return{peaks:t,threshold:s}}function B(o,t){return c(this,arguments,function*(a,s,e=.2){let l=15,r=!1,i=e;if(yield m(n=>c(this,null,function*(){return r?!0:typeof a[n]=="undefined"?(console.warn("valid peaks does not contains the threshold",n,Object.keys(a).length),!1):(a[n].length>l&&(r=!0,i=n),!1)}),e),r&&i){let n=w(a[i]),d=S(s,n);return{bpm:F(d),threshold:i}}return{bpm:[],threshold:i}})}function F(a,s=5){return a.sort((e,o)=>o.count-e.count).splice(0,s)}function w(a){let s=[];for(let e=0;e<a.length;e++)for(let o=0;o<10;o++){let t=a[e],l=e+o,r=a[l]-t;if(!s.some(n=>n.interval===r?(n.count+=1,n.count):!1)){let n={interval:r,count:1};s.push(n)}}return s}function S(a,s){let e=[];for(let o of s){if(o.interval===0)continue;o.interval=Math.abs(o.interval);let t=60/(o.interval/a);for(;t<90;)t*=2;for(;t>180;)t/=2;if(t=Math.round(t),!e.some(r=>r.tempo===t?(r.count+=o.count,r.count):!1)){let r={tempo:t,count:o.count,confidence:0};e.push(r)}}return e}var u={minValidThreshold:()=>.2,validPeaks:()=>v(),nextIndexPeaks:()=>b(),skipIndexes:()=>1,effectiveBufferTime:()=>0},g=class{constructor(){this.options={continuousAnalysis:!1,stabilizationTime:2e4,muteTimeInIndexes:1e4,debug:!1};this.minValidThreshold=u.minValidThreshold();this.validPeaks=u.validPeaks();this.nextIndexPeaks=u.nextIndexPeaks();this.skipIndexes=u.skipIndexes();this.effectiveBufferTime=u.effectiveBufferTime();this.lastTopBpmCandidate=void 0;this.topBpmCandidateCount=0;this.computedStabilizationTimeInSeconds=0;this.updateComputedValues()}setAsyncConfiguration(s){Object.assign(this.options,s),this.updateComputedValues()}updateComputedValues(){this.computedStabilizationTimeInSeconds=this.options.stabilizationTime/1e3}reset(){this.minValidThreshold=u.minValidThreshold(),this.validPeaks=u.validPeaks(),this.nextIndexPeaks=u.nextIndexPeaks(),this.skipIndexes=u.skipIndexes(),this.effectiveBufferTime=u.effectiveBufferTime()}clearValidPeaks(s){return c(this,null,function*(){console.log("clearValidPeaks prev",this.minValidThreshold),this.minValidThreshold=Number.parseFloat(s.toFixed(2)),console.log("clearValidPeaks new",this.minValidThreshold),yield m(e=>c(this,null,function*(){return e<s&&(console.log("clearValidPeaks at",e),console.log("clearValidPeaks",typeof this.validPeaks[e]),delete this.validPeaks[e],delete this.nextIndexPeaks[e]),!1}))})}analyzeChunck(s,e,o,t){return c(this,null,function*(){console.log("analyzeChunck"),this.options.debug&&t({message:"ANALYZE_CHUNK",data:s}),this.effectiveBufferTime+=o;let l=o*this.skipIndexes,r=l-o;yield this.findPeaks(s,o,r,l,t),this.skipIndexes++;let i=yield B(this.validPeaks,e,this.minValidThreshold),{threshold:n}=i;if(t({message:"BPM",result:i}),i.bpm.length>0){let d=i.bpm[0].tempo;this.lastTopBpmCandidate===d?this.topBpmCandidateCount++:(this.topBpmCandidateCount=1,this.lastTopBpmCandidate=d)}this.minValidThreshold<n&&(t({message:"BPM_STABLE",result:i}),yield this.clearValidPeaks(n)),this.options.continuousAnalysis&&this.effectiveBufferTime/e>this.computedStabilizationTimeInSeconds&&(this.reset(),t({message:"ANALYZER_RESETED"}))})}findPeaks(s,e,o,t,l){return c(this,null,function*(){yield m(r=>c(this,null,function*(){if(this.nextIndexPeaks[r]>=t)return!1;let i=this.nextIndexPeaks[r]%e,{peaks:n,threshold:d}=I(s,r,i);if(n.length===0)return!1;for(let x of n){let h=o+x;this.nextIndexPeaks[d]=h+this.options.muteTimeInIndexes,this.validPeaks[d].push(h),this.options.debug&&l({message:"VALID_PEAK",data:{threshold:d,index:h}})}return!1}),this.minValidThreshold)})}};var y=class extends AudioWorkletProcessor{constructor(){super();this.realTimeBpmAnalyzer=new g;this.stopped=!1;this.aggregate=f(),this.port.addEventListener("message",this.onMessage.bind(this)),this.port.start()}onMessage(e){e.data.message==="ASYNC_CONFIGURATION"&&(console.log("[processor.onMessage] ASYNC_CONFIGURATION"),this.realTimeBpmAnalyzer.setAsyncConfiguration(e.data.parameters)),e.data.message==="RESET"&&(console.log("[processor.onMessage] RESET"),this.aggregate=f(),this.stopped=!1,this.realTimeBpmAnalyzer.reset()),e.data.message==="STOP"&&(console.log("[processor.onMessage] STOP"),this.aggregate=f(),this.stopped=!0,this.realTimeBpmAnalyzer.reset())}process(e,o,t){let l=e[0][0];if(this.stopped||!l)return!0;let{isBufferFull:r,buffer:i,bufferSize:n}=this.aggregate(l);return r&&this.realTimeBpmAnalyzer.analyzeChunck(i,sampleRate,n,d=>{this.port.postMessage(d)}).catch(d=>{console.error(d)}),!0}};registerProcessor(A,y);var K={};})();
//# sourceMappingURL=realtime-bpm-processor.js.map
`;
25 changes: 2 additions & 23 deletions src/realtime-bpm-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,6 @@ export class RealTimeBpmAnalyzer {
*/
skipIndexes: number = initialValue.skipIndexes();
effectiveBufferTime: number = initialValue.effectiveBufferTime();
/**
* Stable BPM
*/
lastTopBpmCandidate: number | undefined = undefined;
topBpmCandidateCount = 0;
/**
* Computed values
*/
Expand Down Expand Up @@ -114,7 +109,7 @@ export class RealTimeBpmAnalyzer {
this.minValidThreshold = Number.parseFloat(minThreshold.toFixed(2));

await descendingOverThresholds(async threshold => {
if (threshold < minThreshold) {
if (threshold < minThreshold && typeof this.validPeaks[threshold] !== 'undefined') {
delete this.validPeaks[threshold]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
delete this.nextIndexPeaks[threshold]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
}
Expand Down Expand Up @@ -166,26 +161,10 @@ export class RealTimeBpmAnalyzer {
const {threshold} = result;
postMessage({message: 'BPM', result});

/**
* Save latest top BPM candidate
*/
if (result.bpm.length > 0) {
const latestBpmCandidate = result.bpm[0].tempo;

if (this.lastTopBpmCandidate === latestBpmCandidate) {
this.topBpmCandidateCount++;
} else {
this.topBpmCandidateCount = 1;
this.lastTopBpmCandidate = latestBpmCandidate;
}
}

/**
* If the results found have a "high" threshold, the BPM is considered stable/strong
* If the audio source is weak, the threshold won't move, so we check if the top candidate
* is stable during the last 50 chunks (50 * 4096 = 204_800 ~ 4,6s), this value (50) is totally arbitrary
*/
if (this.minValidThreshold < threshold || this.topBpmCandidateCount >= 50) {
if (this.minValidThreshold < threshold) {
postMessage({message: 'BPM_STABLE', result});
await this.clearValidPeaks(threshold);
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as consts from './consts';
import type {Peaks, ValidPeaks, NextIndexPeaks, OnThresholdFunction, AggregateData} from './types';

/**
* Loop between .9 and minValidThreshold at .3 by default, passoing the threshold to the function
* Loop between .9 and minValidThreshold at .2 by default, passing the threshold to the function
* @param {OnThresholdFunction} onThreshold Function for each iteration, you must return a boolean, true will exit the loop process
* @param {number} minValidThreshold minValidThreshold usualy 0.2
* @param {number} startThreshold startThreshold usualy 0.9
Expand Down

0 comments on commit 0142d70

Please sign in to comment.