audio-inspect
Version:
Lightweight yet powerful audio analysis library
2 lines (1 loc) • 12.6 kB
JavaScript
var b=class t extends Error{name="AudioInspectError";code;cause;details;constructor(r,a,n,e){super(a,{cause:n}),this.code=r,this.cause=n,this.details=e,Error.captureStackTrace&&Error.captureStackTrace(this,t)}};function W(t,r){let a=new Float32Array(t/2+1),n=r/t;for(let e=0;e<a.length;e++)a[e]=e*n;return a}var C=class{constructor(r,a,n=!1){this.size=r;this.sampleRate=a;this.enableProfiling=n;this.complexInput=new Float32Array(this.size*2),this.frequencyBins=W(this.size,this.sampleRate),this.initializationPromise=this.initializeWebFFT()}fftInstance=null;initializationPromise=null;complexInput;frequencyBins;get name(){return"WebFFT"}async waitForInitialization(){this.initializationPromise&&await this.initializationPromise}async initializeWebFFT(){try{let a=(await import("webfft")).default;this.fftInstance=new a(this.size),this.enableProfiling&&this.fftInstance?.profile&&await this.fftInstance.profile()}catch(r){throw new b("UNSUPPORTED_FORMAT",`Failed to initialize WebFFT: ${r instanceof Error?r.message:String(r)}`)}}async fft(r){if(this.initializationPromise&&await this.initializationPromise,!this.fftInstance)throw new b("UNSUPPORTED_FORMAT","WebFFT initialization failed");if(r.length!==this.size)throw new b("INVALID_INPUT",`Invalid input size. Expected: ${this.size}, Actual: ${r.length}`);for(let i=0;i<this.size;i++)this.complexInput[i*2]=r[i],this.complexInput[i*2+1]=0;let a=this.fftInstance.fft(this.complexInput),n=new Float32Array(this.size/2+1),e=new Float32Array(this.size/2+1);for(let i=0;i<n.length;i++){let o=a[i*2],s=a[i*2+1];n[i]=Math.sqrt(o*o+s*s),e[i]=Math.atan2(s,o)}return{complex:a,magnitude:n,phase:e,frequencies:this.frequencyBins}}async profile(){if(this.initializationPromise&&await this.initializationPromise,!this.fftInstance||!this.fftInstance.profile)throw new b("UNSUPPORTED_FORMAT","WebFFT is not initialized");await this.fftInstance.profile()}dispose(){if(this.fftInstance)try{this.fftInstance.dispose&&this.fftInstance.dispose()}catch(r){console.warn("FFT instance disposal failed:",r)}finally{this.fftInstance=null,this.initializationPromise=null}}},E=class{constructor(r,a){this.size=r;this.sampleRate=a;if(!this.isPowerOfTwo(r))throw new b("INVALID_INPUT","FFT size must be a power of two");this.precomputeTables()}bitReversalTable;twiddleFactorsReal;twiddleFactorsImag;scratchReal;scratchImag;frequencyBins;get name(){return"Native FFT (Cooley-Tukey)"}isPowerOfTwo(r){return r>0&&(r&r-1)===0}precomputeTables(){this.bitReversalTable=new Uint32Array(this.size);let r=Math.log2(this.size);for(let n=0;n<this.size;n++){let e=0;for(let i=0;i<r;i++)e=e<<1|n>>i&1;this.bitReversalTable[n]=e}let a=this.size/2;this.twiddleFactorsReal=new Float32Array(a),this.twiddleFactorsImag=new Float32Array(a);for(let n=0;n<a;n++){let e=-2*Math.PI*n/this.size;this.twiddleFactorsReal[n]=Math.cos(e),this.twiddleFactorsImag[n]=Math.sin(e)}this.scratchReal=new Float32Array(this.size),this.scratchImag=new Float32Array(this.size),this.frequencyBins=W(this.size,this.sampleRate)}fft(r){if(r.length!==this.size)throw new b("INVALID_INPUT",`Invalid input size. Expected: ${this.size}, Actual: ${r.length}`);let a=this.scratchReal,n=this.scratchImag;for(let s=0;s<this.size;s++){let u=this.bitReversalTable[s];a[u]=r[s],n[u]=0}for(let s=1;s<this.size;s*=2){let u=s*2,l=this.size/u;for(let m=0;m<this.size;m+=u)for(let h=0;h<s;h++){let c=h*l,d=this.twiddleFactorsReal[c],f=this.twiddleFactorsImag[c],F=m+h,T=m+h+s,x=a[F],M=n[F],p=a[T],w=n[T],R=p*d-w*f,y=p*f+w*d;a[F]=x+R,n[F]=M+y,a[T]=x-R,n[T]=M-y}}let e=new Float32Array(this.size*2),i=new Float32Array(this.size/2+1),o=new Float32Array(this.size/2+1);for(let s=0;s<this.size;s++)if(e[s*2]=a[s],e[s*2+1]=n[s],s<=this.size/2){let u=a[s],l=n[s];i[s]=Math.sqrt(u*u+l*l),o[s]=Math.atan2(l,u)}return{complex:e,magnitude:i,phase:o,frequencies:this.frequencyBins}}dispose(){this.scratchReal=new Float32Array(0),this.scratchImag=new Float32Array(0),this.bitReversalTable=new Uint32Array(0),this.twiddleFactorsReal=new Float32Array(0),this.twiddleFactorsImag=new Float32Array(0),this.frequencyBins=new Float32Array(0)}},D=class{static async createProvider(r){switch(r.type){case"webfft":{let a=new C(r.fftSize,r.sampleRate,r.enableProfiling);return await a.waitForInitialization(),a}case"native":return new E(r.fftSize,r.sampleRate);case"custom":if(!r.customProvider)throw new b("INVALID_INPUT","Custom provider is not specified");return r.customProvider;default:{let a=r.type;throw new b("UNSUPPORTED_FORMAT",`Unsupported FFT provider: ${String(a)}`)}}}static getAvailableProviders(){return["webfft","native"]}};function Q(t){let r=t.provider??"native",a=t.enableProfiling?1:0,n=t.fallbackToNative?1:0;return`${r}|${t.fftSize}|${t.sampleRate}|${a}|${n}`}var O=class{providers=new Map;async getOrCreate(r){let a=Q(r),n=this.providers.get(a);if(n)return n;let e=await _(r);return this.providers.set(a,e),e}clear(){for(let r of this.providers.values())r.dispose();this.providers.clear()}},j=new O;async function k(t){let{cache:r,useSharedCache:a=!1,...n}=t,e=r??(a?j:void 0);if(e)return{provider:await e.getOrCreate(n),release:()=>{}};let i=await _(n);return{provider:i,release:()=>{i.dispose()}}}async function _(t){let{fftSize:r,sampleRate:a,provider:n="native",enableProfiling:e=!1,fallbackToNative:i=n==="webfft",resolver:o=D}=t,s=i&&n!=="native"?[n,"native"]:[n],u;for(let l of s)try{return await o.createProvider({type:l,fftSize:r,sampleRate:a,enableProfiling:e})}catch(m){u=m}throw new b("INITIALIZATION_FAILED","Failed to create FFT provider",u)}function q(t){return t==="none"?"rectangular":t}function G(t,r,a){let n=a?.kaiserBeta??8.6,e=a?.tukeyAlpha??.5;return`${r}|${t}|${n}|${e}`}function U(t){let r=1,a=1,n=t*t/4;for(let e=1;e<50&&(a*=n/(e*e),r+=a,!(a<1e-10*r));e++);return r}function Z(t,r,a){if(!Number.isInteger(t)||t<0)throw new b("INVALID_INPUT",`Invalid window length: ${t}`);if(t===0)return new Float32Array(0);let n=new Float32Array(t);if(t===1||r==="rectangular")return n.fill(1),n;switch(r){case"hann":{for(let e=0;e<t;e++)n[e]=.5*(1-Math.cos(2*Math.PI*e/(t-1)));return n}case"hamming":{for(let e=0;e<t;e++)n[e]=.54-.46*Math.cos(2*Math.PI*e/(t-1));return n}case"blackman":{for(let e=0;e<t;e++)n[e]=.42-.5*Math.cos(2*Math.PI*e/(t-1))+.08*Math.cos(4*Math.PI*e/(t-1));return n}case"bartlett":{for(let e=0;e<t;e++)n[e]=1-Math.abs((e-(t-1)/2)/((t-1)/2));return n}case"kaiser":{let e=a?.kaiserBeta??8.6,i=U(e);for(let o=0;o<t;o++){let s=2*o/(t-1)-1;n[o]=U(e*Math.sqrt(1-s*s))/i}return n}case"tukey":{let e=a?.tukeyAlpha??.5;if(e<=0)return n.fill(1),n;if(e>=1){for(let o=0;o<t;o++)n[o]=.5*(1-Math.cos(2*Math.PI*o/(t-1)));return n}let i=e*(t-1)/2;for(let o=0;o<t;o++)o<i?n[o]=.5*(1+Math.cos(Math.PI*(2*o/(e*(t-1))-1))):o<=(t-1)*(1-e/2)?n[o]=1:n[o]=.5*(1+Math.cos(Math.PI*(2*o/(e*(t-1))-2/e+1)));return n}default:throw new b("INVALID_INPUT",`Unknown window type: ${String(r)}`)}}var L=class{cache=new Map;getOrCreate(r,a,n){let e=q(a),i=G(r,e,n),o=this.cache.get(i);if(o)return o;let s=Z(r,e,n);return this.cache.set(i,s),s}clear(){this.cache.clear()}},X=new L;function B(t){let{src:r,srcStart:a,frameLength:n,dst:e,windowType:i,windowParams:o,cache:s=X}=t;if(e.fill(0),e.length===0||n<=0||a>=r.length)return e;let u=Math.max(0,a),l=Math.min(n,e.length),m=Math.max(0,Math.min(l,r.length-u));if(m===0)return e;let h=q(i);if(h==="rectangular"||l===1)return e.set(r.subarray(u,u+m),0),e;let c=s.getOrCreate(l,h,o);for(let d=0;d<m;d++)e[d]=r[u+d]*c[d];return e}function V(t,r=1,a=-1/0){return!Number.isFinite(t)||!Number.isFinite(r)||t<=0||r<=0?a:20*Math.log10(t/r)}function $(t,r=1,a=-1/0){return!Number.isFinite(t)||!Number.isFinite(r)||t<=0||r<=0?a:10*Math.log10(t/r)}function Y(t){return Number.isInteger(t)&&t>0&&(t&t-1)===0}function K(t){if(!Number.isFinite(t)||t<=1)return 1;if(Y(t))return t;let r=1;for(;r<t&&r<Number.MAX_SAFE_INTEGER;)r*=2;return r}function J(t){let r=2*Math.PI,a=(t+Math.PI)%r;return a<0&&(a+=r),a-Math.PI}async function N(t,r,a,n,e){let i=e??new Float32Array(a);return B({src:t,srcStart:r,frameLength:Math.min(a,Math.max(0,t.length-r)),dst:i,windowType:"hann"}),n.fft(i)}function ee(t,r,a=44){let n=Math.min(t.length,r.length),e=-1/0,i=0;for(let o=-a;o<=a;o++){let s=0,u=0;for(let l=0;l<n;l++){let m=l+o;if(m>=0&&m<n){let h=t[l],c=r[m];s+=h*c,u++}}u>0&&(s/=u,s>e&&(e=s,i=o))}return i}function H(t,r,a){let n=Math.min(t.length,r.length),e=a?new Float32Array(n):void 0,i=a?new Float32Array(n):void 0;if(n===0)return{correlation:0,width:0,balance:0,midSideRatio:0,energyL:0,energyR:0,mid:e,side:i};let o=0,s=0,u=0,l=0,m=0,h=0,c=0,d=0,f=0;for(let g=0;g<n;g++){let I=t[g],v=r[g];o+=I,s+=v,u+=I*v,l+=I*I,m+=v*v,h+=I*I,c+=v*v;let S=(I+v)*.5,z=(I-v)*.5;d+=S*S,f+=z*z,e&&i&&(e[g]=S,i[g]=z)}let F=o/n,T=s/n,x=u/n-F*T,M=Math.sqrt(Math.max(0,l/n-F*F)),p=Math.sqrt(Math.max(0,m/n-T*T)),w=M>1e-10&&p>1e-10?x/(M*p):0,R=Math.max(-1,Math.min(1,w)),y=d+f>1e-10?f/(d+f):0,P=h+c>1e-10?(c-h)/(h+c):0,A=f>1e-10?$(d/f,1):d>1e-10?1/0:0;return{correlation:R,width:y,balance:P,midSideRatio:A,energyL:h,energyR:c,mid:e,side:i}}async function te(t,r,a,n,e){let i=Math.min(t.length,r.length),o=Math.floor(a/2)+1,s=new Float32Array(o),u=new Float32Array(o),l=new Float32Array(o),m=new Float32Array(o),h=new Float32Array(a),c=new Float32Array(a),d=0,f=0;for(;f<i||d===0;){let p=Math.min(f+a,i),[w,R]=await Promise.all([N(t,f,a,e,h),N(r,f,a,e,c)]);for(let y=0;y<o;y++){let P=w.complex[y*2]??0,A=w.complex[y*2+1]??0,g=R.complex[y*2]??0,I=R.complex[y*2+1]??0;s[y]=s[y]+P*g+A*I,u[y]=u[y]+A*g-P*I,l[y]=l[y]+P*P+A*A,m[y]=m[y]+g*g+I*I}if(d++,p>=i)break;f+=n}let F=new Float32Array(o),T=0;for(let p=0;p<o;p++){let w=l[p]*m[p];w>T&&(T=w)}let x=Math.max(1e-10,T*1e-4),M=Math.sqrt(x);for(let p=0;p<o;p++){let w=l[p]*m[p],R=s[p]**2+u[p]**2;if(w<=x){let P=Math.abs(l[p]??0)<=M,A=Math.abs(m[p]??0)<=M;F[p]=P&&A?1:0;continue}let y=R/w;F[p]=Math.min(1,Math.max(0,y))}return F}function re(t,r,a,n){let e=new Float32Array(t.length);for(let i=0;i<e.length;i++){let o=t[i],s=r[i],u=a[i],l=n[i],m=u-l,h=Math.abs(o+s)/2,c=Math.abs(o-s)/2,d=Math.abs(Math.sin(m/2)),f=c/(h+c+1e-10);e[i]=Math.max(f,d)}return e}async function Ae(t,r={}){if(t.numberOfChannels<2)throw new b("INVALID_INPUT","Stereo analysis requires audio with at least two channels");let{frameSize:a=Math.min(8192,t.length),calculatePhase:n=!1,calculateITD:e=!1,calculateILD:i=!1,provider:o="native",enableProfiling:s=!1,providerCache:u}=r,l=t.channelData[0],m=t.channelData[1];if(!l||!m)throw new b("INVALID_INPUT","L/R channel data is missing");let h=Math.min(l.length,m.length);if(h===0)return{correlation:0,width:0,balance:0,midSideRatio:0};let c=H(l,m,!0),d={correlation:c.correlation,width:c.width,balance:c.balance,midSideRatio:c.midSideRatio};if(c.side&&c.mid&&(d.goniometer={x:c.side,y:c.mid}),n){let f=Math.max(32,Math.min(a,h)),F=K(f),T=Math.max(1,Math.floor(f/2)),{provider:x,release:M}=await k({fftSize:F,sampleRate:t.sampleRate,provider:o,enableProfiling:s,fallbackToNative:o==="webfft",cache:u});try{let[p,w]=await Promise.all([N(l.subarray(0,f),0,F,x),N(m.subarray(0,f),0,F,x)]);d.coherence=await te(l.subarray(0,h),m.subarray(0,h),F,T,x),d.widthFrequency=re(p.magnitude,w.magnitude,p.phase,w.phase);let R=0,y=0,P=0;for(let A=1;A<p.phase.length;A++){let g=p.magnitude[A],I=w.magnitude[A],v=g*I;if(v<=0)continue;let S=J(p.phase[A]-w.phase[A]);R+=S*v,y+=Math.cos(S)*v,P+=v}d.phaseDifference=P>1e-10?R/P*180/Math.PI:0,d.phaseCorrelation=P>1e-10?y/P:0}finally{M()}}if(e){let f=Math.max(1,Math.round(t.sampleRate*.001)),F=ee(l.subarray(0,Math.min(a,h)),m.subarray(0,Math.min(a,h)),f);d.itd=F/t.sampleRate*1e3}if(i){let f=Math.sqrt(c.energyL/h),F=Math.sqrt(c.energyR/h);d.ild=f>1e-10&&F>1e-10?V(F/f,1):0}return d}async function Ie(t,r={}){if(t.numberOfChannels<2)throw new b("INVALID_INPUT","Stereo analysis requires audio with at least two channels");let a=t.channelData[0],n=t.channelData[1];if(!a||!n)throw new b("INVALID_INPUT","L/R channel data is missing");let e=Math.min(a.length,n.length);if(e===0)return{times:new Float32Array(0),correlation:new Float32Array(0),width:new Float32Array(0),balance:new Float32Array(0)};let i=Math.max(32,Math.min(r.windowSize??r.frameSize??2048,e)),o=r.hopSize??Math.max(1,Math.floor(i/2));if(o<=0)throw new b("INVALID_INPUT","hopSize must be a positive integer");let s=e<=i?1:Math.floor((e-i)/o)+1,u=new Float32Array(s),l=new Float32Array(s),m=new Float32Array(s),h=new Float32Array(s);for(let c=0;c<s;c++){let d=c*o,f=Math.min(d+i,e),F=H(a.subarray(d,f),n.subarray(d,f),!1);u[c]=(d+(f-d)/2)/t.sampleRate,l[c]=F.correlation,m[c]=F.width,h[c]=F.balance}return{times:u,correlation:l,width:m,balance:h}}export{Ae as getStereoAnalysis,Ie as getTimeVaryingStereoAnalysis};