@jadujoel/web-audio-clip-node 0.1.5 → 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.
Files changed (49) hide show
  1. package/README.md +127 -36
  2. package/dist/audio/ClipNode.d.ts +2 -0
  3. package/dist/audio/ClipNode.js +7 -2
  4. package/dist/audio/processor-code.d.ts +1 -1
  5. package/dist/audio/processor-code.js +1 -1
  6. package/dist/audio/processor-kernel.js +4 -1
  7. package/dist/audio/processor.js +11 -2
  8. package/dist/audio/version.d.ts +1 -1
  9. package/dist/audio/version.js +1 -1
  10. package/dist/audio/workletUrl.js +2 -2
  11. package/dist/components/AudioControl.js +4 -4
  12. package/dist/components/ControlSection.js +1 -1
  13. package/dist/components/DetuneControl.js +2 -2
  14. package/dist/components/FilterControl.js +2 -2
  15. package/dist/components/GainControl.js +2 -2
  16. package/dist/components/PanControl.js +2 -2
  17. package/dist/components/PlaybackRateControl.js +2 -2
  18. package/dist/components/PlayheadSlider.js +2 -2
  19. package/dist/hooks/useClipNode.js +7 -7
  20. package/dist/lib-react.js +14 -14
  21. package/dist/lib.bundle.js +3 -3
  22. package/dist/lib.bundle.js.map +3 -3
  23. package/dist/lib.js +11 -11
  24. package/dist/processor.js +2 -2
  25. package/dist/processor.js.map +4 -4
  26. package/dist/store/clipStore.js +2 -2
  27. package/dist/styles.css.d.ts +3 -0
  28. package/examples/README.md +12 -4
  29. package/examples/cdn-vanilla/README.md +10 -6
  30. package/examples/cdn-vanilla/index.html +1065 -33
  31. package/examples/esm-bundler/package.json +1 -1
  32. package/examples/index.html +17 -0
  33. package/examples/react/README.md +1 -1
  34. package/examples/react/bun.lock +45 -0
  35. package/examples/react/src/App.tsx +56 -6
  36. package/examples/react/src/css.d.ts +1 -0
  37. package/examples/react/tsconfig.json +15 -0
  38. package/examples/self-hosted/package.json +2 -4
  39. package/examples/self-hosted/public/processor.js +4 -0
  40. package/examples/self-hosted/src/main.ts +1 -3
  41. package/examples/streaming/README.md +25 -0
  42. package/examples/streaming/build-worker.ts +21 -0
  43. package/examples/streaming/decode-worker.ts +308 -0
  44. package/examples/streaming/index.html +211 -0
  45. package/examples/streaming/main.ts +276 -0
  46. package/examples/streaming/package.json +12 -0
  47. package/package.json +6 -2
  48. package/examples/esm-bundler/bun.lock +0 -15
  49. package/examples/self-hosted/bun.lock +0 -15
@@ -7,6 +7,6 @@
7
7
  "build": "bun build ./src/main.ts --outdir=dist --minify"
8
8
  },
9
9
  "dependencies": {
10
- "@jadujoel/web-audio-clip-node": "latest"
10
+ "@jadujoel/web-audio-clip-node": "file:../.."
11
11
  }
12
12
  }
@@ -0,0 +1,17 @@
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>Examples</title>
7
+ </head>
8
+ <body>
9
+ <ul>
10
+ <li><a href="cdn-vanilla">Vanilla</a>
11
+ <li><a href="esm-bundler">ESM Bundler</a></li>
12
+ <li><a href="react">React</a></li>
13
+ <li><a href="self-hosted">Self Hosted</a></li>
14
+ <li><a href="streaming">Streaming</a></li>
15
+ </ul>
16
+ </body>
17
+ </html>
@@ -7,4 +7,4 @@ bun install
7
7
  bun index.html
8
8
  ```
9
9
 
10
- Uses `useClipNode` + `useClipControls` to wire everything up, and renders `<TransportButtons />` and `<AudioControl />` out of the box.
10
+ Uses `useClipNode` + `useClipControls` to wire up `TransportButtons`, `PlaybackRateControl`, and `GainControl` with valid props.
@@ -0,0 +1,45 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "react-example",
7
+ "dependencies": {
8
+ "@jadujoel/web-audio-clip-node": "latest",
9
+ "react": "^19",
10
+ "react-dom": "^19",
11
+ "zustand": "^5",
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "latest",
15
+ "@types/react": "^19",
16
+ "@types/react-dom": "^19",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@jadujoel/web-audio-clip-node": ["@jadujoel/web-audio-clip-node@0.1.5", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "zustand": ">=4" }, "optionalPeers": ["react", "react-dom", "zustand"] }, "sha512-q4aUslRnuRqscWh5W6GWnPC1H2HXArHbt7aeiQM+EbmlSmL9WmOHqfMIqaIi6ByzIZsrV395va1zKLoBKwiaHA=="],
22
+
23
+ "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
24
+
25
+ "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
26
+
27
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
28
+
29
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
30
+
31
+ "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
32
+
33
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
34
+
35
+ "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
36
+
37
+ "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
38
+
39
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
40
+
41
+ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
42
+
43
+ "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
44
+ }
45
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
- AudioControl,
2
+ GainControl,
3
+ PlaybackRateControl,
3
4
  TransportButtons,
4
5
  useClipControls,
5
6
  useClipNode,
@@ -8,13 +9,62 @@ import "@jadujoel/web-audio-clip-node/styles.css";
8
9
 
9
10
  export function App() {
10
11
  const controls = useClipControls();
11
- useClipNode({ ...controls });
12
+ const clip = useClipNode({
13
+ values: controls.values,
14
+ enabled: controls.enabled,
15
+ loop: controls.loop,
16
+ setValue: controls.setValue,
17
+ });
12
18
 
13
19
  return (
14
- <div style={{ maxWidth: 480, margin: "2rem auto", fontFamily: "system-ui, sans-serif" }}>
20
+ <main
21
+ style={{
22
+ maxWidth: 480,
23
+ margin: "2rem auto",
24
+ fontFamily: "system-ui, sans-serif",
25
+ display: "grid",
26
+ gap: "1rem",
27
+ }}
28
+ >
15
29
  <h1>ClipNode – React</h1>
16
- <TransportButtons />
17
- <AudioControl />
18
- </div>
30
+ <p>Load a sound, then drive ClipNode with the packaged React controls.</p>
31
+ <TransportButtons
32
+ nodeState={clip.nodeState}
33
+ onStart={clip.start}
34
+ onStop={clip.stop}
35
+ onPause={clip.pause}
36
+ onResume={clip.resume}
37
+ onDispose={clip.dispose}
38
+ onLog={clip.logState}
39
+ onLoadSound={clip.loadSound}
40
+ />
41
+ <PlaybackRateControl
42
+ value={controls.values.playbackRate}
43
+ defaultValue={1}
44
+ enabled={controls.enabled.playbackRate}
45
+ onChange={(value) => {
46
+ controls.setValue("playbackRate", value);
47
+ clip.applyValue("playbackRate", value);
48
+ }}
49
+ onToggle={(enabled) => {
50
+ controls.setEnabled("playbackRate", enabled);
51
+ clip.applyToggle("playbackRate", enabled);
52
+ }}
53
+ />
54
+ <GainControl
55
+ value={controls.values.gain}
56
+ defaultValue={0}
57
+ enabled={controls.enabled.gain}
58
+ onChange={(value) => {
59
+ controls.setValue("gain", value);
60
+ clip.applyValue("gain", value);
61
+ }}
62
+ onToggle={(enabled) => {
63
+ controls.setEnabled("gain", enabled);
64
+ clip.applyToggle("gain", enabled);
65
+ }}
66
+ />
67
+ {clip.statusMessage ? <p>{clip.statusMessage}</p> : null}
68
+ </main>
19
69
  );
20
70
  }
@@ -0,0 +1 @@
1
+ declare module "@jadujoel/web-audio-clip-node/styles.css";
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
12
+ "noEmit": true
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -3,11 +3,9 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
- "setup": "cp node_modules/@jadujoel/web-audio-clip-node/dist/processor.js public/processor.js",
6
+ "setup": "cp ../../dist/processor.js public/processor.js",
7
7
  "dev": "bun index.html",
8
8
  "build": "bun build ./src/main.ts --outdir=dist --minify"
9
9
  },
10
- "dependencies": {
11
- "@jadujoel/web-audio-clip-node": "^0.1.1"
12
- }
10
+ "dependencies": {}
13
11
  }
@@ -0,0 +1,4 @@
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
+
3
+ //# debugId=78F68534B0A72CAE64756E2164756E21
4
+ //# sourceMappingURL=processor.js.map
@@ -28,9 +28,7 @@ document.getElementById("play")!.addEventListener("click", async () => {
28
28
  // Load the processor from your own server (public/processor.js)
29
29
  const processorUrl = getProcessorModuleUrl(window.location.href);
30
30
  await ctx.audioWorklet.addModule(processorUrl);
31
- clip = new ClipNode(ctx, {
32
- processorOptions: { sampleRate: ctx.sampleRate },
33
- });
31
+ clip = new ClipNode(ctx);
34
32
  clip.connect(ctx.destination);
35
33
  clip.buffer = createToneBuffer(ctx);
36
34
  }
@@ -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
+ }