@jadujoel/web-audio-clip-node 0.1.0 → 0.1.1

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 (51) hide show
  1. package/dist/audio/ClipNode.js +312 -0
  2. package/dist/audio/processor-code.js +2 -0
  3. package/dist/audio/processor-kernel.js +861 -0
  4. package/dist/audio/processor.js +80 -0
  5. package/dist/audio/types.js +9 -0
  6. package/dist/audio/utils.js +128 -0
  7. package/dist/audio/version.d.ts +1 -0
  8. package/dist/audio/version.js +2 -0
  9. package/dist/audio/workletUrl.js +17 -0
  10. package/dist/components/AudioControl.js +99 -0
  11. package/dist/components/ContextMenu.js +73 -0
  12. package/dist/components/ControlSection.js +74 -0
  13. package/dist/components/DetuneControl.js +44 -0
  14. package/dist/components/DisplayPanel.js +6 -0
  15. package/dist/components/FilterControl.js +48 -0
  16. package/dist/components/GainControl.js +44 -0
  17. package/dist/components/PanControl.js +50 -0
  18. package/dist/components/PlaybackRateControl.js +44 -0
  19. package/dist/components/PlayheadSlider.js +20 -0
  20. package/dist/components/SnappableSlider.js +174 -0
  21. package/dist/components/TransportButtons.js +9 -0
  22. package/dist/controls/controlDefs.js +211 -0
  23. package/dist/controls/formatValueText.js +80 -0
  24. package/dist/controls/linkedControlPairs.js +51 -0
  25. package/dist/data/cache.js +17 -0
  26. package/dist/data/fileStore.js +39 -0
  27. package/dist/hooks/useClipNode.js +338 -0
  28. package/dist/lib-react.js +17 -19
  29. package/dist/lib.js +16 -44
  30. package/dist/store/clipStore.js +71 -0
  31. package/examples/README.md +10 -0
  32. package/examples/cdn-vanilla/README.md +13 -0
  33. package/examples/cdn-vanilla/index.html +61 -0
  34. package/examples/esm-bundler/README.md +8 -0
  35. package/examples/esm-bundler/index.html +12 -0
  36. package/examples/esm-bundler/package.json +15 -0
  37. package/examples/esm-bundler/src/main.ts +43 -0
  38. package/examples/react/README.md +10 -0
  39. package/examples/react/index.html +12 -0
  40. package/examples/react/package.json +21 -0
  41. package/examples/react/src/App.tsx +20 -0
  42. package/examples/react/src/main.tsx +9 -0
  43. package/examples/react/vite.config.ts +6 -0
  44. package/examples/self-hosted/README.md +11 -0
  45. package/examples/self-hosted/index.html +12 -0
  46. package/examples/self-hosted/package.json +16 -0
  47. package/examples/self-hosted/public/.gitkeep +1 -0
  48. package/examples/self-hosted/src/main.ts +46 -0
  49. package/package.json +3 -2
  50. package/dist/lib-react.js.map +0 -9
  51. package/dist/lib.js.map +0 -9
@@ -0,0 +1,39 @@
1
+ const DB_NAME = "clip-audio-store";
2
+ const DB_VERSION = 1;
3
+ const STORE_NAME = "files";
4
+ const LAST_FILE_KEY = "last-uploaded";
5
+ function openDB() {
6
+ return new Promise((resolve, reject) => {
7
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
8
+ request.onupgradeneeded = () => {
9
+ const db = request.result;
10
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
11
+ db.createObjectStore(STORE_NAME);
12
+ }
13
+ };
14
+ request.onsuccess = () => resolve(request.result);
15
+ request.onerror = () => reject(request.error);
16
+ });
17
+ }
18
+ function tx(db, mode) {
19
+ return db.transaction(STORE_NAME, mode).objectStore(STORE_NAME);
20
+ }
21
+ export async function saveUploadedFile(name, arrayBuffer) {
22
+ const db = await openDB();
23
+ const store = tx(db, "readwrite");
24
+ const data = { name, arrayBuffer };
25
+ await new Promise((resolve, reject) => {
26
+ const req = store.put(data, LAST_FILE_KEY);
27
+ req.onsuccess = () => resolve();
28
+ req.onerror = () => reject(req.error);
29
+ });
30
+ }
31
+ export async function loadUploadedFile() {
32
+ const db = await openDB();
33
+ const store = tx(db, "readonly");
34
+ return new Promise((resolve, reject) => {
35
+ const req = store.get(LAST_FILE_KEY);
36
+ req.onsuccess = () => resolve(req.result ?? null);
37
+ req.onerror = () => reject(req.error);
38
+ });
39
+ }
@@ -0,0 +1,338 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { ClipNode } from "../audio/ClipNode";
3
+ import { float32ArrayFromAudioBuffer, linFromDb } from "../audio/utils";
4
+ import { getProcessorModuleUrl } from "../audio/workletUrl";
5
+ import { SAMPLE_RATE } from "../controls/controlDefs";
6
+ import { loadFromCache } from "../data/cache";
7
+ import { loadUploadedFile, saveUploadedFile } from "../data/fileStore";
8
+ function applyValue(node, key, value) {
9
+ switch (key) {
10
+ case "playhead":
11
+ node.playhead = value;
12
+ break;
13
+ case "offset":
14
+ node.offset = value;
15
+ break;
16
+ case "duration":
17
+ node.duration = value;
18
+ break;
19
+ case "loopStart":
20
+ node.loopStart = value;
21
+ break;
22
+ case "loopEnd":
23
+ node.loopEnd = value;
24
+ break;
25
+ case "loopCrossfade":
26
+ node.loopCrossfade = value;
27
+ break;
28
+ case "fadeIn":
29
+ node.fadeIn = value;
30
+ break;
31
+ case "fadeOut":
32
+ node.fadeOut = value;
33
+ break;
34
+ case "playbackRate":
35
+ node.playbackRate.value = value;
36
+ break;
37
+ case "detune":
38
+ node.detune.value = value;
39
+ break;
40
+ case "gain":
41
+ node.gain.value = linFromDb(value);
42
+ break;
43
+ case "pan":
44
+ node.pan.value = value;
45
+ break;
46
+ case "lowpass":
47
+ node.lowpass.value = value;
48
+ break;
49
+ case "highpass":
50
+ node.highpass.value = value;
51
+ break;
52
+ case "startDelay":
53
+ case "stopDelay":
54
+ break;
55
+ }
56
+ }
57
+ function applyToggle(node, key, on) {
58
+ switch (key) {
59
+ case "fadeIn":
60
+ node.toggleFadeIn(on);
61
+ break;
62
+ case "fadeOut":
63
+ node.toggleFadeOut(on);
64
+ break;
65
+ case "loopCrossfade":
66
+ node.toggleLoopCrossfade(on);
67
+ break;
68
+ case "loopStart":
69
+ node.toggleLoopStart(on);
70
+ break;
71
+ case "loopEnd":
72
+ node.toggleLoopEnd(on);
73
+ break;
74
+ case "playbackRate":
75
+ node.togglePlaybackRate(on);
76
+ break;
77
+ case "detune":
78
+ node.toggleDetune(on);
79
+ break;
80
+ case "gain":
81
+ node.toggleGain(on);
82
+ break;
83
+ case "pan":
84
+ node.togglePan(on);
85
+ break;
86
+ case "lowpass":
87
+ node.toggleLowpass(on);
88
+ break;
89
+ case "highpass":
90
+ node.toggleHighpass(on);
91
+ break;
92
+ case "offset":
93
+ node.offset = on ? node.offset : 0;
94
+ break;
95
+ case "duration":
96
+ node.duration = on ? node.duration : -1;
97
+ break;
98
+ case "startDelay":
99
+ case "stopDelay":
100
+ // These are applied at start/stop time; toggle state is handled in the hook
101
+ break;
102
+ }
103
+ }
104
+ export function useClipNode({ values, enabled, loop, setValue, }) {
105
+ const [nodeState, setNodeState] = useState("initial");
106
+ const [statusMessage, setStatusMessage] = useState(null);
107
+ const [soundName, setSoundName] = useState(null);
108
+ const [audioDuration, setAudioDuration] = useState(null);
109
+ const [infoCurrentTime, setInfoCurrentTime] = useState("0");
110
+ const [infoCurrentFrame, setInfoCurrentFrame] = useState("0");
111
+ const [infoTimesLooped, setInfoTimesLooped] = useState("0");
112
+ const [infoLatency, setInfoLatency] = useState("unknown");
113
+ const [infoTimeTaken, setInfoTimeTaken] = useState("unknown");
114
+ const ctxRef = useRef(null);
115
+ const nodeRef = useRef(null);
116
+ const bufferRef = useRef(null);
117
+ const frameRef = useRef(null);
118
+ // RAF loop for display info
119
+ useEffect(() => {
120
+ let id;
121
+ const tick = () => {
122
+ const f = frameRef.current;
123
+ if (f) {
124
+ const [ct, cf, ph, tt] = f;
125
+ setInfoCurrentTime(ct.toPrecision(4));
126
+ setInfoCurrentFrame(cf.toString());
127
+ setInfoTimeTaken(tt.toFixed(4));
128
+ setValue("playhead", ph);
129
+ }
130
+ id = requestAnimationFrame(tick);
131
+ };
132
+ id = requestAnimationFrame(tick);
133
+ return () => cancelAnimationFrame(id);
134
+ }, [setValue]);
135
+ const ensureContext = useCallback(async () => {
136
+ if (ctxRef.current)
137
+ return ctxRef.current;
138
+ const ctx = new AudioContext({ sampleRate: SAMPLE_RATE });
139
+ await ctx.audioWorklet.addModule(getProcessorModuleUrl());
140
+ ctxRef.current = ctx;
141
+ return ctx;
142
+ }, []);
143
+ const decodeAudio = useCallback(async (source) => {
144
+ const ctx = await ensureContext();
145
+ let arrayBuffer;
146
+ if (typeof source === "string") {
147
+ arrayBuffer = await loadFromCache(source);
148
+ }
149
+ else {
150
+ arrayBuffer = source;
151
+ }
152
+ if (!arrayBuffer)
153
+ throw new Error("Could not load audio data");
154
+ const decoded = await ctx.decodeAudioData(arrayBuffer);
155
+ bufferRef.current = decoded;
156
+ setAudioDuration(decoded.duration);
157
+ return decoded;
158
+ }, [ensureContext]);
159
+ const createNode = useCallback((ctx, buffer) => {
160
+ const node = new ClipNode(ctx, {
161
+ processorOptions: {
162
+ buffer: float32ArrayFromAudioBuffer(buffer),
163
+ loopStart: values.loopStart,
164
+ loopEnd: values.loopEnd,
165
+ duration: values.duration,
166
+ offset: values.offset,
167
+ fadeInDuration: values.fadeIn,
168
+ fadeOutDuration: values.fadeOut,
169
+ loop,
170
+ enableDetune: enabled.detune,
171
+ enableFadeIn: enabled.fadeIn,
172
+ enableFadeOut: enabled.fadeOut,
173
+ enableGain: enabled.gain,
174
+ enableHighpass: enabled.highpass,
175
+ enableLowpass: enabled.lowpass,
176
+ enablePan: enabled.pan,
177
+ enablePlaybackRate: enabled.playbackRate,
178
+ enableLoopStart: enabled.loopStart,
179
+ enableLoopEnd: enabled.loopEnd,
180
+ enableLoopCrossfade: enabled.loopCrossfade,
181
+ },
182
+ });
183
+ node.connect(ctx.destination);
184
+ node.onstatechange = (s) => setNodeState(s);
185
+ node.onlooped = () => setInfoTimesLooped(node.timesLooped.toString());
186
+ node.onframe = (data) => {
187
+ frameRef.current = data;
188
+ };
189
+ node.addEventListener("processorerror", (e) => console.error("processor error", e));
190
+ node.loop = loop;
191
+ node.playbackRate.value = values.playbackRate;
192
+ node.detune.value = values.detune;
193
+ node.lowpass.value = values.lowpass;
194
+ node.highpass.value = values.highpass;
195
+ node.gain.value = linFromDb(values.gain);
196
+ node.pan.value = values.pan;
197
+ setInfoLatency(ctx.outputLatency != null
198
+ ? `base: ${Math.round(ctx.baseLatency * ctx.sampleRate)} | output: ${Math.round(ctx.outputLatency * ctx.sampleRate)}`
199
+ : "unknown");
200
+ return node;
201
+ }, [loop, values, enabled]);
202
+ const start = useCallback(async () => {
203
+ const ctx = await ensureContext();
204
+ const buffer = bufferRef.current;
205
+ if (!buffer) {
206
+ setStatusMessage("Load a sound file first.");
207
+ return;
208
+ }
209
+ setStatusMessage(null);
210
+ if (!nodeRef.current) {
211
+ nodeRef.current = createNode(ctx, buffer);
212
+ }
213
+ ctx.resume();
214
+ const node = nodeRef.current;
215
+ const delay = enabled.startDelay ? values.startDelay : 0;
216
+ const offset = enabled.offset ? values.offset : 0;
217
+ const duration = enabled.duration ? values.duration : -1;
218
+ node.start(ctx.currentTime + delay, offset, duration);
219
+ }, [
220
+ ensureContext,
221
+ createNode,
222
+ values.startDelay,
223
+ values.offset,
224
+ values.duration,
225
+ enabled.startDelay,
226
+ enabled.offset,
227
+ enabled.duration,
228
+ ]);
229
+ const stop = useCallback(() => {
230
+ const ctx = ctxRef.current;
231
+ const node = nodeRef.current;
232
+ if (!ctx || !node)
233
+ return;
234
+ const delay = enabled.stopDelay ? values.stopDelay : 0;
235
+ node.stop(ctx.currentTime + delay);
236
+ }, [values.stopDelay, enabled.stopDelay]);
237
+ const pause = useCallback(() => {
238
+ const ctx = ctxRef.current;
239
+ const node = nodeRef.current;
240
+ if (!ctx || !node)
241
+ return;
242
+ const delay = enabled.stopDelay ? values.stopDelay : 0;
243
+ node.pause(ctx.currentTime + delay);
244
+ }, [values.stopDelay, enabled.stopDelay]);
245
+ const resume = useCallback(() => {
246
+ const ctx = ctxRef.current;
247
+ const node = nodeRef.current;
248
+ if (!ctx || !node)
249
+ return;
250
+ const delay = enabled.startDelay ? values.startDelay : 0;
251
+ node.resume(ctx.currentTime + delay);
252
+ }, [values.startDelay, enabled.startDelay]);
253
+ const dispose = useCallback(() => {
254
+ nodeRef.current?.dispose();
255
+ nodeRef.current = null;
256
+ setNodeState("disposed");
257
+ }, []);
258
+ const logState = useCallback(() => {
259
+ nodeRef.current?.logState();
260
+ }, []);
261
+ const loadFromArrayBuffer = useCallback(async (ab, name) => {
262
+ const buf = await decodeAudio(ab);
263
+ bufferRef.current = buf;
264
+ setSoundName(name);
265
+ setValue("playhead", 0);
266
+ }, [decodeAudio, setValue]);
267
+ // Auto-load last uploaded file from IndexedDB on mount
268
+ useEffect(() => {
269
+ loadUploadedFile()
270
+ .then((stored) => {
271
+ if (stored) {
272
+ loadFromArrayBuffer(stored.arrayBuffer, stored.name).catch((err) => console.error("[fileStore] Failed to restore file:", err));
273
+ }
274
+ })
275
+ .catch((err) => console.error("[fileStore] Failed to load from IndexedDB:", err));
276
+ }, [loadFromArrayBuffer]);
277
+ const loadSound = useCallback(() => {
278
+ const input = document.createElement("input");
279
+ input.type = "file";
280
+ input.accept = "audio/*";
281
+ input.onchange = async () => {
282
+ const file = input.files?.[0];
283
+ if (!file)
284
+ return;
285
+ const ab = await file.arrayBuffer();
286
+ const copy = ab.slice(0);
287
+ await loadFromArrayBuffer(ab, file.name);
288
+ saveUploadedFile(file.name, copy).catch((err) => console.error("[fileStore] Failed to save to IndexedDB:", err));
289
+ };
290
+ input.click();
291
+ }, [loadFromArrayBuffer]);
292
+ const applyValueToNode = useCallback((key, val) => {
293
+ setValue(key, val);
294
+ const node = nodeRef.current;
295
+ if (node)
296
+ applyValue(node, key, val);
297
+ }, [setValue]);
298
+ const applyToggleToNode = useCallback((key, on) => {
299
+ const node = nodeRef.current;
300
+ if (node)
301
+ applyToggle(node, key, on);
302
+ }, []);
303
+ const applyValuesToNode = useCallback((valuesToApply) => {
304
+ const node = nodeRef.current;
305
+ if (!node)
306
+ return;
307
+ for (const [key, value] of Object.entries(valuesToApply)) {
308
+ applyValue(node, key, value);
309
+ }
310
+ }, []);
311
+ const setLoopOnNode = useCallback((checked) => {
312
+ const node = nodeRef.current;
313
+ if (node)
314
+ node.loop = checked;
315
+ }, []);
316
+ return {
317
+ nodeState,
318
+ statusMessage,
319
+ soundName,
320
+ audioDuration,
321
+ infoCurrentTime,
322
+ infoCurrentFrame,
323
+ infoTimesLooped,
324
+ infoLatency,
325
+ infoTimeTaken,
326
+ start,
327
+ stop,
328
+ pause,
329
+ resume,
330
+ dispose,
331
+ logState,
332
+ loadSound,
333
+ applyValue: applyValueToNode,
334
+ applyValues: applyValuesToNode,
335
+ applyToggle: applyToggleToNode,
336
+ setLoopOnNode,
337
+ };
338
+ }
package/dist/lib-react.js CHANGED
@@ -1,19 +1,17 @@
1
- export {
2
- useClipNode,
3
- useClipControls,
4
- TransportButtons,
5
- SnappableSlider,
6
- PlayheadSlider,
7
- PlaybackRateControl,
8
- PanControl,
9
- GainControl,
10
- FilterControl,
11
- DisplayPanel,
12
- DetuneControl,
13
- ControlSection,
14
- ContextMenu,
15
- AudioControl
16
- };
17
-
18
- //# debugId=030B87E04A0B95F064756E2164756E21
19
- //# sourceMappingURL=lib-react.js.map
1
+ // Store
2
+ // Components
3
+ export { AudioControl } from "./components/AudioControl";
4
+ export { ContextMenu } from "./components/ContextMenu";
5
+ export { ControlSection } from "./components/ControlSection";
6
+ export { DetuneControl } from "./components/DetuneControl";
7
+ export { DisplayPanel } from "./components/DisplayPanel";
8
+ export { FilterControl } from "./components/FilterControl";
9
+ export { GainControl } from "./components/GainControl";
10
+ export { PanControl } from "./components/PanControl";
11
+ export { PlaybackRateControl } from "./components/PlaybackRateControl";
12
+ export { PlayheadSlider } from "./components/PlayheadSlider";
13
+ export { SnappableSlider } from "./components/SnappableSlider";
14
+ export { TransportButtons } from "./components/TransportButtons";
15
+ // Hooks
16
+ export { useClipNode } from "./hooks/useClipNode";
17
+ export { useClipControls } from "./store/clipStore";
package/dist/lib.js CHANGED
@@ -1,44 +1,16 @@
1
- export {
2
- transportLinkedControlPairs,
3
- saveUploadedFile,
4
- remapTempoRelativeValue,
5
- processorCode,
6
- processBlock,
7
- presets,
8
- paramDefs,
9
- loopLinkedControlPairs,
10
- loopControlDefs,
11
- loadUploadedFile,
12
- loadFromCache,
13
- linFromDb,
14
- isTempoRelativeSnap,
15
- handleProcessorMessage,
16
- getTempoSnapInterval,
17
- getSnappedValue,
18
- getProperties,
19
- getProcessorModuleUrl,
20
- getProcessorCdnUrl,
21
- getProcessorBlobUrl,
22
- getLinkedControlUpdates,
23
- getLinkedControlPairForControl,
24
- getActiveLinkedControls,
25
- generateSnapPoints,
26
- formatValueText,
27
- formatTickLabel,
28
- float32ArrayFromAudioBuffer,
29
- dbFromLin,
30
- createFilterState,
31
- controlDefs,
32
- buildLinkedControlPairDefaults,
33
- buildDefaults,
34
- audioBufferFromFloat32Array,
35
- allDefs,
36
- State,
37
- SAMPLE_RATE,
38
- SAMPLE_BLOCK_SIZE,
39
- DEFAULT_TEMPO,
40
- ClipNode
41
- };
42
-
43
- //# debugId=9B64F311A83C9C3D64756E2164756E21
44
- //# sourceMappingURL=lib.js.map
1
+ // Core audio
2
+ export { ClipNode } from "./audio/ClipNode";
3
+ export { processorCode } from "./audio/processor-code";
4
+ // Processor kernel (for advanced / testing)
5
+ export { createFilterState, getProperties, handleProcessorMessage, processBlock, SAMPLE_BLOCK_SIZE, } from "./audio/processor-kernel";
6
+ export { State } from "./audio/types";
7
+ // Utils
8
+ export { audioBufferFromFloat32Array, dbFromLin, float32ArrayFromAudioBuffer, generateSnapPoints, getSnappedValue, getTempoSnapInterval, isTempoRelativeSnap, linFromDb, presets, remapTempoRelativeValue, } from "./audio/utils";
9
+ export { getProcessorBlobUrl, getProcessorCdnUrl, getProcessorModuleUrl, } from "./audio/workletUrl";
10
+ // Controls
11
+ export { allDefs, buildDefaults, controlDefs, DEFAULT_TEMPO, loopControlDefs, paramDefs, SAMPLE_RATE, } from "./controls/controlDefs";
12
+ export { formatTickLabel, formatValueText } from "./controls/formatValueText";
13
+ export { buildLinkedControlPairDefaults, getActiveLinkedControls, getLinkedControlPairForControl, getLinkedControlUpdates, loopLinkedControlPairs, transportLinkedControlPairs, } from "./controls/linkedControlPairs";
14
+ // Data
15
+ export { loadFromCache } from "./data/cache";
16
+ export { loadUploadedFile, saveUploadedFile } from "./data/fileStore";
@@ -0,0 +1,71 @@
1
+ import { create } from "zustand";
2
+ import { persist } from "zustand/middleware";
3
+ import { buildDefaults, DEFAULT_TEMPO, } from "../controls/controlDefs";
4
+ import { buildLinkedControlPairDefaults, } from "../controls/linkedControlPairs";
5
+ const STORAGE_KEY = "clip-node-state";
6
+ function searchParamsIncludes(key) {
7
+ return (typeof window !== "undefined" &&
8
+ new URLSearchParams(window.location.search).has(key));
9
+ }
10
+ const defaults = buildDefaults();
11
+ const linkedPairDefaults = buildLinkedControlPairDefaults();
12
+ export const useClipControls = create()(searchParamsIncludes("disable-state")
13
+ ? (set) => ({
14
+ ...defaults,
15
+ linkedPairs: linkedPairDefaults,
16
+ loop: false,
17
+ tempo: DEFAULT_TEMPO,
18
+ setValue: (key, val) => set((s) => ({ values: { ...s.values, [key]: val } })),
19
+ setValuesPartial: (values) => set((s) => ({ values: { ...s.values, ...values } })),
20
+ setSnap: (key, snap) => set((s) => ({ snaps: { ...s.snaps, [key]: snap } })),
21
+ setSnapsPartial: (snaps) => set((s) => ({ snaps: { ...s.snaps, ...snaps } })),
22
+ setEnabled: (key, on) => set((s) => ({ enabled: { ...s.enabled, [key]: on } })),
23
+ setEnabledPartial: (enabled) => set((s) => ({ enabled: { ...s.enabled, ...enabled } })),
24
+ setMin: (key, val) => set((s) => ({ mins: { ...s.mins, [key]: val } })),
25
+ setMinsPartial: (mins) => set((s) => ({ mins: { ...s.mins, ...mins } })),
26
+ setMax: (key, val) => set((s) => ({ maxs: { ...s.maxs, [key]: val } })),
27
+ setMaxsPartial: (maxs) => set((s) => ({ maxs: { ...s.maxs, ...maxs } })),
28
+ setMaxLocked: (key, locked) => set((s) => ({ maxLocked: { ...s.maxLocked, [key]: locked } })),
29
+ setMaxLockedPartial: (maxLocked) => set((s) => ({ maxLocked: { ...s.maxLocked, ...maxLocked } })),
30
+ setLinkedPair: (key, on) => set((s) => ({ linkedPairs: { ...s.linkedPairs, [key]: on } })),
31
+ setLoop: (checked) => set({ loop: checked }),
32
+ setTempo: (tempo) => set({ tempo }),
33
+ setTempoAndValues: (tempo, values) => set((s) => ({ tempo, values: { ...s.values, ...values } })),
34
+ setValues: (values) => set({ values }),
35
+ })
36
+ : persist((set) => ({
37
+ ...defaults,
38
+ linkedPairs: linkedPairDefaults,
39
+ loop: false,
40
+ tempo: DEFAULT_TEMPO,
41
+ setValue: (key, val) => set((s) => ({ values: { ...s.values, [key]: val } })),
42
+ setValuesPartial: (values) => set((s) => ({ values: { ...s.values, ...values } })),
43
+ setSnap: (key, snap) => set((s) => ({ snaps: { ...s.snaps, [key]: snap } })),
44
+ setSnapsPartial: (snaps) => set((s) => ({ snaps: { ...s.snaps, ...snaps } })),
45
+ setEnabled: (key, on) => set((s) => ({ enabled: { ...s.enabled, [key]: on } })),
46
+ setEnabledPartial: (enabled) => set((s) => ({ enabled: { ...s.enabled, ...enabled } })),
47
+ setMin: (key, val) => set((s) => ({ mins: { ...s.mins, [key]: val } })),
48
+ setMinsPartial: (mins) => set((s) => ({ mins: { ...s.mins, ...mins } })),
49
+ setMax: (key, val) => set((s) => ({ maxs: { ...s.maxs, [key]: val } })),
50
+ setMaxsPartial: (maxs) => set((s) => ({ maxs: { ...s.maxs, ...maxs } })),
51
+ setMaxLocked: (key, locked) => set((s) => ({ maxLocked: { ...s.maxLocked, [key]: locked } })),
52
+ setMaxLockedPartial: (maxLocked) => set((s) => ({ maxLocked: { ...s.maxLocked, ...maxLocked } })),
53
+ setLinkedPair: (key, on) => set((s) => ({ linkedPairs: { ...s.linkedPairs, [key]: on } })),
54
+ setLoop: (checked) => set({ loop: checked }),
55
+ setTempo: (tempo) => set({ tempo }),
56
+ setTempoAndValues: (tempo, values) => set((s) => ({ tempo, values: { ...s.values, ...values } })),
57
+ setValues: (values) => set({ values }),
58
+ }), {
59
+ name: STORAGE_KEY,
60
+ partialize: (state) => ({
61
+ values: state.values,
62
+ snaps: state.snaps,
63
+ enabled: state.enabled,
64
+ mins: state.mins,
65
+ maxs: state.maxs,
66
+ maxLocked: state.maxLocked,
67
+ linkedPairs: state.linkedPairs,
68
+ loop: state.loop,
69
+ tempo: state.tempo,
70
+ }),
71
+ }));
@@ -0,0 +1,10 @@
1
+ # Examples
2
+
3
+ Each subdirectory demonstrates a different way to use `@jadujoel/web-audio-clip-node`.
4
+
5
+ | Example | Description | Build step? |
6
+ |---------|-------------|-------------|
7
+ | [cdn-vanilla](./cdn-vanilla/) | Pure HTML + `<script type="module">` via jsDelivr CDN | No |
8
+ | [esm-bundler](./esm-bundler/) | Vite + TypeScript with `npm install` | Yes |
9
+ | [react](./react/) | Vite + React using the built-in hooks & components | Yes |
10
+ | [self-hosted](./self-hosted/) | Vite + self-hosted `processor.js` via `getProcessorModuleUrl()` | Yes |
@@ -0,0 +1,13 @@
1
+ # CDN Vanilla Example
2
+
3
+ Zero-install example — open `index.html` directly in a browser (or use any static file server).
4
+
5
+ Everything is loaded from [jsDelivr](https://www.jsdelivr.com/).
6
+
7
+ ```sh
8
+ # Option A: just open the file
9
+ open index.html
10
+
11
+ # Option B: serve it
12
+ npx serve .
13
+ ```
@@ -0,0 +1,61 @@
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 – CDN Vanilla Example</title>
7
+ <style>
8
+ body { font-family: system-ui, sans-serif; max-width: 480px; margin: 2rem auto; }
9
+ button { font-size: 1rem; padding: 0.5rem 1rem; margin: 0.25rem; cursor: pointer; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <h1>ClipNode – CDN</h1>
14
+ <p>Loads the library directly from jsDelivr. No bundler, no npm.</p>
15
+ <button id="play" type="button">▶ Play</button>
16
+ <button id="stop" type="button">■ Stop</button>
17
+ <p id="status">Click Play to start (loads a sample tone).</p>
18
+
19
+ <script type="module">
20
+ // Import the core library and processor from the CDN
21
+ import { ClipNode, getProcessorCdnUrl } from "https://cdn.jsdelivr.net/npm/@jadujoel/web-audio-clip-node@0.1.1/dist/lib.js";
22
+
23
+ const status = document.getElementById("status");
24
+ let ctx;
25
+ let clip;
26
+
27
+ // Generate a simple sine-wave AudioBuffer for demo purposes
28
+ function createToneBuffer(audioCtx, freq = 440, duration = 2) {
29
+ const length = audioCtx.sampleRate * duration;
30
+ const buf = audioCtx.createBuffer(1, length, audioCtx.sampleRate);
31
+ const data = buf.getChannelData(0);
32
+ for (let i = 0; i < length; i++) {
33
+ data[i] = Math.sin(2 * Math.PI * freq * i / audioCtx.sampleRate);
34
+ }
35
+ return buf;
36
+ }
37
+
38
+ document.getElementById("play").addEventListener("click", async () => {
39
+ if (!ctx) {
40
+ ctx = new AudioContext();
41
+ const processorUrl = getProcessorCdnUrl("0.1.1");
42
+ await ctx.audioWorklet.addModule(processorUrl);
43
+ clip = new ClipNode(ctx, {
44
+ processorOptions: { sampleRate: ctx.sampleRate },
45
+ });
46
+ clip.connect(ctx.destination);
47
+ clip.buffer = createToneBuffer(ctx);
48
+ }
49
+ clip.start();
50
+ status.textContent = "Playing…";
51
+ });
52
+
53
+ document.getElementById("stop").addEventListener("click", () => {
54
+ if (clip) {
55
+ clip.stop();
56
+ status.textContent = "Stopped.";
57
+ }
58
+ });
59
+ </script>
60
+ </body>
61
+ </html>
@@ -0,0 +1,8 @@
1
+ # ESM Bundler Example (Vite + TypeScript)
2
+
3
+ Uses `npm install` + Vite. The processor is loaded via `getProcessorBlobUrl()` (embedded, zero-config).
4
+
5
+ ```sh
6
+ npm install
7
+ npm run dev
8
+ ```
@@ -0,0 +1,12 @@
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 – ESM Bundler Example</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="./src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "esm-bundler-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build"
8
+ },
9
+ "dependencies": {
10
+ "@jadujoel/web-audio-clip-node": "^0.1.1"
11
+ },
12
+ "devDependencies": {
13
+ "vite": "^6"
14
+ }
15
+ }