@jadujoel/web-audio-clip-node 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@
11
11
  <li><a href="esm-bundler">ESM Bundler</a></li>
12
12
  <li><a href="react">React</a></li>
13
13
  <li><a href="self-hosted">Self Hosted</a></li>
14
+ <li><a href="streaming">Streaming</a></li>
14
15
  </ul>
15
16
  </body>
16
17
  </html>
@@ -1,4 +1,4 @@
1
- var P={Initial:0,Started:1,Stopped:2,Paused:3,Scheduled:4,Ended:5,Disposed:6};var _=128;function i(V=[]){let A=V[0]?.length??0,F=A>0;return{totalLength:F?A:null,committedLength:F?A:0,streamEnded:F,streaming:!1,writtenSpans:F?[{startSample:0,endSample:A}]:[],pendingWrites:[],lowWaterThreshold:_*4,lowWaterNotified:!1,lastUnderrunSample:null}}function G0(V){return V[0]?.length??0}function o(V){return V.streamBuffer.totalLength??G0(V.buffer)}function H0(V,A){return Array.from({length:V},()=>new Float32Array(A))}function E0(V,A){let F=[...V,A].sort((J,k)=>J.startSample-k.startSample),T=[];for(let J of F){let k=T[T.length-1];if(!k||J.startSample>k.endSample){T.push({...J});continue}k.endSample=Math.max(k.endSample,J.endSample)}return T}function I0(V){let A=0;for(let F of V){if(F.startSample>A)break;A=Math.max(A,F.endSample)}return A}function w0(V,A){if(V.committedLength-Math.floor(A)>=V.lowWaterThreshold)V.lowWaterNotified=!1}function R0(V,A,F){let T=G0(V.buffer),J=V.buffer.length;if(T>=F&&J>=A)return;let k=Math.max(T,F),Q=Math.max(J,A),X=H0(Q,k);for(let U=0;U<J;U++)X[U].set(V.buffer[U].subarray(0,T));if(V.buffer=X,V.streamBuffer.totalLength==null||V.streamBuffer.totalLength<k)V.streamBuffer.totalLength=k}function B0(V,A){let F=Math.max(Math.floor(A.startSample),0),T=A.channelData[0]?.length??0,J=A.totalLength??null,k=Math.max(F+T,J??0);R0(V,Math.max(A.channelData.length,V.buffer.length,1),k);for(let Q=0;Q<A.channelData.length;Q++)V.buffer[Q].set(A.channelData[Q],F);if(J!=null)V.streamBuffer.totalLength=J;if(T>0)V.streamBuffer.writtenSpans=E0(V.streamBuffer.writtenSpans,{startSample:F,endSample:F+T}),V.streamBuffer.committedLength=I0(V.streamBuffer.writtenSpans);if(A.streamEnded===!0)V.streamBuffer.streamEnded=!0;w0(V.streamBuffer,V.playhead)}function y0(V){if(V.streamBuffer.pendingWrites.length===0)return;for(let A of V.streamBuffer.pendingWrites)B0(V,A);V.streamBuffer.pendingWrites=[]}function _0(V,A){V.buffer=A,V.streamBuffer=i(A)}function v0(V={},A){let{buffer:F=[],streamBuffer:T=i(F),duration:J=-1,loop:k=!1,loopStart:Q=0,loopEnd:X=(F[0]?.length??0)/A,loopCrossfade:U=0,playhead:$=0,offset:M=0,startWhen:Y=0,stopWhen:j=0,pauseWhen:z=0,resumeWhen:C=0,playedSamples:N=0,state:K=P.Initial,timesLooped:w=0,fadeInDuration:v=0,fadeOutDuration:Z=0,enableFadeIn:E=v>0,enableFadeOut:O=Z>0,enableLoopStart:R=!0,enableLoopEnd:d=!0,enableLoopCrossfade:b=U>0,enableHighpass:c=!0,enableLowpass:g=!0,enableGain:s=!0,enablePan:t=!0,enableDetune:r=!0,enablePlaybackRate:p=!0}=V;return{buffer:F,streamBuffer:T,loop:k,loopStart:Q,loopEnd:X,loopCrossfade:U,duration:J,playhead:$,offset:M,startWhen:Y,stopWhen:j,pauseWhen:z,resumeWhen:C,playedSamples:N,state:K,timesLooped:w,fadeInDuration:v,fadeOutDuration:Z,enableFadeIn:E,enableFadeOut:O,enableLoopStart:R,enableLoopEnd:d,enableHighpass:c,enableLowpass:g,enableGain:s,enablePan:t,enableDetune:r,enablePlaybackRate:p,enableLoopCrossfade:b}}function b0(V,A){return o(V)/A}function n(V,A){let F=b0(V,A);if(F<=0){V.loopStart=0,V.loopEnd=0;return}if(!Number.isFinite(V.loopStart)||V.loopStart<0)V.loopStart=0;if(V.loopStart>=F)V.loopStart=0;if(!Number.isFinite(V.loopEnd)||V.loopEnd<=V.loopStart||V.loopEnd>F)V.loopEnd=F}function e(V,A,F){if(A===void 0)return V.offset=0,0;if(A<0)return e(V,o(V)+A,F);if(A>(o(V)||1)-1)return e(V,o(V)%A,F);let T=Math.floor(A*F);return V.offset=T,T}function L0(V){let{playhead:A,bufferLength:F,loop:T,loopStartSamples:J,loopEndSamples:k}=V,Q=128;if(!T&&A+128>F)Q=Math.max(F-A,0);let X=Array(Q);if(!T){for(let Y=0,j=A;Y<Q;Y++,j++)X[Y]=j;let M=A+Q;return{playhead:M,indexes:X,looped:!1,ended:M>=F}}let U=A,$=!1;for(let M=0;M<Q;M++,U++){if(U>=k)U=J+(U-k),$=!0;X[M]=U}return{indexes:X,looped:$,ended:!1,playhead:U}}function x0(V){let{playhead:A,bufferLength:F,loop:T,loopStartSamples:J,loopEndSamples:k,playbackRates:Q}=V,X=128;if(!T&&A+128>F)X=Math.max(F-A,0);let U=Array(X),$=A,M=!1;if(T){for(let Y=0;Y<X;Y++){U[Y]=Math.min(Math.max(Math.floor($),0),F-1);let j=Q[Y]??Q[0]??1;if($+=j,j>=0&&($>k||$>F))$=J,M=!0;else if(j<0&&($<J||$<0))$=k,M=!0}return{playhead:$,indexes:U,looped:M,ended:!1}}for(let Y=0;Y<X;Y++)U[Y]=Math.min(Math.max(Math.floor($),0),F-1),$+=Q[Y]??Q[0]??1;return{playhead:$,indexes:U,looped:!1,ended:$>=F||$<0}}function m0(V,A,F){let T=Math.min(V.length,A.length);for(let J=0;J<F.length;J++)for(let k=0;k<T;k++)V[k][J]=A[k][F[J]];for(let J=T;J<V.length;J++)for(let k=0;k<V[J].length;k++)V[J][k]=0;for(let J=F.length;J<V[0].length;J++)for(let k=0;k<T;k++)V[k][J]=0}function h(V){for(let A=0;A<V.length;A++)for(let F=0;F<V[A].length;F++)V[A][F]=0}function S0(V){if(V.length>=2)for(let A=0;A<V[0].length;A++)V[1][A]=V[0][A];else{let A=new Float32Array(V[0].length);for(let F=0;F<V[0].length;F++)A[F]=V[0][F];V.push(A)}}function M0(V,A){for(let F=A.length;F<V.length;F++)A[F]=new Float32Array(V[F].length);for(let F=0;F<V.length;F++)for(let T=0;T<V[F].length;T++)A[F][T]=V[F][T]}function c0(V){let A=0;for(let F=0;F<V.length;F++)for(let T=0;T<V[F].length;T++)if(Number.isNaN(V[F][T]))A++,V[F][T]=0;return A}function V0(){return[{x_1:0,x_2:0,y_1:0,y_2:0},{x_1:0,x_2:0,y_1:0,y_2:0}]}function g0(V,A){if(A.length===1){let T=A[0];if(T===1)return;for(let J of V)for(let k=0;k<J.length;k++)J[k]*=T;return}let F=A[0];for(let T of V)for(let J=0;J<T.length;J++)F=A[J]??F,T[J]*=F}function h0(V,A){let F=A[0];for(let T=0;T<V[0].length;T++){F=A[T]??F;let J=F<=0?1:1-F,k=F>=0?1:1+F;V[0][T]*=J,V[1][T]*=k}}function d0(V,A,F,T){for(let J=0;J<V.length;J++){let k=V[J],{x_1:Q,x_2:X,y_1:U,y_2:$}=T[J]??{x_1:0,x_2:0,y_1:0,y_2:0};if(A.length===1){let M=A[0];if(M>=20000)return;let Y=2*Math.PI*M/F,j=Math.sin(Y)/2,z=(1-Math.cos(Y))/2,C=1-Math.cos(Y),N=(1-Math.cos(Y))/2,K=1+j,w=-2*Math.cos(Y),v=1-j,Z=z/K,E=C/K,O=N/K,R=w/K,d=v/K;for(let b=0;b<k.length;b++){let c=k[b],g=Z*c+E*Q+O*X-R*U-d*$;X=Q,Q=c,$=U,U=g,k[b]=g}}else{let M=A[0];for(let Y=0;Y<k.length;Y++){let j=A[Y]??M,z=2*Math.PI*j/F,C=Math.sin(z)/2,N=(1-Math.cos(z))/2,K=1-Math.cos(z),w=(1-Math.cos(z))/2,v=1+C,Z=-2*Math.cos(z),E=1-C,O=k[Y],R=N/v*O+K/v*Q+w/v*X-Z/v*U-E/v*$;X=Q,Q=O,$=U,U=R,k[Y]=R}}T[J]={x_1:Q,x_2:X,y_1:U,y_2:$}}}function u0(V,A,F,T){for(let J=0;J<V.length;J++){let k=V[J],{x_1:Q,x_2:X,y_1:U,y_2:$}=T[J]??{x_1:0,x_2:0,y_1:0,y_2:0};if(A.length===1){let M=A[0];if(M<=20)return;let Y=2*Math.PI*M/F,j=Math.sin(Y)/2,z=(1+Math.cos(Y))/2,C=-(1+Math.cos(Y)),N=(1+Math.cos(Y))/2,K=1+j,w=-2*Math.cos(Y),v=1-j;for(let Z=0;Z<k.length;Z++){let E=k[Z],O=z/K*E+C/K*Q+N/K*X-w/K*U-v/K*$;X=Q,Q=E,$=U,U=O,k[Z]=O}}else{let M=A[0];for(let Y=0;Y<k.length;Y++){let j=A[Y]??M,z=2*Math.PI*j/F,C=Math.sin(z)/2,N=(1+Math.cos(z))/2,K=-(1+Math.cos(z)),w=(1+Math.cos(z))/2,v=1+C,Z=-2*Math.cos(z),E=1-C,O=k[Y],R=N/v*O+K/v*Q+w/v*X-Z/v*U-E/v*$;X=Q,Q=O,$=U,U=R,k[Y]=R}}T[J]={x_1:Q,x_2:X,y_1:U,y_2:$}}}function P0(V,A,F,T){let{type:J,data:k}=A;switch(J){case"buffer":return _0(V,k),n(V,T),[];case"bufferInit":{let Q=k;return V.buffer=H0(Q.channels,Q.totalLength),V.streamBuffer={...i(),totalLength:Q.totalLength,streamEnded:!1,streaming:Q.streaming??!0},n(V,T),[]}case"bufferRange":return V.streamBuffer.pendingWrites.push(k),[];case"bufferEnd":{let Q=k;if(Q?.totalLength!=null)V.streamBuffer.totalLength=Q.totalLength;return V.streamBuffer.streamEnded=!0,[]}case"bufferReset":return V.buffer=[],V.streamBuffer=i(),n(V,T),[];case"start":V.timesLooped=0;{let Q=k;if(V.duration=Q?.duration??-1,V.duration===-1)V.duration=V.loop?Number.MAX_SAFE_INTEGER:(V.buffer[0]?.length??0)/T;e(V,Q?.offset,T),n(V,T),V.playhead=V.offset,V.startWhen=Q?.when??F,V.stopWhen=V.startWhen+V.duration,V.playedSamples=0,V.state=P.Scheduled}return[{type:"scheduled"}];case"stop":if(V.state===P.Ended||V.state===P.Initial)return[];return V.stopWhen=k??V.stopWhen,V.state=P.Stopped,[{type:"stopped"}];case"pause":return V.state=P.Paused,V.pauseWhen=k??F,[{type:"paused"}];case"resume":return V.state=P.Started,V.startWhen=k??F,[{type:"resume"}];case"dispose":return V.state=P.Disposed,V.buffer=[],V.streamBuffer=i(),[{type:"disposed"}];case"loop":{let Q=k,X=V.state;if(Q&&(X===P.Scheduled||X===P.Started))V.stopWhen=Number.MAX_SAFE_INTEGER,V.duration=Number.MAX_SAFE_INTEGER;if(V.loop=Q,Q)n(V,T);return[]}case"loopStart":return V.loopStart=k,[];case"loopEnd":return V.loopEnd=k,[];case"loopCrossfade":return V.loopCrossfade=k,[];case"playhead":return V.playhead=Math.floor(k),[];case"fadeIn":return V.fadeInDuration=k,[];case"fadeOut":return V.fadeOutDuration=k,[];case"toggleGain":return V.enableGain=k??!V.enableGain,[];case"togglePan":return V.enablePan=k??!V.enablePan,[];case"toggleLowpass":return V.enableLowpass=k??!V.enableLowpass,[];case"toggleHighpass":return V.enableHighpass=k??!V.enableHighpass,[];case"toggleDetune":return V.enableDetune=k??!V.enableDetune,[];case"togglePlaybackRate":return V.enablePlaybackRate=k??!V.enablePlaybackRate,[];case"toggleFadeIn":return V.enableFadeIn=k??!V.enableFadeIn,[];case"toggleFadeOut":return V.enableFadeOut=k??!V.enableFadeOut,[];case"toggleLoopStart":return V.enableLoopStart=k??!V.enableLoopStart,[];case"toggleLoopEnd":return V.enableLoopEnd=k??!V.enableLoopEnd,[];case"toggleLoopCrossfade":return V.enableLoopCrossfade=k??!V.enableLoopCrossfade,[];case"logState":return[]}return[]}function j0(V,A,F,T,J){let k=[],Q=V.state;if(Q===P.Disposed)return{keepAlive:!1,messages:k};if(y0(V),Q===P.Initial)return{keepAlive:!0,messages:k};if(Q===P.Ended)return h(A[0]),{keepAlive:!0,messages:k};if(Q===P.Scheduled)if(T.currentTime>=V.startWhen)Q=V.state=P.Started,k.push({type:"started"});else return h(A[0]),{keepAlive:!0,messages:k};else if(Q===P.Paused){if(T.currentTime>V.pauseWhen)return h(A[0]),{keepAlive:!0,messages:k}}if(T.currentTime>V.stopWhen)return h(A[0]),V.state=P.Ended,k.push({type:"ended"}),V.playedSamples=0,{keepAlive:!0,messages:k};let X=A[0],U=o(V);if(U===0)return h(X),{keepAlive:!0,messages:k};let{playbackRate:$,detune:M,lowpass:Y,highpass:j,gain:z,pan:C}=F,{buffer:N,loopStart:K,loopEnd:w,loopCrossfade:v,stopWhen:Z,playedSamples:E,enableLowpass:O,enableHighpass:R,enableGain:d,enablePan:b,enableDetune:c,enableFadeOut:g,enableFadeIn:s,enableLoopStart:t,enableLoopEnd:r,enableLoopCrossfade:p,playhead:D,fadeInDuration:A0,fadeOutDuration:F0}=V,K0=V.streamBuffer.streaming&&V.streamBuffer.committedLength<U,k0=V.loop&&!K0,u=Math.min(N.length,X.length),W0=V.duration*T.sampleRate,Z0=Math.floor(T.sampleRate*v),q0=Math.max(U-_,0),L=t?Math.min(Math.floor(K*T.sampleRate),q0):0,x=r?Math.min(Math.floor(w*T.sampleRate),U):U,N0=x-L,T0=c&&M.length>0&&M[0]!==0,l=$;if(T0){let G=Math.max($.length,M.length,_);l=new Float32Array(G);for(let W=0;W<G;W++){let I=$[W]??$[$.length-1],H=M[W]??M[M.length-1];l[W]=I*2**(H/1200)}}let J0=V.enablePlaybackRate||T0,O0=J0&&l.length>0&&l.every((G)=>G===0);if(V.streamBuffer.streaming&&!V.streamBuffer.streamEnded&&!V.streamBuffer.lowWaterNotified&&V.streamBuffer.committedLength-Math.floor(D)<V.streamBuffer.lowWaterThreshold)k.push({type:"bufferLowWater",data:{playhead:Math.floor(D),committedLength:V.streamBuffer.committedLength}}),V.streamBuffer.lowWaterNotified=!0;if(O0){h(X);for(let G=1;G<A.length;G++)M0(X,A[G]);return{keepAlive:!0,messages:k}}let Q0={bufferLength:U,loop:k0,playhead:D,loopStartSamples:L,loopEndSamples:x,durationSamples:W0,playbackRates:l},{indexes:a,ended:U0,looped:X0,playhead:Y0}=J0?x0(Q0):L0(Q0),f=a.find((G)=>G>=V.streamBuffer.committedLength&&G<U);if(f!==void 0&&!V.streamBuffer.streamEnded&&V.streamBuffer.lastUnderrunSample!==f)k.push({type:"bufferUnderrun",data:{playhead:Math.floor(D),committedLength:V.streamBuffer.committedLength,requestedSample:f}}),V.streamBuffer.lastUnderrunSample=f;else if(f===void 0)V.streamBuffer.lastUnderrunSample=null;m0(X,N,a);let m=Math.min(Math.floor(v*T.sampleRate),N0),D0=k0&&D>L&&D<x,C0=p&&Z0>0&&U>_;if(D0&&C0){{let G=L+m;if(m>0&&D>L&&D<G){let W=D-L,I=Math.min(Math.floor(G-D),_);for(let H=0;H<I;H++){let B=(W+H)/m,S=Math.cos(Math.PI*B/2),q=Math.floor(x-m+W+H);if(q>=0&&q<U)for(let y=0;y<u;y++)X[y][H]+=N[y][q]*S}}}{let G=x-m;if(m>0&&D>G&&D<x){let W=D-G,I=Math.min(Math.floor(x-D),_);for(let H=0;H<I;H++){let B=(W+H)/m,S=Math.sin(Math.PI*B/2),q=Math.floor(L+W+H);if(q>=0&&q<U)for(let y=0;y<u;y++)X[y][H]+=N[y][q]*S}}}}if(s&&A0>0){let G=Math.floor(A0*T.sampleRate),W=G-E;if(W>0){let I=Math.min(W,_);for(let H=0;H<I;H++){let B=(E+H)/G,S=B*B*B;for(let q=0;q<u;q++)X[q][H]*=S}}}if(g&&F0>0){let G=Math.floor(F0*T.sampleRate),W=Math.floor(T.sampleRate*(Z-T.currentTime));if(W<G+_)for(let I=0;I<_;I++){let H=W-I;if(H>=G)continue;let B=H<=0?0:H/G,S=B*B*B;for(let q=0;q<u;q++)X[q][I]*=S}}if(O)d0(X,Y,T.sampleRate,J.lowpass);if(R)u0(X,j,T.sampleRate,J.highpass);if(d)g0(X,z);if(u===1)S0(X);if(b)h0(X,C);if(X0)V.timesLooped++,k.push({type:"looped",data:V.timesLooped});if(U0)V.state=P.Ended,k.push({type:"ended"});V.playedSamples+=a.length,V.playhead=Y0;let $0=c0(X);if($0>0)return console.log({numNans:$0,indexes:a,playhead:Y0,ended:U0,looped:X0,sourceLength:U}),{keepAlive:!0,messages:k};for(let G=1;G<A.length;G++)M0(X,A[G]);return{keepAlive:!0,messages:k}}class z0 extends AudioWorkletProcessor{static get parameterDescriptors(){return[{name:"playbackRate",automationRate:"a-rate",defaultValue:1},{name:"detune",automationRate:"a-rate",defaultValue:0},{name:"gain",automationRate:"a-rate",defaultValue:1,minValue:0},{name:"pan",automationRate:"a-rate",defaultValue:0},{name:"highpass",automationRate:"a-rate",defaultValue:20,minValue:20,maxValue:20000},{name:"lowpass",automationRate:"a-rate",defaultValue:20000,minValue:20,maxValue:20000}]}properties;filterState={lowpass:V0(),highpass:V0()};lastFrameTime=0;constructor(V){super(V);this.properties=v0(V?.processorOptions,sampleRate),this.port.onmessage=(A)=>{let F=P0(this.properties,A.data,currentTime,sampleRate);for(let T of F)this.port.postMessage(T);if(this.properties.state===P.Disposed)this.port.close()}}process(V,A,F){try{let T=j0(this.properties,A,F,{currentTime,currentFrame,sampleRate},this.filterState);for(let k of T.messages)this.port.postMessage(k);let J=currentTime-this.lastFrameTime;return this.lastFrameTime=currentTime,this.port.postMessage({type:"frame",data:[currentTime,currentFrame,Math.floor(this.properties.playhead),J*1000]}),T.keepAlive}catch(T){return this.port.postMessage({type:"processorError",data:{error:String(T),state:this.properties.state,bufferChannels:this.properties.buffer?.length,bufferLength:this.properties.buffer?.[0]?.length,paramKeys:Object.keys(F),hasPlaybackRate:!!F.playbackRate,hasDetune:!!F.detune,hasGain:!!F.gain,hasPan:!!F.pan,outputChannels:A[0]?.length}}),!0}}}registerProcessor("ClipProcessor",z0);
1
+ var j={Initial:0,Started:1,Stopped:2,Paused:3,Scheduled:4,Ended:5,Disposed:6};var _=128;function i(V=[]){let A=V[0]?.length??0,F=A>0;return{totalLength:F?A:null,committedLength:F?A:0,streamEnded:F,streaming:!1,writtenSpans:F?[{startSample:0,endSample:A}]:[],pendingWrites:[],lowWaterThreshold:_*4,lowWaterNotified:!1,lastUnderrunSample:null}}function H0(V){return V[0]?.length??0}function o(V){return V.streamBuffer.totalLength??H0(V.buffer)}function P0(V,A){return Array.from({length:V},()=>new Float32Array(A))}function E0(V,A){let F=[...V,A].sort((T,k)=>T.startSample-k.startSample),M=[];for(let T of F){let k=M[M.length-1];if(!k||T.startSample>k.endSample){M.push({...T});continue}k.endSample=Math.max(k.endSample,T.endSample)}return M}function I0(V){let A=0;for(let F of V){if(F.startSample>A)break;A=Math.max(A,F.endSample)}return A}function w0(V,A){if(V.committedLength-Math.floor(A)>=V.lowWaterThreshold)V.lowWaterNotified=!1}function R0(V,A,F){let M=H0(V.buffer),T=V.buffer.length;if(M>=F&&T>=A)return;let k=Math.max(M,F),J=Math.max(T,A),U=P0(J,k);for(let Q=0;Q<T;Q++)U[Q].set(V.buffer[Q].subarray(0,M));if(V.buffer=U,V.streamBuffer.totalLength==null||V.streamBuffer.totalLength<k)V.streamBuffer.totalLength=k}function B0(V,A){let F=Math.max(Math.floor(A.startSample),0),M=A.channelData[0]?.length??0,T=A.totalLength??null,k=Math.max(F+M,T??0);R0(V,Math.max(A.channelData.length,V.buffer.length,1),k);for(let J=0;J<A.channelData.length;J++)V.buffer[J].set(A.channelData[J],F);if(T!=null)V.streamBuffer.totalLength=T;if(M>0)V.streamBuffer.writtenSpans=E0(V.streamBuffer.writtenSpans,{startSample:F,endSample:F+M}),V.streamBuffer.committedLength=I0(V.streamBuffer.writtenSpans);if(A.streamEnded===!0)V.streamBuffer.streamEnded=!0;w0(V.streamBuffer,V.playhead)}function y0(V){if(V.streamBuffer.pendingWrites.length===0)return;for(let A of V.streamBuffer.pendingWrites)B0(V,A);V.streamBuffer.pendingWrites=[]}function _0(V,A){V.buffer=A,V.streamBuffer=i(A)}function j0(V={},A){let{buffer:F=[],streamBuffer:M=i(F),duration:T=-1,loop:k=!1,loopStart:J=0,loopEnd:U=(F[0]?.length??0)/A,loopCrossfade:Q=0,playhead:Y=0,offset:$=0,startWhen:X=0,stopWhen:v=0,pauseWhen:z=0,resumeWhen:C=0,playedSamples:N=0,state:K=j.Initial,timesLooped:w=0,fadeInDuration:P=0,fadeOutDuration:Z=0,enableFadeIn:E=P>0,enableFadeOut:O=Z>0,enableLoopStart:R=!0,enableLoopEnd:d=!0,enableLoopCrossfade:b=Q>0,enableHighpass:c=!0,enableLowpass:g=!0,enableGain:s=!0,enablePan:t=!0,enableDetune:r=!0,enablePlaybackRate:p=!0}=V;return{buffer:F,streamBuffer:M,loop:k,loopStart:J,loopEnd:U,loopCrossfade:Q,duration:T,playhead:Y,offset:$,startWhen:X,stopWhen:v,pauseWhen:z,resumeWhen:C,playedSamples:N,state:K,timesLooped:w,fadeInDuration:P,fadeOutDuration:Z,enableFadeIn:E,enableFadeOut:O,enableLoopStart:R,enableLoopEnd:d,enableHighpass:c,enableLowpass:g,enableGain:s,enablePan:t,enableDetune:r,enablePlaybackRate:p,enableLoopCrossfade:b}}function b0(V,A){return o(V)/A}function n(V,A){let F=b0(V,A);if(F<=0){V.loopStart=0,V.loopEnd=0;return}if(!Number.isFinite(V.loopStart)||V.loopStart<0)V.loopStart=0;if(V.loopStart>=F)V.loopStart=0;if(!Number.isFinite(V.loopEnd)||V.loopEnd<=V.loopStart||V.loopEnd>F)V.loopEnd=F}function e(V,A,F){if(A===void 0)return V.offset=0,0;if(A<0)return e(V,o(V)+A,F);if(A>(o(V)||1)-1)return e(V,o(V)%A,F);let M=Math.floor(A*F);return V.offset=M,M}function L0(V){let{playhead:A,bufferLength:F,loop:M,loopStartSamples:T,loopEndSamples:k}=V,J=128;if(!M&&A+128>F)J=Math.max(F-A,0);let U=Array(J);if(!M){for(let X=0,v=A;X<J;X++,v++)U[X]=v;let $=A+J;return{playhead:$,indexes:U,looped:!1,ended:$>=F}}let Q=A,Y=!1;for(let $=0;$<J;$++,Q++){if(Q>=k)Q=T+(Q-k),Y=!0;U[$]=Q}return{indexes:U,looped:Y,ended:!1,playhead:Q}}function x0(V){let{playhead:A,bufferLength:F,loop:M,loopStartSamples:T,loopEndSamples:k,playbackRates:J}=V,U=128;if(!M&&A+128>F)U=Math.max(F-A,0);let Q=Array(U),Y=A,$=!1;if(M){for(let X=0;X<U;X++){Q[X]=Math.min(Math.max(Math.floor(Y),0),F-1);let v=J[X]??J[0]??1;if(Y+=v,v>=0&&(Y>k||Y>F))Y=T,$=!0;else if(v<0&&(Y<T||Y<0))Y=k,$=!0}return{playhead:Y,indexes:Q,looped:$,ended:!1}}for(let X=0;X<U;X++)Q[X]=Math.min(Math.max(Math.floor(Y),0),F-1),Y+=J[X]??J[0]??1;return{playhead:Y,indexes:Q,looped:!1,ended:Y>=F||Y<0}}function m0(V,A,F){let M=Math.min(V.length,A.length);for(let T=0;T<F.length;T++)for(let k=0;k<M;k++)V[k][T]=A[k][F[T]];for(let T=M;T<V.length;T++)for(let k=0;k<V[T].length;k++)V[T][k]=0;for(let T=F.length;T<V[0].length;T++)for(let k=0;k<M;k++)V[k][T]=0}function h(V){for(let A=0;A<V.length;A++)for(let F=0;F<V[A].length;F++)V[A][F]=0}function S0(V){if(V.length>=2)for(let A=0;A<V[0].length;A++)V[1][A]=V[0][A];else{let A=new Float32Array(V[0].length);for(let F=0;F<V[0].length;F++)A[F]=V[0][F];V.push(A)}}function G0(V,A){for(let F=A.length;F<V.length;F++)A[F]=new Float32Array(V[F].length);for(let F=0;F<V.length;F++)for(let M=0;M<V[F].length;M++)A[F][M]=V[F][M]}function c0(V){let A=0;for(let F=0;F<V.length;F++)for(let M=0;M<V[F].length;M++)if(Number.isNaN(V[F][M]))A++,V[F][M]=0;return A}function V0(){return[{x_1:0,x_2:0,y_1:0,y_2:0},{x_1:0,x_2:0,y_1:0,y_2:0}]}function g0(V,A){if(A.length===1){let M=A[0];if(M===1)return;for(let T of V)for(let k=0;k<T.length;k++)T[k]*=M;return}let F=A[0];for(let M of V)for(let T=0;T<M.length;T++)F=A[T]??F,M[T]*=F}function h0(V,A){let F=A[0];for(let M=0;M<V[0].length;M++){F=A[M]??F;let T=F<=0?1:1-F,k=F>=0?1:1+F;V[0][M]*=T,V[1][M]*=k}}function d0(V,A,F,M){for(let T=0;T<V.length;T++){let k=V[T],{x_1:J,x_2:U,y_1:Q,y_2:Y}=M[T]??{x_1:0,x_2:0,y_1:0,y_2:0};if(A.length===1){let $=A[0];if($>=20000)return;let X=2*Math.PI*$/F,v=Math.sin(X)/2,z=(1-Math.cos(X))/2,C=1-Math.cos(X),N=(1-Math.cos(X))/2,K=1+v,w=-2*Math.cos(X),P=1-v,Z=z/K,E=C/K,O=N/K,R=w/K,d=P/K;for(let b=0;b<k.length;b++){let c=k[b],g=Z*c+E*J+O*U-R*Q-d*Y;U=J,J=c,Y=Q,Q=g,k[b]=g}}else{let $=A[0];for(let X=0;X<k.length;X++){let v=A[X]??$,z=2*Math.PI*v/F,C=Math.sin(z)/2,N=(1-Math.cos(z))/2,K=1-Math.cos(z),w=(1-Math.cos(z))/2,P=1+C,Z=-2*Math.cos(z),E=1-C,O=k[X],R=N/P*O+K/P*J+w/P*U-Z/P*Q-E/P*Y;U=J,J=O,Y=Q,Q=R,k[X]=R}}M[T]={x_1:J,x_2:U,y_1:Q,y_2:Y}}}function u0(V,A,F,M){for(let T=0;T<V.length;T++){let k=V[T],{x_1:J,x_2:U,y_1:Q,y_2:Y}=M[T]??{x_1:0,x_2:0,y_1:0,y_2:0};if(A.length===1){let $=A[0];if($<=20)return;let X=2*Math.PI*$/F,v=Math.sin(X)/2,z=(1+Math.cos(X))/2,C=-(1+Math.cos(X)),N=(1+Math.cos(X))/2,K=1+v,w=-2*Math.cos(X),P=1-v;for(let Z=0;Z<k.length;Z++){let E=k[Z],O=z/K*E+C/K*J+N/K*U-w/K*Q-P/K*Y;U=J,J=E,Y=Q,Q=O,k[Z]=O}}else{let $=A[0];for(let X=0;X<k.length;X++){let v=A[X]??$,z=2*Math.PI*v/F,C=Math.sin(z)/2,N=(1+Math.cos(z))/2,K=-(1+Math.cos(z)),w=(1+Math.cos(z))/2,P=1+C,Z=-2*Math.cos(z),E=1-C,O=k[X],R=N/P*O+K/P*J+w/P*U-Z/P*Q-E/P*Y;U=J,J=O,Y=Q,Q=R,k[X]=R}}M[T]={x_1:J,x_2:U,y_1:Q,y_2:Y}}}function A0(V,A,F,M){let{type:T,data:k}=A;switch(T){case"buffer":return _0(V,k),n(V,M),[];case"bufferInit":{let J=k;return V.buffer=P0(J.channels,J.totalLength),V.streamBuffer={...i(),totalLength:J.totalLength,streamEnded:!1,streaming:J.streaming??!0},n(V,M),[]}case"bufferRange":return V.streamBuffer.pendingWrites.push(k),[];case"bufferEnd":{let J=k;if(J?.totalLength!=null)V.streamBuffer.totalLength=J.totalLength;return V.streamBuffer.streamEnded=!0,[]}case"bufferReset":return V.buffer=[],V.streamBuffer=i(),n(V,M),[];case"start":V.timesLooped=0;{let J=k;if(V.duration=J?.duration??-1,V.duration===-1)V.duration=V.loop?Number.MAX_SAFE_INTEGER:(V.buffer[0]?.length??0)/M;e(V,J?.offset,M),n(V,M),V.playhead=V.offset,V.startWhen=J?.when??F,V.stopWhen=V.startWhen+V.duration,V.playedSamples=0,V.state=j.Scheduled}return[{type:"scheduled"}];case"stop":if(V.state===j.Ended||V.state===j.Initial)return[];return V.stopWhen=k??V.stopWhen,V.state=j.Stopped,[{type:"stopped"}];case"pause":return V.state=j.Paused,V.pauseWhen=k??F,[{type:"paused"}];case"resume":return V.state=j.Started,V.startWhen=k??F,[{type:"resume"}];case"dispose":return V.state=j.Disposed,V.buffer=[],V.streamBuffer=i(),[{type:"disposed"}];case"loop":{let J=k,U=V.state;if(J&&(U===j.Scheduled||U===j.Started))V.stopWhen=Number.MAX_SAFE_INTEGER,V.duration=Number.MAX_SAFE_INTEGER;if(V.loop=J,J)n(V,M);return[]}case"loopStart":return V.loopStart=k,[];case"loopEnd":return V.loopEnd=k,[];case"loopCrossfade":return V.loopCrossfade=k,V.enableLoopCrossfade=V.loopCrossfade>0,[];case"playhead":return V.playhead=Math.floor(k),[];case"fadeIn":return V.fadeInDuration=k,V.enableFadeIn=V.fadeInDuration>0,[];case"fadeOut":return V.fadeOutDuration=k,V.enableFadeOut=V.fadeOutDuration>0,[];case"toggleGain":return V.enableGain=k??!V.enableGain,[];case"togglePan":return V.enablePan=k??!V.enablePan,[];case"toggleLowpass":return V.enableLowpass=k??!V.enableLowpass,[];case"toggleHighpass":return V.enableHighpass=k??!V.enableHighpass,[];case"toggleDetune":return V.enableDetune=k??!V.enableDetune,[];case"togglePlaybackRate":return V.enablePlaybackRate=k??!V.enablePlaybackRate,[];case"toggleFadeIn":return V.enableFadeIn=k??!V.enableFadeIn,[];case"toggleFadeOut":return V.enableFadeOut=k??!V.enableFadeOut,[];case"toggleLoopStart":return V.enableLoopStart=k??!V.enableLoopStart,[];case"toggleLoopEnd":return V.enableLoopEnd=k??!V.enableLoopEnd,[];case"toggleLoopCrossfade":return V.enableLoopCrossfade=k??!V.enableLoopCrossfade,[];case"logState":return[]}return[]}function v0(V,A,F,M,T){let k=[],J=V.state;if(J===j.Disposed)return{keepAlive:!1,messages:k};if(y0(V),J===j.Initial)return{keepAlive:!0,messages:k};if(J===j.Ended)return h(A[0]),{keepAlive:!0,messages:k};if(J===j.Scheduled)if(M.currentTime>=V.startWhen)J=V.state=j.Started,k.push({type:"started"});else return h(A[0]),{keepAlive:!0,messages:k};else if(J===j.Paused){if(M.currentTime>V.pauseWhen)return h(A[0]),{keepAlive:!0,messages:k}}if(M.currentTime>V.stopWhen)return h(A[0]),V.state=j.Ended,k.push({type:"ended"}),V.playedSamples=0,{keepAlive:!0,messages:k};let U=A[0],Q=o(V);if(Q===0)return h(U),{keepAlive:!0,messages:k};let{playbackRate:Y,detune:$,lowpass:X,highpass:v,gain:z,pan:C}=F,{buffer:N,loopStart:K,loopEnd:w,loopCrossfade:P,stopWhen:Z,playedSamples:E,enableLowpass:O,enableHighpass:R,enableGain:d,enablePan:b,enableDetune:c,enableFadeOut:g,enableFadeIn:s,enableLoopStart:t,enableLoopEnd:r,enableLoopCrossfade:p,playhead:D,fadeInDuration:F0,fadeOutDuration:k0}=V,K0=V.streamBuffer.streaming&&V.streamBuffer.committedLength<Q,M0=V.loop&&!K0,u=Math.min(N.length,U.length),W0=V.duration*M.sampleRate,Z0=Math.floor(M.sampleRate*P),q0=Math.max(Q-_,0),L=t?Math.min(Math.floor(K*M.sampleRate),q0):0,x=r?Math.min(Math.floor(w*M.sampleRate),Q):Q,N0=x-L,T0=c&&$.length>0&&$[0]!==0,l=Y;if(T0){let G=Math.max(Y.length,$.length,_);l=new Float32Array(G);for(let W=0;W<G;W++){let I=Y[W]??Y[Y.length-1],H=$[W]??$[$.length-1];l[W]=I*2**(H/1200)}}let J0=V.enablePlaybackRate||T0,O0=J0&&l.length>0&&l.every((G)=>G===0);if(V.streamBuffer.streaming&&!V.streamBuffer.streamEnded&&!V.streamBuffer.lowWaterNotified&&V.streamBuffer.committedLength-Math.floor(D)<V.streamBuffer.lowWaterThreshold)k.push({type:"bufferLowWater",data:{playhead:Math.floor(D),committedLength:V.streamBuffer.committedLength}}),V.streamBuffer.lowWaterNotified=!0;if(O0){h(U);for(let G=1;G<A.length;G++)G0(U,A[G]);return{keepAlive:!0,messages:k}}let Q0={bufferLength:Q,loop:M0,playhead:D,loopStartSamples:L,loopEndSamples:x,durationSamples:W0,playbackRates:l},{indexes:a,ended:U0,looped:X0,playhead:Y0}=J0?x0(Q0):L0(Q0),f=a.find((G)=>G>=V.streamBuffer.committedLength&&G<Q);if(f!==void 0&&!V.streamBuffer.streamEnded&&V.streamBuffer.lastUnderrunSample!==f)k.push({type:"bufferUnderrun",data:{playhead:Math.floor(D),committedLength:V.streamBuffer.committedLength,requestedSample:f}}),V.streamBuffer.lastUnderrunSample=f;else if(f===void 0)V.streamBuffer.lastUnderrunSample=null;m0(U,N,a);let m=Math.min(Math.floor(P*M.sampleRate),N0),D0=M0&&D>L&&D<x,C0=p&&Z0>0&&Q>_;if(D0&&C0){{let G=L+m;if(m>0&&D>L&&D<G){let W=D-L,I=Math.min(Math.floor(G-D),_);for(let H=0;H<I;H++){let B=(W+H)/m,S=Math.cos(Math.PI*B/2),q=Math.floor(x-m+W+H);if(q>=0&&q<Q)for(let y=0;y<u;y++)U[y][H]+=N[y][q]*S}}}{let G=x-m;if(m>0&&D>G&&D<x){let W=D-G,I=Math.min(Math.floor(x-D),_);for(let H=0;H<I;H++){let B=(W+H)/m,S=Math.sin(Math.PI*B/2),q=Math.floor(L+W+H);if(q>=0&&q<Q)for(let y=0;y<u;y++)U[y][H]+=N[y][q]*S}}}}if(s&&F0>0){let G=Math.floor(F0*M.sampleRate),W=G-E;if(W>0){let I=Math.min(W,_);for(let H=0;H<I;H++){let B=(E+H)/G,S=B*B*B;for(let q=0;q<u;q++)U[q][H]*=S}}}if(g&&k0>0){let G=Math.floor(k0*M.sampleRate),W=Math.floor(M.sampleRate*(Z-M.currentTime));if(W<G+_)for(let I=0;I<_;I++){let H=W-I;if(H>=G)continue;let B=H<=0?0:H/G,S=B*B*B;for(let q=0;q<u;q++)U[q][I]*=S}}if(O)d0(U,X,M.sampleRate,T.lowpass);if(R)u0(U,v,M.sampleRate,T.highpass);if(d)g0(U,z);if(u===1)S0(U);if(b)h0(U,C);if(X0)V.timesLooped++,k.push({type:"looped",data:V.timesLooped});if(U0)V.state=j.Ended,k.push({type:"ended"});V.playedSamples+=a.length,V.playhead=Y0;let $0=c0(U);if($0>0)return console.log({numNans:$0,indexes:a,playhead:Y0,ended:U0,looped:X0,sourceLength:Q}),{keepAlive:!0,messages:k};for(let G=1;G<A.length;G++)G0(U,A[G]);return{keepAlive:!0,messages:k}}class z0 extends AudioWorkletProcessor{static get parameterDescriptors(){return[{name:"playbackRate",automationRate:"a-rate",defaultValue:1},{name:"detune",automationRate:"a-rate",defaultValue:0},{name:"gain",automationRate:"a-rate",defaultValue:1,minValue:0},{name:"pan",automationRate:"a-rate",defaultValue:0},{name:"highpass",automationRate:"a-rate",defaultValue:20,minValue:20,maxValue:20000},{name:"lowpass",automationRate:"a-rate",defaultValue:20000,minValue:20,maxValue:20000}]}properties;filterState={lowpass:V0(),highpass:V0()};lastFrameTime=0;constructor(V){super(V);this.properties=j0(V?.processorOptions,sampleRate),this.port.onmessage=(A)=>{if(A.data.type==="transferPort"){let M=A.data.data;M.onmessage=(T)=>{let k=A0(this.properties,T.data,currentTime,sampleRate);for(let J of k)this.port.postMessage(J)};return}let F=A0(this.properties,A.data,currentTime,sampleRate);for(let M of F)this.port.postMessage(M);if(this.properties.state===j.Disposed)this.port.close()}}process(V,A,F){try{let M=v0(this.properties,A,F,{currentTime,currentFrame,sampleRate},this.filterState);for(let k of M.messages)this.port.postMessage(k);let T=currentTime-this.lastFrameTime;return this.lastFrameTime=currentTime,this.port.postMessage({type:"frame",data:[currentTime,currentFrame,Math.floor(this.properties.playhead),T*1000]}),M.keepAlive}catch(M){return this.port.postMessage({type:"processorError",data:{error:String(M),state:this.properties.state,bufferChannels:this.properties.buffer?.length,bufferLength:this.properties.buffer?.[0]?.length,paramKeys:Object.keys(F),hasPlaybackRate:!!F.playbackRate,hasDetune:!!F.detune,hasGain:!!F.gain,hasPan:!!F.pan,outputChannels:A[0]?.length}}),!0}}}registerProcessor("ClipProcessor",z0);
2
2
 
3
- //# debugId=12FC7555EABD465B64756E2164756E21
3
+ //# debugId=78F68534B0A72CAE64756E2164756E21
4
4
  //# sourceMappingURL=processor.js.map
@@ -0,0 +1,25 @@
1
+ # Streaming Example (WebCodecs AudioDecoder + Worker)
2
+
3
+ Streams an MP3 file over HTTP, decodes it progressively using the WebCodecs
4
+ `AudioDecoder` API inside a Web Worker, and sends decoded audio directly to
5
+ the ClipNode processor via a `MessagePort`.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ fetch → ReadableStream → MP3 frame parser → AudioDecoder (Worker)
11
+ ↓ MessagePort
12
+ ClipProcessor (AudioWorklet)
13
+
14
+ AudioContext.destination
15
+ ```
16
+
17
+ The main thread only handles UI and transport controls.
18
+
19
+ ## Browser Support
20
+
21
+ Chrome 94+, Edge 94+, Firefox 130+, Safari 26+
22
+
23
+ ```sh
24
+ bun run dev
25
+ ```
@@ -0,0 +1,21 @@
1
+ // Build decode-worker.ts → generated/worker-code.ts (as an exported string constant)
2
+ const result = await Bun.build({
3
+ entrypoints: ["./decode-worker.ts"],
4
+ target: "browser",
5
+ minify: true,
6
+ });
7
+
8
+ if (!result.success) {
9
+ console.error("Worker build failed:", result.logs);
10
+ process.exit(1);
11
+ }
12
+
13
+ const code = await result.outputs[0].text();
14
+ const escaped = JSON.stringify(code);
15
+
16
+ await Bun.write(
17
+ "./generated/worker-code.ts",
18
+ `// AUTO-GENERATED — do not edit. Run \`bun run build:worker\` to regenerate.\nexport const workerCode = ${escaped};\n`,
19
+ );
20
+
21
+ console.log(`Built worker code (${code.length} bytes) → generated/worker-code.ts`);
@@ -0,0 +1,308 @@
1
+ // Decode Worker — runs fetch → MP3 demux → AudioDecoder off the main thread.
2
+ // Sends decoded Float32Array data directly to the ClipProcessor via a
3
+ // transferred MessagePort, bypassing the main thread for audio data.
4
+
5
+ // @ts-expect-error redeclare self as DedicatedWorkerGlobalScope
6
+ declare const self: DedicatedWorkerGlobalScope;
7
+
8
+ // ── MP3 frame parser ─────────────────────────────────────────────────
9
+
10
+ const BITRATES = [
11
+ 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
12
+ ] as const;
13
+ const SAMPLE_RATES = [44100, 48000, 32000, 0] as const;
14
+ const SAMPLES_PER_FRAME = 1152; // MPEG1 Layer III
15
+
16
+ interface Mp3FrameInfo {
17
+ offset: number;
18
+ size: number;
19
+ bitrate: number;
20
+ sampleRate: number;
21
+ channels: number;
22
+ }
23
+
24
+ interface ParseResult {
25
+ frames: Mp3FrameInfo[];
26
+ leftover: Uint8Array<ArrayBuffer>;
27
+ }
28
+
29
+ function parseMp3Frames(buf: Uint8Array): ParseResult {
30
+ const frames: Mp3FrameInfo[] = [];
31
+ let i = 0;
32
+
33
+ // Skip ID3v2 tag if present
34
+ if (
35
+ buf.length >= 10 &&
36
+ buf[0] === 0x49 &&
37
+ buf[1] === 0x44 &&
38
+ buf[2] === 0x33
39
+ ) {
40
+ const size =
41
+ ((buf[6] & 0x7f) << 21) |
42
+ ((buf[7] & 0x7f) << 14) |
43
+ ((buf[8] & 0x7f) << 7) |
44
+ (buf[9] & 0x7f);
45
+ i = 10 + size;
46
+ }
47
+
48
+ while (i + 4 <= buf.length) {
49
+ // Sync word: 11 set bits
50
+ if (buf[i] !== 0xff || (buf[i + 1] & 0xe0) !== 0xe0) {
51
+ i++;
52
+ continue;
53
+ }
54
+
55
+ const header = buf[i + 1];
56
+ const mpegVersion = (header >> 3) & 0x03;
57
+ const layer = (header >> 1) & 0x03;
58
+
59
+ // Only MPEG1 Layer III
60
+ if (mpegVersion !== 3 || layer !== 1) {
61
+ i++;
62
+ continue;
63
+ }
64
+
65
+ const bitrateIndex = (buf[i + 2] >> 4) & 0x0f;
66
+ const sampleRateIndex = (buf[i + 2] >> 2) & 0x03;
67
+ const padding = (buf[i + 2] >> 1) & 0x01;
68
+ const channelMode = (buf[i + 3] >> 6) & 0x03;
69
+
70
+ const bitrate = BITRATES[bitrateIndex] * 1000;
71
+ const sampleRate = SAMPLE_RATES[sampleRateIndex];
72
+
73
+ if (bitrate === 0 || sampleRate === 0) {
74
+ i++;
75
+ continue;
76
+ }
77
+
78
+ const frameSize =
79
+ Math.floor((144 * bitrate) / sampleRate) + padding;
80
+
81
+ if (i + frameSize > buf.length) {
82
+ // Incomplete frame — return as leftover
83
+ break;
84
+ }
85
+
86
+ frames.push({
87
+ offset: i,
88
+ size: frameSize,
89
+ bitrate,
90
+ sampleRate,
91
+ channels: channelMode === 3 ? 1 : 2,
92
+ });
93
+ i += frameSize;
94
+ }
95
+
96
+ return { frames, leftover: buf.slice(i) };
97
+ }
98
+
99
+ // ── Concat helper ────────────────────────────────────────────────────
100
+
101
+ function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
102
+ const out = new Uint8Array(a.length + b.length);
103
+ out.set(a);
104
+ out.set(b, a.length);
105
+ return out;
106
+ }
107
+
108
+ // ── Throttle stream ──────────────────────────────────────────────────
109
+
110
+ function createThrottleStream(bytesPerSec: number): TransformStream<Uint8Array, Uint8Array> {
111
+ let totalBytes = 0;
112
+ const startTime = performance.now();
113
+ return new TransformStream({
114
+ async transform(chunk, controller) {
115
+ totalBytes += chunk.length;
116
+ const elapsed = (performance.now() - startTime) / 1000;
117
+ const expected = totalBytes / bytesPerSec;
118
+ const delay = expected - elapsed;
119
+ if (delay > 0) {
120
+ await new Promise(resolve => setTimeout(resolve, delay * 1000));
121
+ }
122
+ controller.enqueue(chunk);
123
+ },
124
+ });
125
+ }
126
+
127
+ // ── Main worker entry ────────────────────────────────────────────────
128
+
129
+ let abortController: AbortController | null = null;
130
+
131
+ self.onmessage = (ev: MessageEvent) => {
132
+ const { type } = ev.data;
133
+ if (type === "init") {
134
+ const { port, url, throttle } = ev.data as {
135
+ port: MessagePort;
136
+ url: string;
137
+ throttle?: number;
138
+ };
139
+ abortController = new AbortController();
140
+ startStreaming(port, url, abortController.signal, throttle ?? 0);
141
+ } else if (type === "abort") {
142
+ abortController?.abort();
143
+ }
144
+ };
145
+
146
+ async function startStreaming(
147
+ processorPort: MessagePort,
148
+ url: string,
149
+ signal: AbortSignal,
150
+ throttle: number,
151
+ ) {
152
+ let totalBytes: number | null = null;
153
+ let bytesReceived = 0;
154
+ let samplesDecoded = 0;
155
+ let leftover = new Uint8Array(0);
156
+ let initialized = false;
157
+
158
+ const decoder = new AudioDecoder({
159
+ output(audioData: AudioData) {
160
+ const numFrames = audioData.numberOfFrames;
161
+ const numChannels = audioData.numberOfChannels;
162
+ const channelData: Float32Array[] = [];
163
+
164
+ for (let ch = 0; ch < numChannels; ch++) {
165
+ const dest = new Float32Array(numFrames);
166
+ audioData.copyTo(dest, { planeIndex: ch, format: "f32-planar" });
167
+ channelData.push(dest);
168
+ }
169
+ audioData.close();
170
+
171
+ if (!initialized) {
172
+ initialized = true;
173
+
174
+ // Estimate total samples from Content-Length if available
175
+ let estimatedTotalSamples: number | null = null;
176
+ if (totalBytes !== null) {
177
+ // Rough estimate; will be corrected by bufferEnd
178
+ estimatedTotalSamples = Math.ceil(
179
+ (totalBytes / 417) * SAMPLES_PER_FRAME,
180
+ ); // ~128kbps average frame size
181
+ }
182
+
183
+ processorPort.postMessage({
184
+ type: "bufferInit",
185
+ data: {
186
+ channels: numChannels,
187
+ totalLength: estimatedTotalSamples ?? 0,
188
+ streaming: true,
189
+ },
190
+ });
191
+
192
+ self.postMessage({ type: "info", sampleRate: audioData.sampleRate, channels: numChannels });
193
+ }
194
+
195
+ processorPort.postMessage({
196
+ type: "bufferRange",
197
+ data: {
198
+ startSample: samplesDecoded,
199
+ channelData,
200
+ },
201
+ });
202
+
203
+ samplesDecoded += numFrames;
204
+ self.postMessage({ type: "decoded", samplesDecoded });
205
+ },
206
+ error(e: DOMException) {
207
+ self.postMessage({ type: "error", message: e.message });
208
+ },
209
+ });
210
+
211
+ try {
212
+ const response = await fetch(url, { signal });
213
+ if (!response.ok) {
214
+ self.postMessage({
215
+ type: "error",
216
+ message: `Fetch failed: ${response.status} ${response.statusText}`,
217
+ });
218
+ return;
219
+ }
220
+ if (!response.body) {
221
+ self.postMessage({ type: "error", message: "Response has no body" });
222
+ return;
223
+ }
224
+
225
+ const contentLength = response.headers.get("content-length");
226
+ totalBytes = contentLength ? Number.parseInt(contentLength, 10) : null;
227
+
228
+ const body = throttle > 0
229
+ ? response.body.pipeThrough(createThrottleStream(throttle))
230
+ : response.body;
231
+ const reader = body.getReader();
232
+ let configuredDecoder = false;
233
+ let timestampUs = 0;
234
+
235
+ while (true) {
236
+ const { done, value } = await reader.read();
237
+ if (done) break;
238
+
239
+ bytesReceived += value.length;
240
+ self.postMessage({ type: "progress", bytesReceived, totalBytes });
241
+
242
+ const combined = leftover.length > 0 ? concat(leftover, value) : value;
243
+ const { frames, leftover: remainder } = parseMp3Frames(combined);
244
+ leftover = remainder;
245
+
246
+ for (const frame of frames) {
247
+ if (!configuredDecoder) {
248
+ decoder.configure({
249
+ codec: "mp3",
250
+ sampleRate: frame.sampleRate,
251
+ numberOfChannels: frame.channels,
252
+ });
253
+ configuredDecoder = true;
254
+ }
255
+
256
+ const frameData = combined.slice(
257
+ frame.offset,
258
+ frame.offset + frame.size,
259
+ );
260
+ decoder.decode(
261
+ new EncodedAudioChunk({
262
+ type: "key",
263
+ timestamp: timestampUs,
264
+ data: frameData,
265
+ }),
266
+ );
267
+ timestampUs += Math.round(
268
+ (SAMPLES_PER_FRAME / frame.sampleRate) * 1_000_000,
269
+ );
270
+ }
271
+ }
272
+
273
+ // Flush remaining decoded data
274
+ if (configuredDecoder) {
275
+ await decoder.flush();
276
+ } else {
277
+ self.postMessage({
278
+ type: "error",
279
+ message: "No MP3 frames found in the stream",
280
+ });
281
+ return;
282
+ }
283
+
284
+ // Signal end of buffer
285
+ processorPort.postMessage({
286
+ type: "bufferEnd",
287
+ data: { totalLength: samplesDecoded },
288
+ });
289
+
290
+ self.postMessage({ type: "done", samplesDecoded });
291
+ } catch (e: unknown) {
292
+ if (e instanceof DOMException && e.name === "AbortError") {
293
+ self.postMessage({ type: "aborted" });
294
+ } else {
295
+ self.postMessage({
296
+ type: "error",
297
+ message: e instanceof Error ? e.message : String(e),
298
+ });
299
+ }
300
+ } finally {
301
+ try {
302
+ decoder.close();
303
+ } catch {
304
+ // already closed
305
+ }
306
+ processorPort.close();
307
+ }
308
+ }
@@ -0,0 +1,211 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ClipNode – Streaming Example</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ body {
10
+ font-family: system-ui, sans-serif;
11
+ max-width: 640px;
12
+ margin: 2rem auto;
13
+ padding: 0 1rem;
14
+ color: #e2e8f0;
15
+ background: #0f172a;
16
+ }
17
+ h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
18
+ p.desc { color: #94a3b8; margin-top: 0; font-size: 0.9rem; }
19
+ label { display: block; margin-top: 1rem; font-size: 0.85rem; color: #94a3b8; }
20
+ input[type="text"] {
21
+ width: 100%; box-sizing: border-box;
22
+ padding: 0.5rem; margin-top: 0.25rem;
23
+ border: 1px solid #334155; border-radius: 6px;
24
+ background: #1e293b; color: #e2e8f0; font-size: 0.9rem;
25
+ }
26
+ select {
27
+ width: 100%; box-sizing: border-box;
28
+ padding: 0.5rem; margin-top: 0.25rem;
29
+ border: 1px solid #334155; border-radius: 6px;
30
+ background: #1e293b; color: #e2e8f0; font-size: 0.9rem;
31
+ }
32
+ .buttons { display: flex; gap: 0.5rem; margin-top: 1rem; }
33
+ button {
34
+ padding: 0.5rem 1.25rem; border: none; border-radius: 6px;
35
+ font-size: 0.9rem; cursor: pointer; font-weight: 600;
36
+ }
37
+ button:disabled { opacity: 0.4; cursor: default; }
38
+ #stream { background: #38bdf8; color: #0f172a; }
39
+ #pause { background: #facc15; color: #0f172a; }
40
+ #stop { background: #fb7185; color: #0f172a; }
41
+ .progress-wrap {
42
+ margin-top: 1rem; height: 6px; border-radius: 3px;
43
+ background: #1e293b; overflow: hidden;
44
+ }
45
+ .progress-bar {
46
+ height: 100%; width: 0%; border-radius: 3px;
47
+ background: #38bdf8; transition: width 0.15s;
48
+ }
49
+ #status {
50
+ margin-top: 0.75rem; font-size: 0.85rem; color: #94a3b8;
51
+ min-height: 1.2em;
52
+ }
53
+
54
+ /* ── Controls ─────────────────────────────────────── */
55
+ .controls {
56
+ margin-top: 1.5rem;
57
+ border-top: 1px solid #334155;
58
+ padding-top: 1rem;
59
+ }
60
+ .controls h2 {
61
+ font-size: 1rem; margin: 0 0 0.75rem;
62
+ color: #cbd5e1;
63
+ }
64
+ .control-row {
65
+ display: grid;
66
+ grid-template-columns: 110px 1fr 56px;
67
+ align-items: center;
68
+ gap: 0.5rem;
69
+ margin-bottom: 0.5rem;
70
+ }
71
+ .control-row label {
72
+ margin: 0; font-size: 0.82rem; color: #94a3b8;
73
+ text-align: right; padding-right: 0.25rem;
74
+ }
75
+ .control-row input[type="range"] {
76
+ width: 100%; accent-color: #38bdf8;
77
+ }
78
+ .control-row .val {
79
+ font-size: 0.8rem; color: #e2e8f0;
80
+ font-variant-numeric: tabular-nums;
81
+ text-align: left;
82
+ }
83
+ .control-group {
84
+ margin-bottom: 0.75rem;
85
+ }
86
+ .control-group summary {
87
+ cursor: pointer; font-size: 0.9rem;
88
+ color: #cbd5e1; font-weight: 600;
89
+ padding: 0.25rem 0;
90
+ }
91
+ .control-group[open] summary { margin-bottom: 0.5rem; }
92
+ .loop-toggle {
93
+ display: flex; align-items: center; gap: 0.5rem;
94
+ margin-bottom: 0.5rem;
95
+ }
96
+ .loop-toggle label { margin: 0; font-size: 0.85rem; }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <h1>ClipNode — Streaming</h1>
101
+ <p class="desc">
102
+ Stream &amp; decode an MP3 in a Web Worker, feeding decoded audio
103
+ directly to the AudioWorklet processor via MessagePort.
104
+ </p>
105
+
106
+ <label for="url">Audio URL</label>
107
+ <input type="text" id="url" value="https://jadujoel.github.io/web-audio-clip-node/example.mp3" />
108
+
109
+ <label for="throttle-select">Network Speed</label>
110
+ <select id="throttle-select">
111
+ <option value="0" selected>Normal (unlimited)</option>
112
+ <option value="204800">Slow (~200 KB/s)</option>
113
+ <option value="51200">Turtle (~50 KB/s)</option>
114
+ </select>
115
+
116
+ <div class="buttons">
117
+ <button id="stream">▶ Stream &amp; Play</button>
118
+ <button id="pause" disabled>⏸ Pause</button>
119
+ <button id="stop" disabled>■ Stop</button>
120
+ </div>
121
+
122
+ <div class="progress-wrap"><div class="progress-bar" id="progress"></div></div>
123
+ <p id="status">Idle</p>
124
+
125
+ <!-- ── Audio Controls ───────────────────────────────── -->
126
+ <div class="controls" id="controls" style="display:none">
127
+ <h2>Controls</h2>
128
+
129
+ <details class="control-group" open>
130
+ <summary>Volume &amp; Panning</summary>
131
+ <div class="control-row">
132
+ <label for="ctrl-gain">Gain</label>
133
+ <input type="range" id="ctrl-gain" min="0" max="2" step="0.01" value="1" />
134
+ <span class="val" id="val-gain">1.00</span>
135
+ </div>
136
+ <div class="control-row">
137
+ <label for="ctrl-pan">Pan</label>
138
+ <input type="range" id="ctrl-pan" min="-1" max="1" step="0.01" value="0" />
139
+ <span class="val" id="val-pan">C</span>
140
+ </div>
141
+ </details>
142
+
143
+ <details class="control-group" open>
144
+ <summary>Speed &amp; Pitch</summary>
145
+ <div class="control-row">
146
+ <label for="ctrl-rate">Playback Rate</label>
147
+ <input type="range" id="ctrl-rate" min="0.25" max="4" step="0.01" value="1" />
148
+ <span class="val" id="val-rate">1.00×</span>
149
+ </div>
150
+ <div class="control-row">
151
+ <label for="ctrl-detune">Detune</label>
152
+ <input type="range" id="ctrl-detune" min="-2400" max="2400" step="1" value="0" />
153
+ <span class="val" id="val-detune">0 ct</span>
154
+ </div>
155
+ </details>
156
+
157
+ <details class="control-group">
158
+ <summary>Filters</summary>
159
+ <div class="control-row">
160
+ <label for="ctrl-lowpass">Lowpass</label>
161
+ <input type="range" id="ctrl-lowpass" min="20" max="20000" step="1" value="20000" />
162
+ <span class="val" id="val-lowpass">20000 Hz</span>
163
+ </div>
164
+ <div class="control-row">
165
+ <label for="ctrl-highpass">Highpass</label>
166
+ <input type="range" id="ctrl-highpass" min="20" max="20000" step="1" value="20" />
167
+ <span class="val" id="val-highpass">20 Hz</span>
168
+ </div>
169
+ </details>
170
+
171
+ <details class="control-group">
172
+ <summary>Fades</summary>
173
+ <div class="control-row">
174
+ <label for="ctrl-fadein">Fade In</label>
175
+ <input type="range" id="ctrl-fadein" min="0" max="5" step="0.01" value="0" />
176
+ <span class="val" id="val-fadein">0.00 s</span>
177
+ </div>
178
+ <div class="control-row">
179
+ <label for="ctrl-fadeout">Fade Out</label>
180
+ <input type="range" id="ctrl-fadeout" min="0" max="5" step="0.01" value="0" />
181
+ <span class="val" id="val-fadeout">0.00 s</span>
182
+ </div>
183
+ </details>
184
+
185
+ <details class="control-group">
186
+ <summary>Loop</summary>
187
+ <div class="loop-toggle">
188
+ <input type="checkbox" id="ctrl-loop" checked />
189
+ <label for="ctrl-loop">Loop enabled</label>
190
+ </div>
191
+ <div class="control-row">
192
+ <label for="ctrl-loopstart">Loop Start</label>
193
+ <input type="range" id="ctrl-loopstart" min="0" max="120" step="0.01" value="0" />
194
+ <span class="val" id="val-loopstart">0.00 s</span>
195
+ </div>
196
+ <div class="control-row">
197
+ <label for="ctrl-loopend">Loop End</label>
198
+ <input type="range" id="ctrl-loopend" min="0" max="120" step="0.01" value="0" />
199
+ <span class="val" id="val-loopend">0.00 s</span>
200
+ </div>
201
+ <div class="control-row">
202
+ <label for="ctrl-crossfade">Crossfade</label>
203
+ <input type="range" id="ctrl-crossfade" min="0" max="5" step="0.01" value="0" />
204
+ <span class="val" id="val-crossfade">0.00 s</span>
205
+ </div>
206
+ </details>
207
+ </div>
208
+
209
+ <script type="module" src="./main.ts"></script>
210
+ </body>
211
+ </html>