UNPKG

audio-inspect

Version:

Lightweight yet powerful audio analysis library

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