@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.
- package/README.md +77 -32
- package/dist/audio/ClipNode.d.ts +2 -0
- package/dist/audio/ClipNode.js +6 -1
- package/dist/audio/processor-code.d.ts +1 -1
- package/dist/audio/processor-code.js +1 -1
- package/dist/audio/processor-kernel.js +3 -0
- package/dist/audio/processor.js +9 -0
- package/dist/audio/version.d.ts +1 -1
- package/dist/audio/version.js +1 -1
- package/dist/lib.bundle.js +3 -3
- package/dist/lib.bundle.js.map +3 -3
- package/dist/processor.js +2 -2
- package/dist/processor.js.map +4 -4
- package/examples/README.md +12 -4
- package/examples/cdn-vanilla/README.md +10 -6
- package/examples/cdn-vanilla/index.html +1065 -33
- package/examples/index.html +1 -0
- package/examples/self-hosted/public/processor.js +2 -2
- package/examples/streaming/README.md +25 -0
- package/examples/streaming/build-worker.ts +21 -0
- package/examples/streaming/decode-worker.ts +308 -0
- package/examples/streaming/index.html +211 -0
- package/examples/streaming/main.ts +276 -0
- package/examples/streaming/package.json +12 -0
- package/package.json +1 -1
package/examples/index.html
CHANGED
|
@@ -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=
|
|
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 & 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 & 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 & 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 & 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>
|