@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.
- package/README.md +127 -36
- package/dist/audio/ClipNode.d.ts +2 -0
- package/dist/audio/ClipNode.js +7 -2
- package/dist/audio/processor-code.d.ts +1 -1
- package/dist/audio/processor-code.js +1 -1
- package/dist/audio/processor-kernel.js +4 -1
- package/dist/audio/processor.js +11 -2
- package/dist/audio/version.d.ts +1 -1
- package/dist/audio/version.js +1 -1
- package/dist/audio/workletUrl.js +2 -2
- package/dist/components/AudioControl.js +4 -4
- package/dist/components/ControlSection.js +1 -1
- package/dist/components/DetuneControl.js +2 -2
- package/dist/components/FilterControl.js +2 -2
- package/dist/components/GainControl.js +2 -2
- package/dist/components/PanControl.js +2 -2
- package/dist/components/PlaybackRateControl.js +2 -2
- package/dist/components/PlayheadSlider.js +2 -2
- package/dist/hooks/useClipNode.js +7 -7
- package/dist/lib-react.js +14 -14
- package/dist/lib.bundle.js +3 -3
- package/dist/lib.bundle.js.map +3 -3
- package/dist/lib.js +11 -11
- package/dist/processor.js +2 -2
- package/dist/processor.js.map +4 -4
- package/dist/store/clipStore.js +2 -2
- package/dist/styles.css.d.ts +3 -0
- package/examples/README.md +12 -4
- package/examples/cdn-vanilla/README.md +10 -6
- package/examples/cdn-vanilla/index.html +1065 -33
- package/examples/esm-bundler/package.json +1 -1
- package/examples/index.html +17 -0
- package/examples/react/README.md +1 -1
- package/examples/react/bun.lock +45 -0
- package/examples/react/src/App.tsx +56 -6
- package/examples/react/src/css.d.ts +1 -0
- package/examples/react/tsconfig.json +15 -0
- package/examples/self-hosted/package.json +2 -4
- package/examples/self-hosted/public/processor.js +4 -0
- package/examples/self-hosted/src/main.ts +1 -3
- 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 +6 -2
- package/examples/esm-bundler/bun.lock +0 -15
- package/examples/self-hosted/bun.lock +0 -15
|
@@ -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>
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { ClipNode, getProcessorBlobUrl } from "@jadujoel/web-audio-clip-node";
|
|
2
|
+
import { workerCode } from "./generated/worker-code";
|
|
3
|
+
|
|
4
|
+
function getWorkerBlobUrl(): string {
|
|
5
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
6
|
+
return URL.createObjectURL(blob);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ── DOM references ───────────────────────────────────────────────────
|
|
10
|
+
const streamBtn = document.getElementById("stream") as HTMLButtonElement;
|
|
11
|
+
const pauseBtn = document.getElementById("pause") as HTMLButtonElement;
|
|
12
|
+
const stopBtn = document.getElementById("stop") as HTMLButtonElement;
|
|
13
|
+
const urlInput = document.getElementById("url") as HTMLInputElement;
|
|
14
|
+
const throttleSelect = document.getElementById("throttle-select") as HTMLSelectElement;
|
|
15
|
+
const progressBar = document.getElementById("progress") as HTMLDivElement;
|
|
16
|
+
const statusText = document.getElementById("status") as HTMLParagraphElement;
|
|
17
|
+
const controlsPanel = document.getElementById("controls") as HTMLDivElement;
|
|
18
|
+
|
|
19
|
+
// Control sliders
|
|
20
|
+
const gainSlider = document.getElementById("ctrl-gain") as HTMLInputElement;
|
|
21
|
+
const panSlider = document.getElementById("ctrl-pan") as HTMLInputElement;
|
|
22
|
+
const rateSlider = document.getElementById("ctrl-rate") as HTMLInputElement;
|
|
23
|
+
const detuneSlider = document.getElementById("ctrl-detune") as HTMLInputElement;
|
|
24
|
+
const lowpassSlider = document.getElementById("ctrl-lowpass") as HTMLInputElement;
|
|
25
|
+
const highpassSlider = document.getElementById(
|
|
26
|
+
"ctrl-highpass",
|
|
27
|
+
) as HTMLInputElement;
|
|
28
|
+
const fadeInSlider = document.getElementById("ctrl-fadein") as HTMLInputElement;
|
|
29
|
+
const fadeOutSlider = document.getElementById(
|
|
30
|
+
"ctrl-fadeout",
|
|
31
|
+
) as HTMLInputElement;
|
|
32
|
+
const loopCheckbox = document.getElementById("ctrl-loop") as HTMLInputElement;
|
|
33
|
+
const loopStartSlider = document.getElementById(
|
|
34
|
+
"ctrl-loopstart",
|
|
35
|
+
) as HTMLInputElement;
|
|
36
|
+
const loopEndSlider = document.getElementById(
|
|
37
|
+
"ctrl-loopend",
|
|
38
|
+
) as HTMLInputElement;
|
|
39
|
+
const crossfadeSlider = document.getElementById(
|
|
40
|
+
"ctrl-crossfade",
|
|
41
|
+
) as HTMLInputElement;
|
|
42
|
+
|
|
43
|
+
// Value displays
|
|
44
|
+
const valGain = document.getElementById("val-gain") as HTMLSpanElement;
|
|
45
|
+
const valPan = document.getElementById("val-pan") as HTMLSpanElement;
|
|
46
|
+
const valRate = document.getElementById("val-rate") as HTMLSpanElement;
|
|
47
|
+
const valDetune = document.getElementById("val-detune") as HTMLSpanElement;
|
|
48
|
+
const valLowpass = document.getElementById("val-lowpass") as HTMLSpanElement;
|
|
49
|
+
const valHighpass = document.getElementById("val-highpass") as HTMLSpanElement;
|
|
50
|
+
const valFadeIn = document.getElementById("val-fadein") as HTMLSpanElement;
|
|
51
|
+
const valFadeOut = document.getElementById("val-fadeout") as HTMLSpanElement;
|
|
52
|
+
const valLoopStart = document.getElementById(
|
|
53
|
+
"val-loopstart",
|
|
54
|
+
) as HTMLSpanElement;
|
|
55
|
+
const valLoopEnd = document.getElementById("val-loopend") as HTMLSpanElement;
|
|
56
|
+
const valCrossfade = document.getElementById(
|
|
57
|
+
"val-crossfade",
|
|
58
|
+
) as HTMLSpanElement;
|
|
59
|
+
|
|
60
|
+
// ── State ────────────────────────────────────────────────────────────
|
|
61
|
+
let ctx: AudioContext | null = null;
|
|
62
|
+
let clip: ClipNode | null = null;
|
|
63
|
+
let worker: Worker | null = null;
|
|
64
|
+
|
|
65
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
66
|
+
function setStatus(msg: string) {
|
|
67
|
+
statusText.textContent = msg;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setProgress(ratio: number) {
|
|
71
|
+
progressBar.style.width = `${Math.min(100, ratio * 100).toFixed(1)}%`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Stream & Play ────────────────────────────────────────────────────
|
|
75
|
+
streamBtn.addEventListener("click", async () => {
|
|
76
|
+
const url = urlInput.value.trim();
|
|
77
|
+
if (!url) {
|
|
78
|
+
setStatus("Enter a URL first.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Tear down previous run
|
|
83
|
+
if (worker) {
|
|
84
|
+
worker.postMessage({ type: "abort" });
|
|
85
|
+
worker.terminate();
|
|
86
|
+
worker = null;
|
|
87
|
+
}
|
|
88
|
+
if (clip) {
|
|
89
|
+
clip.stop();
|
|
90
|
+
clip.disconnect();
|
|
91
|
+
clip = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create AudioContext
|
|
95
|
+
if (!ctx) {
|
|
96
|
+
ctx = new AudioContext();
|
|
97
|
+
await ctx.audioWorklet.addModule(getProcessorBlobUrl());
|
|
98
|
+
} else {
|
|
99
|
+
await ctx.resume();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create ClipNode
|
|
103
|
+
clip = new ClipNode(ctx);
|
|
104
|
+
clip.loop = true;
|
|
105
|
+
clip.connect(ctx.destination);
|
|
106
|
+
|
|
107
|
+
// Apply current slider values to the new clip
|
|
108
|
+
applyControls();
|
|
109
|
+
|
|
110
|
+
// Show controls panel
|
|
111
|
+
controlsPanel.style.display = "";
|
|
112
|
+
pauseBtn.disabled = false;
|
|
113
|
+
stopBtn.disabled = false;
|
|
114
|
+
|
|
115
|
+
// Create MessageChannel: port1 → Worker, port2 → Processor
|
|
116
|
+
const channel = new MessageChannel();
|
|
117
|
+
|
|
118
|
+
// Transfer port2 to the processor via the ClipNode API (zero main-thread allocation)
|
|
119
|
+
clip.transferPort(channel.port2);
|
|
120
|
+
|
|
121
|
+
// Create and start decode worker from inline Blob URL
|
|
122
|
+
worker = new Worker(getWorkerBlobUrl());
|
|
123
|
+
|
|
124
|
+
worker.onmessage = (ev: MessageEvent) => {
|
|
125
|
+
const { type } = ev.data;
|
|
126
|
+
switch (type) {
|
|
127
|
+
case "progress": {
|
|
128
|
+
const { bytesReceived, totalBytes } = ev.data;
|
|
129
|
+
if (totalBytes) {
|
|
130
|
+
setProgress(bytesReceived / totalBytes);
|
|
131
|
+
setStatus(
|
|
132
|
+
`Downloading… ${((bytesReceived / 1024) | 0)} / ${((totalBytes / 1024) | 0)} KB`,
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
setStatus(`Downloading… ${((bytesReceived / 1024) | 0)} KB`);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "decoded": {
|
|
140
|
+
const { samplesDecoded } = ev.data;
|
|
141
|
+
if (samplesDecoded > 0 && clip && clip.state === "initial") {
|
|
142
|
+
clip.start();
|
|
143
|
+
setStatus("Streaming & playing…");
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "info": {
|
|
148
|
+
setStatus(`Decoding: ${ev.data.sampleRate} Hz, ${ev.data.channels} ch`);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "done": {
|
|
152
|
+
setStatus(`Done — ${ev.data.samplesDecoded} samples decoded.`);
|
|
153
|
+
setProgress(1);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "error": {
|
|
157
|
+
setStatus(`Error: ${ev.data.message}`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "aborted": {
|
|
161
|
+
setStatus("Aborted.");
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Init the worker with the port and absolute URL
|
|
168
|
+
const absoluteUrl = new URL(url, location.href).href;
|
|
169
|
+
const throttle = Number(throttleSelect.value);
|
|
170
|
+
worker.postMessage(
|
|
171
|
+
{ type: "init", port: channel.port1, url: absoluteUrl, throttle },
|
|
172
|
+
[channel.port1],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
setStatus(throttle > 0 ? `Starting stream… (${(throttle / 1024).toFixed(0)} KB/s)` : "Starting stream…");
|
|
176
|
+
setProgress(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── Transport controls ───────────────────────────────────────────────
|
|
180
|
+
pauseBtn.addEventListener("click", () => {
|
|
181
|
+
if (!clip || !ctx) return;
|
|
182
|
+
if (clip.state === "playing" || clip.state === "started") {
|
|
183
|
+
clip.pause();
|
|
184
|
+
setStatus("Paused.");
|
|
185
|
+
} else if (clip.state === "paused") {
|
|
186
|
+
clip.start();
|
|
187
|
+
setStatus("Resumed.");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
stopBtn.addEventListener("click", () => {
|
|
192
|
+
if (!clip) return;
|
|
193
|
+
clip.stop();
|
|
194
|
+
if (worker) {
|
|
195
|
+
worker.postMessage({ type: "abort" });
|
|
196
|
+
worker.terminate();
|
|
197
|
+
worker = null;
|
|
198
|
+
}
|
|
199
|
+
setProgress(0);
|
|
200
|
+
setStatus("Stopped.");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── Control wiring ───────────────────────────────────────────────────
|
|
204
|
+
function formatPan(v: number): string {
|
|
205
|
+
if (Math.abs(v) < 0.005) return "C";
|
|
206
|
+
return v < 0 ? `L ${(-v * 100).toFixed(0)}` : `R ${(v * 100).toFixed(0)}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatHz(v: number): string {
|
|
210
|
+
return v >= 1000 ? `${(v / 1000).toFixed(1)} kHz` : `${v} Hz`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function applyControls() {
|
|
214
|
+
if (!clip) return;
|
|
215
|
+
clip.gain.value = Number(gainSlider.value);
|
|
216
|
+
clip.pan.value = Number(panSlider.value);
|
|
217
|
+
clip.playbackRate.value = Number(rateSlider.value);
|
|
218
|
+
clip.detune.value = Number(detuneSlider.value);
|
|
219
|
+
clip.lowpass.value = Number(lowpassSlider.value);
|
|
220
|
+
clip.highpass.value = Number(highpassSlider.value);
|
|
221
|
+
clip.fadeIn = Number(fadeInSlider.value);
|
|
222
|
+
clip.fadeOut = Number(fadeOutSlider.value);
|
|
223
|
+
clip.loop = loopCheckbox.checked;
|
|
224
|
+
clip.loopStart = Number(loopStartSlider.value);
|
|
225
|
+
clip.loopEnd = Number(loopEndSlider.value);
|
|
226
|
+
clip.loopCrossfade = Number(crossfadeSlider.value);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Wire each slider to update the value display and the clip
|
|
230
|
+
gainSlider.addEventListener("input", () => {
|
|
231
|
+
valGain.textContent = Number(gainSlider.value).toFixed(2);
|
|
232
|
+
if (clip) clip.gain.value = Number(gainSlider.value);
|
|
233
|
+
});
|
|
234
|
+
panSlider.addEventListener("input", () => {
|
|
235
|
+
valPan.textContent = formatPan(Number(panSlider.value));
|
|
236
|
+
if (clip) clip.pan.value = Number(panSlider.value);
|
|
237
|
+
});
|
|
238
|
+
rateSlider.addEventListener("input", () => {
|
|
239
|
+
valRate.textContent = `${Number(rateSlider.value).toFixed(2)}×`;
|
|
240
|
+
if (clip) clip.playbackRate.value = Number(rateSlider.value);
|
|
241
|
+
});
|
|
242
|
+
detuneSlider.addEventListener("input", () => {
|
|
243
|
+
valDetune.textContent = `${detuneSlider.value} ct`;
|
|
244
|
+
if (clip) clip.detune.value = Number(detuneSlider.value);
|
|
245
|
+
});
|
|
246
|
+
lowpassSlider.addEventListener("input", () => {
|
|
247
|
+
valLowpass.textContent = formatHz(Number(lowpassSlider.value));
|
|
248
|
+
if (clip) clip.lowpass.value = Number(lowpassSlider.value);
|
|
249
|
+
});
|
|
250
|
+
highpassSlider.addEventListener("input", () => {
|
|
251
|
+
valHighpass.textContent = formatHz(Number(highpassSlider.value));
|
|
252
|
+
if (clip) clip.highpass.value = Number(highpassSlider.value);
|
|
253
|
+
});
|
|
254
|
+
fadeInSlider.addEventListener("input", () => {
|
|
255
|
+
valFadeIn.textContent = `${Number(fadeInSlider.value).toFixed(2)} s`;
|
|
256
|
+
if (clip) clip.fadeIn = Number(fadeInSlider.value);
|
|
257
|
+
});
|
|
258
|
+
fadeOutSlider.addEventListener("input", () => {
|
|
259
|
+
valFadeOut.textContent = `${Number(fadeOutSlider.value).toFixed(2)} s`;
|
|
260
|
+
if (clip) clip.fadeOut = Number(fadeOutSlider.value);
|
|
261
|
+
});
|
|
262
|
+
loopCheckbox.addEventListener("change", () => {
|
|
263
|
+
if (clip) clip.loop = loopCheckbox.checked;
|
|
264
|
+
});
|
|
265
|
+
loopStartSlider.addEventListener("input", () => {
|
|
266
|
+
valLoopStart.textContent = `${Number(loopStartSlider.value).toFixed(2)} s`;
|
|
267
|
+
if (clip) clip.loopStart = Number(loopStartSlider.value);
|
|
268
|
+
});
|
|
269
|
+
loopEndSlider.addEventListener("input", () => {
|
|
270
|
+
valLoopEnd.textContent = `${Number(loopEndSlider.value).toFixed(2)} s`;
|
|
271
|
+
if (clip) clip.loopEnd = Number(loopEndSlider.value);
|
|
272
|
+
});
|
|
273
|
+
crossfadeSlider.addEventListener("input", () => {
|
|
274
|
+
valCrossfade.textContent = `${Number(crossfadeSlider.value).toFixed(2)} s`;
|
|
275
|
+
if (clip) clip.loopCrossfade = Number(crossfadeSlider.value);
|
|
276
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "streaming-example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build:worker": "bun run build-worker.ts",
|
|
7
|
+
"dev": "bun run build:worker && bun index.html"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@jadujoel/web-audio-clip-node": "file:../.."
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jadujoel/web-audio-clip-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-featured AudioWorklet clip player with playback rate, detune, gain, pan, filters, looping, fades, crossfade, and streaming buffer support. React components included.",
|
|
6
6
|
"keywords": [
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"import": "./dist/lib-react.js"
|
|
31
31
|
},
|
|
32
32
|
"./processor": "./dist/processor.js",
|
|
33
|
-
"./styles.css":
|
|
33
|
+
"./styles.css": {
|
|
34
|
+
"types": "./dist/styles.css.d.ts",
|
|
35
|
+
"default": "./dist/styles.css"
|
|
36
|
+
}
|
|
34
37
|
},
|
|
35
38
|
"main": "./dist/lib.js",
|
|
36
39
|
"types": "./dist/lib.d.ts",
|
|
@@ -45,6 +48,7 @@
|
|
|
45
48
|
"build": "bun build.ts",
|
|
46
49
|
"build:lib": "bun build.ts --lib",
|
|
47
50
|
"dev": "bun serve.ts",
|
|
51
|
+
"examples": "bun examples.ts",
|
|
48
52
|
"lint": "biome check",
|
|
49
53
|
"test": "bun test",
|
|
50
54
|
"typecheck": "tsc --noEmit",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 0,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "esm-bundler-example",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@jadujoel/web-audio-clip-node": "latest",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
"packages": {
|
|
13
|
-
"@jadujoel/web-audio-clip-node": ["@jadujoel/web-audio-clip-node@0.1.4", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "zustand": ">=4" }, "optionalPeers": ["react", "react-dom", "zustand"] }, "sha512-mQhckPRRz6fOUdqJbBatyWyhEo0zOKTaKHt5KltxF+F0o4iq7GcLhlR/iv/Qu/qn0rH+9eOlNxypDF6gG5su/w=="],
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "self-hosted-example",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@jadujoel/web-audio-clip-node": "^0.1.1",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
"packages": {
|
|
13
|
-
"@jadujoel/web-audio-clip-node": ["@jadujoel/web-audio-clip-node@0.1.4", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "zustand": ">=4" }, "optionalPeers": ["react", "react-dom", "zustand"] }, "sha512-mQhckPRRz6fOUdqJbBatyWyhEo0zOKTaKHt5KltxF+F0o4iq7GcLhlR/iv/Qu/qn0rH+9eOlNxypDF6gG5su/w=="],
|
|
14
|
-
}
|
|
15
|
-
}
|