audio-inspect
Version:
Lightweight yet powerful audio analysis library
2 lines (1 loc) • 13.1 kB
JavaScript
var c=class e extends Error{name="AudioInspectError";code;cause;details;constructor(r,i,t,n){super(i,{cause:t}),this.code=r,this.cause=t,this.details=n,Error.captureStackTrace&&Error.captureStackTrace(this,e)}};function L(e,r){let i=new Float32Array(e/2+1),t=r/e;for(let n=0;n<i.length;n++)i[n]=n*t;return i}var N=class{constructor(r,i,t=!1){this.size=r;this.sampleRate=i;this.enableProfiling=t;this.complexInput=new Float32Array(this.size*2),this.frequencyBins=L(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 i=(await import("webfft")).default;this.fftInstance=new i(this.size),this.enableProfiling&&this.fftInstance?.profile&&await this.fftInstance.profile()}catch(r){throw new c("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 c("UNSUPPORTED_FORMAT","WebFFT initialization failed");if(r.length!==this.size)throw new c("INVALID_INPUT",`Invalid input size. Expected: ${this.size}, Actual: ${r.length}`);for(let a=0;a<this.size;a++)this.complexInput[a*2]=r[a],this.complexInput[a*2+1]=0;let i=this.fftInstance.fft(this.complexInput),t=new Float32Array(this.size/2+1),n=new Float32Array(this.size/2+1);for(let a=0;a<t.length;a++){let o=i[a*2],s=i[a*2+1];t[a]=Math.sqrt(o*o+s*s),n[a]=Math.atan2(s,o)}return{complex:i,magnitude:t,phase:n,frequencies:this.frequencyBins}}async profile(){if(this.initializationPromise&&await this.initializationPromise,!this.fftInstance||!this.fftInstance.profile)throw new c("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}}},z=class{constructor(r,i){this.size=r;this.sampleRate=i;if(!this.isPowerOfTwo(r))throw new c("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 t=0;t<this.size;t++){let n=0;for(let a=0;a<r;a++)n=n<<1|t>>a&1;this.bitReversalTable[t]=n}let i=this.size/2;this.twiddleFactorsReal=new Float32Array(i),this.twiddleFactorsImag=new Float32Array(i);for(let t=0;t<i;t++){let n=-2*Math.PI*t/this.size;this.twiddleFactorsReal[t]=Math.cos(n),this.twiddleFactorsImag[t]=Math.sin(n)}this.scratchReal=new Float32Array(this.size),this.scratchImag=new Float32Array(this.size),this.frequencyBins=L(this.size,this.sampleRate)}fft(r){if(r.length!==this.size)throw new c("INVALID_INPUT",`Invalid input size. Expected: ${this.size}, Actual: ${r.length}`);let i=this.scratchReal,t=this.scratchImag;for(let s=0;s<this.size;s++){let l=this.bitReversalTable[s];i[l]=r[s],t[l]=0}for(let s=1;s<this.size;s*=2){let l=s*2,u=this.size/l;for(let f=0;f<this.size;f+=l)for(let m=0;m<s;m++){let d=m*u,p=this.twiddleFactorsReal[d],A=this.twiddleFactorsImag[d],F=f+m,y=f+m+s,w=i[F],I=t[F],h=i[y],b=t[y],T=h*p-b*A,v=h*A+b*p;i[F]=w+T,t[F]=I+v,i[y]=w-T,t[y]=I-v}}let n=new Float32Array(this.size*2),a=new Float32Array(this.size/2+1),o=new Float32Array(this.size/2+1);for(let s=0;s<this.size;s++)if(n[s*2]=i[s],n[s*2+1]=t[s],s<=this.size/2){let l=i[s],u=t[s];a[s]=Math.sqrt(l*l+u*u),o[s]=Math.atan2(u,l)}return{complex:n,magnitude:a,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)}},g=class{static async createProvider(r){switch(r.type){case"webfft":{let i=new N(r.fftSize,r.sampleRate,r.enableProfiling);return await i.waitForInitialization(),i}case"native":return new z(r.fftSize,r.sampleRate);case"custom":if(!r.customProvider)throw new c("INVALID_INPUT","Custom provider is not specified");return r.customProvider;default:{let i=r.type;throw new c("UNSUPPORTED_FORMAT",`Unsupported FFT provider: ${String(i)}`)}}}static getAvailableProviders(){return["webfft","native"]}};function ie(e){let r=e.provider??"native",i=e.enableProfiling?1:0,t=e.fallbackToNative?1:0;return`${r}|${e.fftSize}|${e.sampleRate}|${i}|${t}`}var C=class{providers=new Map;async getOrCreate(r){let i=ie(r),t=this.providers.get(i);if(t)return t;let n=await B(r);return this.providers.set(i,n),n}clear(){for(let r of this.providers.values())r.dispose();this.providers.clear()}},ae=new C;async function D(e){let{cache:r,useSharedCache:i=!1,...t}=e,n=r??(i?ae:void 0);if(n)return{provider:await n.getOrCreate(t),release:()=>{}};let a=await B(t);return{provider:a,release:()=>{a.dispose()}}}async function B(e){let{fftSize:r,sampleRate:i,provider:t="native",enableProfiling:n=!1,fallbackToNative:a=t==="webfft",resolver:o=g}=e,s=a&&t!=="native"?[t,"native"]:[t],l;for(let u of s)try{return await o.createProvider({type:u,fftSize:r,sampleRate:i,enableProfiling:n})}catch(f){l=f}throw new c("INITIALIZATION_FAILED","Failed to create FFT provider",l)}function oe(e){return Array.isArray(e)}function M(e,r="mix"){if(e.numberOfChannels<=0)throw new c("INVALID_INPUT","No channels available");let i=Array.from({length:e.numberOfChannels},(a,o)=>o),t;if(r==="mix")t=i;else{if(r==="all")throw new c("INVALID_INPUT",'Channel selector "all" is not supported for scalar results; use "mix" or a single channel index');if(oe(r)){if(r.length===0)throw new c("INVALID_INPUT","Channel selection array cannot be empty");if(r.length>1)throw new c("INVALID_INPUT",'Multi-channel selector arrays are not supported for scalar results; use "mix" or a single channel index');t=r.slice()}else if(typeof r=="number"){if(!Number.isInteger(r))throw new c("INVALID_INPUT",`Invalid channel number: ${r}`);t=[r]}else throw new c("INVALID_INPUT",`Invalid channel selector: ${String(r)}`)}for(let a of t){if(a<0||a>=e.numberOfChannels)throw new c("INVALID_INPUT",`Invalid channel number: ${a}`);if(!e.channelData[a])throw new c("INVALID_INPUT",`Channel ${a} data does not exist`)}if(t.length===1){let a=t[0];return e.channelData[a]}let n=new Float32Array(e.length);for(let a=0;a<e.length;a++){let o=0;for(let s of t){let l=e.channelData[s];l&&a<l.length&&(o+=l[a])}n[a]=o/t.length}return n}function $(e,r=1,i=-1/0){return!Number.isFinite(e)||!Number.isFinite(r)||e<=0||r<=0?i:20*Math.log10(e/r)}function K(e){return e==="none"?"rectangular":e}function se(e,r,i){let t=i?.kaiserBeta??8.6,n=i?.tukeyAlpha??.5;return`${r}|${e}|${t}|${n}`}function V(e){let r=1,i=1,t=e*e/4;for(let n=1;n<50&&(i*=t/(n*n),r+=i,!(i<1e-10*r));n++);return r}function le(e,r,i){if(!Number.isInteger(e)||e<0)throw new c("INVALID_INPUT",`Invalid window length: ${e}`);if(e===0)return new Float32Array(0);let t=new Float32Array(e);if(e===1||r==="rectangular")return t.fill(1),t;switch(r){case"hann":{for(let n=0;n<e;n++)t[n]=.5*(1-Math.cos(2*Math.PI*n/(e-1)));return t}case"hamming":{for(let n=0;n<e;n++)t[n]=.54-.46*Math.cos(2*Math.PI*n/(e-1));return t}case"blackman":{for(let n=0;n<e;n++)t[n]=.42-.5*Math.cos(2*Math.PI*n/(e-1))+.08*Math.cos(4*Math.PI*n/(e-1));return t}case"bartlett":{for(let n=0;n<e;n++)t[n]=1-Math.abs((n-(e-1)/2)/((e-1)/2));return t}case"kaiser":{let n=i?.kaiserBeta??8.6,a=V(n);for(let o=0;o<e;o++){let s=2*o/(e-1)-1;t[o]=V(n*Math.sqrt(1-s*s))/a}return t}case"tukey":{let n=i?.tukeyAlpha??.5;if(n<=0)return t.fill(1),t;if(n>=1){for(let o=0;o<e;o++)t[o]=.5*(1-Math.cos(2*Math.PI*o/(e-1)));return t}let a=n*(e-1)/2;for(let o=0;o<e;o++)o<a?t[o]=.5*(1+Math.cos(Math.PI*(2*o/(n*(e-1))-1))):o<=(e-1)*(1-n/2)?t[o]=1:t[o]=.5*(1+Math.cos(Math.PI*(2*o/(n*(e-1))-2/n+1)));return t}default:throw new c("INVALID_INPUT",`Unknown window type: ${String(r)}`)}}var E=class{cache=new Map;getOrCreate(r,i,t){let n=K(i),a=se(r,n,t),o=this.cache.get(a);if(o)return o;let s=le(r,n,t);return this.cache.set(a,s),s}clear(){this.cache.clear()}},G=new E;function j(e,r,i){return G.getOrCreate(e,r,i)}function O(e){let{src:r,srcStart:i,frameLength:t,dst:n,windowType:a,windowParams:o,cache:s=G}=e;if(n.fill(0),n.length===0||t<=0||i>=r.length)return n;let l=Math.max(0,i),u=Math.min(t,n.length),f=Math.max(0,Math.min(u,r.length-l));if(f===0)return n;let m=K(a);if(m==="rectangular"||u===1)return n.set(r.subarray(l,l+f),0),n;let d=s.getOrCreate(u,m,o);for(let p=0;p<f;p++)n[p]=r[l+p]*d[p];return n}var q=new WeakMap;function ce(e){return Array.isArray(e)?`[${e.map(r=>String(r)).join(",")}]`:String(e)}function ue(e){return[e.fftSize,e.windowFunction,ce(e.channel),e.provider,e.enableProfiling?1:0,e.normalization].join("|")}function Q(e,r){if(!Number.isInteger(e)||e<=0||(e&e-1)!==0)throw new c("INVALID_INPUT",`${r} must be a power of two`)}function W(e,r){if(!Number.isInteger(e)||e<=0)throw new c("INVALID_INPUT",`${r} must be a positive integer`)}function H(e,r,i){if(!Number.isFinite(r)||!Number.isFinite(i)||r<0)throw new c("INVALID_INPUT",`Invalid frequency range: min=${r}, max=${i}`);let t=e/2;if(r>=i||i>t)throw new c("INVALID_INPUT",`Frequency range must satisfy 0 <= min < max <= Nyquist (${t})`)}function Z(e,r,i){let t=e.findIndex(o=>o>=r);t<0&&(t=0);let n=e.findIndex(o=>o>i),a=n===-1?e.length:n;return t>=a?{startIndex:0,endIndex:0}:{startIndex:t,endIndex:a}}function fe(e,r){if(r<=1||e==="none")return 1;let i=j(r,e),t=0;for(let a=0;a<i.length;a++)t+=i[a]??0;let n=t/r;return n>0?n:1}function X(e,r,i,t){if(r==="none")return e;if(i<=0||e.length===0)return e.fill(0),e;let n=fe(t,i),a=i*n;if(a<=0)return e.fill(0),e;let o=1/a,s=o*2,l=e.length-1;for(let u=0;u<e.length;u++){let f=u===0||u===l?o:s;e[u]=(e[u]??0)*f}return e}function Y(e,r){if(r==="amplitude")return e.slice();let i=new Float32Array(e.length);for(let t=0;t<e.length;t++)i[t]=$(e[t]??0,1);return i}function me(e,r,i){return e===0?0:e<r?1:Math.floor((e-r)/i)+1}function J(e,r){if(e==="dbfs"&&r!=="amplitude")throw new c("INVALID_INPUT",'scale="dbfs" requires normalization="amplitude"')}async function de(e,r={}){let{fftSize:i=2048,windowFunction:t="hann",channel:n="mix",provider:a="native",enableProfiling:o=!1,providerCache:s,normalization:l="amplitude"}=r;Q(i,"FFT size");let u=ue({fftSize:i,windowFunction:t,channel:n,provider:a,enableProfiling:o,normalization:l}),f=q.get(e);f||(f=new Map,q.set(e,f));let m=f.get(u);if(m)return m;let d=(async()=>{let A=M(e,n),F=new Float32Array(i),y=Math.min(A.length,i);O({src:A,srcStart:0,frameLength:y,dst:F,windowType:t});let{provider:w,release:I}=await D({fftSize:i,sampleRate:e.sampleRate,provider:a,enableProfiling:o,fallbackToNative:a==="webfft",cache:s});try{let h=await w.fft(F),b=new Float32Array(h.magnitude);return X(b,l,y,t),{...h,magnitude:b,fftSize:i,windowFunction:t,providerName:w.name,normalization:l}}finally{I()}})();f.set(u,d);let p=()=>{f.delete(u),f.size===0&&q.delete(e)};return d.then(p,p),d}async function Ce(e,r={}){let{fftSize:i=2048,minFrequency:t=0,maxFrequency:n=e.sampleRate/2,scale:a="dbfs",normalization:o="amplitude",...s}=r;H(e.sampleRate,t,n),J(a,o);let l=await de(e,{...s,fftSize:i,normalization:o}),{startIndex:u,endIndex:f}=Z(l.frequencies,t,n),m=l.frequencies.slice(u,f),d=l.magnitude.slice(u,f);return{frequencies:m,values:Y(d,a),scale:a}}async function De(e,r={}){let{fftSize:i=2048,frameSize:t=i,hopSize:n=Math.max(1,Math.floor(t/2)),maxFrames:a,minFrequency:o=0,maxFrequency:s=e.sampleRate/2,scale:l="dbfs",normalization:u="amplitude",windowFunction:f="hann",channel:m="mix",provider:d="native",enableProfiling:p=!1,providerCache:A}=r;if(Q(i,"FFT size"),W(t,"frameSize"),W(n,"hopSize"),t>i)throw new c("INVALID_INPUT","frameSize must be <= fftSize");a!==void 0&&W(a,"maxFrames"),H(e.sampleRate,o,s),J(l,u);let F=M(e,m),y=me(F.length,t,n),w=a===void 0?y:Math.min(y,a);if(w===0)return{times:new Float32Array(0),frequencies:new Float32Array(0),frames:[],frameCount:0,frequencyBins:0,scale:l,latest:new Float32Array(0)};let I=new Float32Array(w),h=new Array(w),b=new Float32Array(i),T=new Float32Array(0),v=0,x=0,{provider:ee,release:re}=await D({fftSize:i,sampleRate:e.sampleRate,provider:d,enableProfiling:p,fallbackToNative:d==="webfft",cache:A});try{for(let P=0;P<w;P++){let R=P*n,k=Math.min(t,Math.max(0,F.length-R));O({src:F,srcStart:R,frameLength:k,dst:b,windowType:f});let S=await ee.fft(b),_=new Float32Array(S.magnitude);if(X(_,u,k,f),P===0){let U=Z(S.frequencies,o,s);v=U.startIndex,x=U.endIndex,T=S.frequencies.slice(v,x)}let ne=_.slice(v,x);h[P]=Y(ne,l),I[P]=(R+t/2)/e.sampleRate}}finally{re()}let te=h[h.length-1]??new Float32Array(T.length);return{times:I,frequencies:T,frames:h,frameCount:h.length,frequencyBins:T.length,scale:l,latest:te}}export{de as getFFT,De as getSpectrogram,Ce as getSpectrum};