@weasel-js/diagram 1.10.1 → 1.11.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.
@@ -0,0 +1,138 @@
1
+ import { GraphNodeDef, GraphTarget, GraphPatch, AnalyserTap, AudioEngine } from '@weasel-js/audio';
2
+ import { E as EffectPipeline, a as EffectRun, b as EffectStageType } from './stageTypes-DvnR8cKQ.js';
3
+ import { VisibleRafOptions } from '@weasel-js/react';
4
+ import '@weasel-js/core';
5
+ import '@weasel-js/core/math';
6
+
7
+ /** What a run supplies to its source stages. */
8
+ interface AudioSources {
9
+ audio?: AudioBuffer;
10
+ }
11
+ type Values = Readonly<Record<string, unknown>>;
12
+ /** How one stage type becomes a graph node: its settings, its levels, and the node it builds. */
13
+ interface AudioStageNode {
14
+ /** The stage's discrete settings, from its param values and the run's sources. Throws to fail the stage. */
15
+ settings(params: Values, sources: AudioSources): Values;
16
+ /** The ids of its numeric params, each a `params` key of the def `build` returns. */
17
+ levels: readonly string[];
18
+ build(ctx: BaseAudioContext, settings: Values, levels: Readonly<Record<string, number>>): GraphNodeDef;
19
+ }
20
+ /** The graph node of each stage type, by `type`. */
21
+ type AudioStageNodes = Readonly<Record<string, AudioStageNode>>;
22
+ /** The graph node each stage type builds; `audio.output` has none. */
23
+ declare const audioStageNodes: AudioStageNodes;
24
+
25
+ /** A node the plan builds: its stage type, its discrete settings and its numeric levels. */
26
+ interface PlanNode {
27
+ type: string;
28
+ settings: Readonly<Record<string, unknown>>;
29
+ levels: Readonly<Record<string, number>>;
30
+ }
31
+ /** A connection the plan makes, from a node to another node's port or to the graph's output. */
32
+ interface PlanEdge {
33
+ from: string;
34
+ to: GraphTarget;
35
+ }
36
+ /** The graph an audio pipeline should play: what to build, how it connects, and which stages it left out. */
37
+ interface LivePlan {
38
+ nodes: Readonly<Record<string, PlanNode>>;
39
+ /** Sorted by `edgeKey`, so equal plans compare equal. */
40
+ edges: readonly PlanEdge[];
41
+ /** Why a stage is not in `nodes`, by stage id. A stage left out for want of input has none. */
42
+ errors: Readonly<Record<string, string>>;
43
+ }
44
+ /** The plan of a graph holding nothing. */
45
+ declare const EMPTY_PLAN: LivePlan;
46
+ /** Compiles the audio stages of `pipeline`; stages of other types are ignored. */
47
+ declare function livePlan(pipeline: EffectPipeline, nodes: AudioStageNodes, sources: AudioSources): LivePlan;
48
+
49
+ /** Makes the def for a node that is added or replaced. */
50
+ type BuildNode = (id: string, node: PlanNode) => GraphNodeDef;
51
+ /** The patches that turn the graph `prev` describes into the one `next` describes. */
52
+ declare function diffPlans(prev: LivePlan, next: LivePlan, build: BuildNode): GraphPatch[];
53
+
54
+ /** What a stage's preview slot shows while its graph plays. */
55
+ type AudioPreview = 'off' | 'scope' | 'spectrum';
56
+ type Size = {
57
+ width: number;
58
+ height: number;
59
+ };
60
+ /** The waveform as a line across the bitmap, silence at mid-height. */
61
+ declare function drawScope(tap: AnalyserTap, size: Size): ImageBitmap;
62
+ /** The spectrum as filled bars rising from the bottom, low frequencies on the left. */
63
+ declare function drawSpectrum(tap: AnalyserTap, size: Size): ImageBitmap;
64
+
65
+ /** What a live runner last published: its run, and whether it plays. */
66
+ interface LiveAudioRunnerState {
67
+ run: EffectRun | undefined;
68
+ playing: boolean;
69
+ }
70
+ /** What a live runner plays, and through which engine. */
71
+ interface LiveAudioRunnerOptions {
72
+ engine: AudioEngine;
73
+ pipeline: EffectPipeline;
74
+ nodes?: AudioStageNodes;
75
+ sources?: AudioSources;
76
+ /** The engine bus the graph plays through. */
77
+ bus?: string;
78
+ }
79
+ /** Plays an audio pipeline through a patchable graph, editing the graph as the pipeline changes. */
80
+ interface LiveAudioRunner {
81
+ readonly state: LiveAudioRunnerState;
82
+ /** The graph follows `pipeline` while it plays, or while it does not. */
83
+ setPipeline(pipeline: EffectPipeline): void;
84
+ /** A new buffer replaces each file node. */
85
+ setSources(sources: AudioSources): void;
86
+ /** Unlocks the engine, then starts the graph. */
87
+ play(): Promise<void>;
88
+ pause(): void;
89
+ /** Which stages draw into their `out` preview: a scope, a spectrum, or none. Takes effect on the next `frame`. */
90
+ setPreviews(mode: AudioPreview): void;
91
+ /** Publishes a run with each tapped stage's preview drawn from its tap's current window. */
92
+ frame(): void;
93
+ /** Call after the render that shows `run`: the bitmaps of earlier runs are closed. */
94
+ drawn(run: EffectRun | undefined): void;
95
+ /** A fresh graph holding the current plan, not playing: files start again from 0. */
96
+ reset(): void;
97
+ subscribe(listener: () => void): () => void;
98
+ /** Disposes the graph; the runner does nothing after. The engine is the caller's. */
99
+ dispose(): void;
100
+ }
101
+ /** A runner over `opts.pipeline` holding a fresh graph on `opts.engine`, not playing. */
102
+ declare function createLiveAudioRunner(opts: LiveAudioRunnerOptions): LiveAudioRunner;
103
+
104
+ /** Stage types over audio, each with a graph node in `audioStageNodes` except
105
+ * `audio.output`, which is the graph's own output. Every `type` is prefixed
106
+ * because `IMAGE_STAGE_TYPES` already has an `oscillator`. */
107
+ declare const AUDIO_STAGE_TYPES: readonly EffectStageType[];
108
+
109
+ /** What `useFollowAudioRunner` keeps a runner following, and how its preview loop is gated. */
110
+ interface FollowAudioRunnerOptions extends Pick<VisibleRafOptions, 'target' | 'requestFrame' | 'cancelFrame'> {
111
+ pipeline: EffectPipeline;
112
+ sources?: AudioSources;
113
+ previews: AudioPreview;
114
+ /** The nodes the plan is read with while there is no runner. */
115
+ nodes?: AudioStageNodes;
116
+ }
117
+ /**
118
+ * Keeps a caller-owned `runner` following `pipeline`, `sources` and `previews`, draws its previews every
119
+ * visible frame while it plays, and closes the bitmaps of runs no render shows. While `runner` is null,
120
+ * the run holds the plan's errors, so a stage that cannot play says why before any engine exists.
121
+ */
122
+ declare function useFollowAudioRunner(runner: LiveAudioRunner | null, opts: FollowAudioRunnerOptions): LiveAudioRunnerState;
123
+
124
+ /** Options for `useLiveAudioRunner`: the runner's, which it follows as they change, and how its preview loop is gated. */
125
+ interface UseLiveAudioRunnerOptions extends LiveAudioRunnerOptions, Pick<FollowAudioRunnerOptions, 'previews' | 'target' | 'requestFrame' | 'cancelFrame'> {
126
+ }
127
+ /** What `useLiveAudioRunner` returns: the runner's state, and the runner to drive, null until it mounts. */
128
+ interface LiveAudioRunnerHandle extends LiveAudioRunnerState {
129
+ runner: LiveAudioRunner | null;
130
+ }
131
+ /**
132
+ * A runner made on mount and remade when `engine`, `bus` or `nodes` change, following `pipeline`,
133
+ * `sources` and `previews` through `useFollowAudioRunner`, and disposed on unmount. Before the first
134
+ * mount the runner is null and the run holds the plan's errors. The engine is the caller's.
135
+ */
136
+ declare function useLiveAudioRunner(opts: UseLiveAudioRunnerOptions): LiveAudioRunnerHandle;
137
+
138
+ export { AUDIO_STAGE_TYPES, type AudioPreview, type AudioSources, type AudioStageNode, type AudioStageNodes, type BuildNode, EMPTY_PLAN, type FollowAudioRunnerOptions, type LiveAudioRunner, type LiveAudioRunnerHandle, type LiveAudioRunnerOptions, type LiveAudioRunnerState, type LivePlan, type PlanEdge, type PlanNode, type UseLiveAudioRunnerOptions, audioStageNodes, createLiveAudioRunner, diffPlans, drawScope, drawSpectrum, livePlan, useFollowAudioRunner, useLiveAudioRunner };
@@ -0,0 +1,527 @@
1
+ import { formatParam, uniqueStages, runOrder, paramValues, PREVIEW_DENSITY, closeBitmaps, previewsOf, PREVIEW_SIZE } from './chunk-QKSWZEJH.js';
2
+ import './chunk-43LDZ6L4.js';
3
+ import './chunk-IM6DHWZO.js';
4
+ import { createMixNode, createFileSourceNode, compressorNode, reverbNode, delayNode, filterNode, createPanNode, createGainNode, createNoiseNode, createOscillatorNode } from '@weasel-js/audio';
5
+ import { useEffect, useSyncExternalStore, useMemo, useState } from 'react';
6
+ import { useVisibleRaf, useLatest } from '@weasel-js/react';
7
+
8
+ var node = (settingIds, levels, make) => ({
9
+ settings: (params) => Object.fromEntries(settingIds.filter((id) => params[id] !== void 0).map((id) => [id, params[id]])),
10
+ levels,
11
+ build: (ctx, settings, levels2) => make(ctx, { ...settings, ...levels2 })
12
+ });
13
+ var audioStageNodes = {
14
+ "audio.file": {
15
+ settings: (_params, sources) => {
16
+ if (!sources.audio) throw new Error("no source audio");
17
+ return { buffer: sources.audio };
18
+ },
19
+ levels: [],
20
+ build: (ctx, settings) => createFileSourceNode(ctx, { buffer: settings.buffer, loop: true })
21
+ },
22
+ "audio.oscillator": node(["wave"], ["frequency"], createOscillatorNode),
23
+ "audio.noise": node(["color"], [], createNoiseNode),
24
+ "audio.gain": node([], ["gain"], createGainNode),
25
+ "audio.pan": node([], ["pan"], createPanNode),
26
+ "audio.filter": node(["type"], ["frequency", "Q"], filterNode),
27
+ "audio.delay": node([], ["time", "feedback", "mix"], delayNode),
28
+ "audio.reverb": node(["seconds"], ["mix"], reverbNode),
29
+ "audio.compressor": node([], ["threshold", "ratio"], compressorNode),
30
+ "audio.mix": {
31
+ settings: () => ({}),
32
+ levels: ["a", "b"],
33
+ build: (ctx, _settings, levels) => {
34
+ const def = createMixNode(ctx, { inputs: ["a", "b"] });
35
+ for (const id of ["a", "b"]) if (levels[id] !== void 0) def.params[id](levels[id], 0);
36
+ return def;
37
+ }
38
+ }
39
+ };
40
+
41
+ // src/effect/audio/plan.ts
42
+ var EMPTY_PLAN = { nodes: {}, edges: [], errors: {} };
43
+ var OUTPUT_TYPE = "audio.output";
44
+ var edgeKey = (e) => JSON.stringify([e.from, "output" in e.to ? null : [e.to.node, e.to.port]]);
45
+ function levelsOf(levels, params) {
46
+ const out = {};
47
+ for (const id of levels) {
48
+ const v = params[id];
49
+ if (typeof v === "number") out[id] = v;
50
+ }
51
+ return out;
52
+ }
53
+ function livePlan(pipeline, nodes, sources) {
54
+ const stages = /* @__PURE__ */ new Map();
55
+ for (const s of uniqueStages(pipeline)) {
56
+ if (s.type !== void 0 && (Object.hasOwn(nodes, s.type) || s.type === OUTPUT_TYPE)) stages.set(s.id, s);
57
+ }
58
+ const { order, cyclic } = runOrder(pipeline);
59
+ const planNodes = {};
60
+ const errors = {};
61
+ for (const id of cyclic) if (stages.has(id)) errors[id] = "on a feedback loop";
62
+ const origins = /* @__PURE__ */ new Map();
63
+ const edges = /* @__PURE__ */ new Map();
64
+ for (const id of order) {
65
+ const stage = stages.get(id);
66
+ if (stage === void 0) continue;
67
+ const isOutput = stage.type === OUTPUT_TYPE;
68
+ const bypassed = stage.bypassed === true && !isOutput;
69
+ const firstAudio = (stage.inputs ?? []).find((i) => i.type === "audio")?.id;
70
+ const fed = pipeline.wires.filter((w) => w.to.stage === id && stages.has(w.from.stage) && (!bypassed || w.to.port === firstAudio)).flatMap((w) => (origins.get(w.from.stage) ?? []).map((from) => ({ from, port: w.to.port })));
71
+ if ((stage.inputs?.length ?? 0) > 0 && fed.length === 0) continue;
72
+ if (bypassed) {
73
+ origins.set(id, [...new Set(fed.map((f) => f.from))]);
74
+ continue;
75
+ }
76
+ if (!isOutput) {
77
+ const entry = nodes[stage.type];
78
+ const params = paramValues(stage);
79
+ try {
80
+ planNodes[id] = { type: stage.type, settings: entry.settings(params, sources), levels: levelsOf(entry.levels, params) };
81
+ } catch (err) {
82
+ errors[id] = err instanceof Error ? err.message : String(err);
83
+ continue;
84
+ }
85
+ origins.set(id, [id]);
86
+ }
87
+ for (const f of fed) {
88
+ const edge = { from: f.from, to: isOutput ? { output: true } : { node: id, port: f.port } };
89
+ edges.set(edgeKey(edge), edge);
90
+ }
91
+ }
92
+ return {
93
+ nodes: planNodes,
94
+ edges: [...edges.values()].sort((a, b) => {
95
+ const [ka, kb] = [edgeKey(a), edgeKey(b)];
96
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
97
+ }),
98
+ errors
99
+ };
100
+ }
101
+
102
+ // src/effect/audio/patches.ts
103
+ function sameSettings(a, b) {
104
+ const keys = Object.keys(a);
105
+ return keys.length === Object.keys(b).length && keys.every((k) => k in b && Object.is(a[k], b[k]));
106
+ }
107
+ var touches = (e, ids) => ids.has(e.from) || "node" in e.to && ids.has(e.to.node);
108
+ function diffPlans(prev, next, build) {
109
+ const prevIds = Object.keys(prev.nodes).sort();
110
+ const nextIds = Object.keys(next.nodes).sort();
111
+ const gone = /* @__PURE__ */ new Set();
112
+ const fresh = /* @__PURE__ */ new Set();
113
+ const groups = {
114
+ disconnect: [],
115
+ remove: [],
116
+ add: [],
117
+ replace: [],
118
+ set: [],
119
+ connect: []
120
+ };
121
+ for (const id of prevIds) {
122
+ const now = next.nodes[id];
123
+ if (now === void 0 || now.type !== prev.nodes[id].type) {
124
+ gone.add(id);
125
+ fresh.add(id);
126
+ groups.remove.push({ kind: "remove", id });
127
+ }
128
+ }
129
+ for (const id of nextIds) {
130
+ const now = next.nodes[id];
131
+ const was = prev.nodes[id];
132
+ if (was === void 0 || gone.has(id)) {
133
+ fresh.add(id);
134
+ groups.add.push({ kind: "add", id, def: build(id, now) });
135
+ } else if (!sameSettings(was.settings, now.settings)) {
136
+ groups.replace.push({ kind: "replace", id, def: build(id, now) });
137
+ } else {
138
+ for (const param2 of Object.keys(now.levels).sort()) {
139
+ const value = now.levels[param2];
140
+ if (!Object.is(was.levels[param2], value)) groups.set.push({ kind: "set", id, param: param2, value });
141
+ }
142
+ }
143
+ }
144
+ const nextKeys = new Set(next.edges.map(edgeKey));
145
+ const prevKeys = new Set(prev.edges.map(edgeKey));
146
+ for (const e of prev.edges) {
147
+ if (!nextKeys.has(edgeKey(e)) && !touches(e, gone)) {
148
+ groups.disconnect.push({ kind: "disconnect", from: e.from, to: e.to });
149
+ }
150
+ }
151
+ for (const e of next.edges) {
152
+ if (!prevKeys.has(edgeKey(e)) || touches(e, fresh)) groups.connect.push({ kind: "connect", from: e.from, to: e.to });
153
+ }
154
+ return [
155
+ ...groups.disconnect,
156
+ ...groups.remove,
157
+ ...groups.add,
158
+ ...groups.replace,
159
+ ...groups.set,
160
+ ...groups.connect
161
+ ];
162
+ }
163
+
164
+ // src/effect/audio/scope.ts
165
+ var INK = "#8a8f98";
166
+ var BANDS = 32;
167
+ var LINE = 1.5;
168
+ function canvasOf(size) {
169
+ const canvas = new OffscreenCanvas(
170
+ Math.max(1, Math.round(size.width * PREVIEW_DENSITY)),
171
+ Math.max(1, Math.round(size.height * PREVIEW_DENSITY))
172
+ );
173
+ return { canvas, g: canvas.getContext("2d"), w: canvas.width, h: canvas.height };
174
+ }
175
+ function drawScope(tap, size) {
176
+ const { canvas, g, w, h } = canvasOf(size);
177
+ const wave = tap.waveform();
178
+ g.strokeStyle = INK;
179
+ g.lineWidth = LINE * PREVIEW_DENSITY;
180
+ g.lineJoin = "round";
181
+ g.beginPath();
182
+ for (let x = 0; x < w; x += 1) {
183
+ const v = wave[Math.min(wave.length - 1, Math.floor(x / Math.max(1, w - 1) * (wave.length - 1)))] / 255;
184
+ const y = h - v * h;
185
+ if (x === 0) g.moveTo(x, y);
186
+ else g.lineTo(x, y);
187
+ }
188
+ g.stroke();
189
+ return canvas.transferToImageBitmap();
190
+ }
191
+ function drawSpectrum(tap, size) {
192
+ const { canvas, g, w, h } = canvasOf(size);
193
+ const bands = tap.bands(BANDS);
194
+ const slot = w / BANDS;
195
+ g.fillStyle = INK;
196
+ for (let i = 0; i < BANDS; i += 1) {
197
+ const bar = bands[i] * h;
198
+ g.fillRect(i * slot, h - bar, slot * 0.8, bar);
199
+ }
200
+ return canvas.transferToImageBitmap();
201
+ }
202
+
203
+ // src/effect/audio/livePreviews.ts
204
+ var errorsRun = (errors) => ({ stages: Object.fromEntries(Object.entries(errors).map(([id, error]) => [id, { error }])) });
205
+ function createLivePreviews() {
206
+ let mode = "off";
207
+ let taps = /* @__PURE__ */ new Map();
208
+ let published = [];
209
+ const tapAll = (graph, ids) => {
210
+ for (const id of ids) if (!taps.has(id)) taps.set(id, graph.tap(id));
211
+ };
212
+ const disposeTaps = () => {
213
+ for (const t of taps.values()) t.dispose();
214
+ taps = /* @__PURE__ */ new Map();
215
+ };
216
+ return {
217
+ mode: () => mode,
218
+ setMode(next, graph, ids) {
219
+ mode = next;
220
+ if (next === "off") disposeTaps();
221
+ else tapAll(graph, ids);
222
+ },
223
+ follow(graph, removed, planned) {
224
+ if (mode === "off") return;
225
+ for (const id of removed) taps.delete(id);
226
+ const live = new Set(planned);
227
+ for (const id of [...taps.keys()]) if (!live.has(id)) taps.delete(id);
228
+ tapAll(graph, planned);
229
+ },
230
+ restart() {
231
+ taps = /* @__PURE__ */ new Map();
232
+ },
233
+ draw(errors) {
234
+ if (mode === "off" || taps.size === 0) return void 0;
235
+ const paint = mode === "scope" ? drawScope : drawSpectrum;
236
+ const stages = { ...errorsRun(errors).stages };
237
+ for (const [id, tap] of taps) {
238
+ stages[id] = { ...stages[id], previews: { out: paint(tap, { width: PREVIEW_SIZE, height: PREVIEW_SIZE }) } };
239
+ }
240
+ return { stages };
241
+ },
242
+ track(run) {
243
+ published.push(run);
244
+ },
245
+ drawn(run) {
246
+ const at = run === void 0 ? published.length : published.indexOf(run);
247
+ if (at <= 0) return;
248
+ const keep = new Set(run === void 0 ? [] : previewsOf(run));
249
+ closeBitmaps(published.slice(0, at).flatMap(previewsOf).filter((b) => !keep.has(b)));
250
+ published = published.slice(at);
251
+ },
252
+ dispose() {
253
+ disposeTaps();
254
+ closeBitmaps(published.flatMap(previewsOf));
255
+ published = [];
256
+ }
257
+ };
258
+ }
259
+
260
+ // src/effect/audio/liveRunner.ts
261
+ var messageOf = (err) => err instanceof Error ? err.message : String(err);
262
+ var idsOf = (p) => "id" in p ? [p.id] : [p.from, ..."node" in p.to ? [p.to.node] : []];
263
+ var failingStage = (patches, message) => patches.flatMap(idsOf).find((id) => message.includes(`"${id}"`));
264
+ var sameErrors = (a, b) => {
265
+ const keys = Object.keys(a);
266
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
267
+ };
268
+ function applySets(graph, sets, failures) {
269
+ if (sets.length === 0) return;
270
+ try {
271
+ graph.apply(sets);
272
+ } catch (err) {
273
+ console.warn(`@weasel-js/diagram: audio graph edit failed: ${messageOf(err)}`);
274
+ for (const p of sets) {
275
+ try {
276
+ graph.apply([p]);
277
+ } catch (one) {
278
+ if (p.kind === "set") failures[p.id] = messageOf(one);
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ }
284
+ function createLiveAudioRunner(opts) {
285
+ const { engine, bus } = opts;
286
+ const nodes = opts.nodes ?? audioStageNodes;
287
+ let pipeline = opts.pipeline;
288
+ let sources = opts.sources ?? {};
289
+ let graph = engine.graph(bus);
290
+ let plan = EMPTY_PLAN;
291
+ let errors = {};
292
+ let state = { run: { stages: {} }, playing: false };
293
+ let disposed = false;
294
+ const listeners = /* @__PURE__ */ new Set();
295
+ const previews = createLivePreviews();
296
+ const set = (next) => {
297
+ const merged = { ...state, ...next };
298
+ if (merged.run === state.run && merged.playing === state.playing) return;
299
+ if (merged.run !== state.run && merged.run !== void 0) previews.track(merged.run);
300
+ state = merged;
301
+ for (const l of [...listeners]) l();
302
+ };
303
+ const publishErrors = (next) => {
304
+ if (sameErrors(errors, next)) return;
305
+ errors = next;
306
+ set({ run: errorsRun(next) });
307
+ };
308
+ const sync = (from) => {
309
+ const next = livePlan(pipeline, nodes, sources);
310
+ const merged = { ...next.errors };
311
+ let adopted = next;
312
+ let structural = [];
313
+ let removed = [];
314
+ let sets = [];
315
+ try {
316
+ const patches = diffPlans(from, next, (_id, n) => nodes[n.type].build(engine.context, n.settings, n.levels));
317
+ structural = patches.filter((p) => p.kind !== "set");
318
+ sets = patches.filter((p) => p.kind === "set");
319
+ if (structural.length > 0) graph.apply(structural);
320
+ removed = structural.flatMap((p) => p.kind === "remove" ? [p.id] : []);
321
+ } catch (err) {
322
+ const message = messageOf(err);
323
+ const id = failingStage(structural, message);
324
+ if (id !== void 0) merged[id] = message;
325
+ console.warn(`@weasel-js/diagram: audio graph edit failed: ${message}`);
326
+ adopted = from;
327
+ sets = [];
328
+ }
329
+ applySets(graph, sets, merged);
330
+ plan = adopted;
331
+ previews.follow(graph, removed, Object.keys(adopted.nodes));
332
+ publishErrors(merged);
333
+ };
334
+ sync(EMPTY_PLAN);
335
+ return {
336
+ get state() {
337
+ return state;
338
+ },
339
+ setPipeline(next) {
340
+ if (disposed) return;
341
+ pipeline = next;
342
+ sync(plan);
343
+ },
344
+ setSources(next) {
345
+ if (disposed) return;
346
+ sources = next;
347
+ sync(plan);
348
+ },
349
+ async play() {
350
+ if (disposed) return;
351
+ await engine.unlock();
352
+ if (disposed) return;
353
+ graph.start();
354
+ set({ playing: true });
355
+ },
356
+ pause() {
357
+ if (disposed) return;
358
+ graph.stop();
359
+ set({ playing: false });
360
+ },
361
+ setPreviews(mode) {
362
+ if (disposed) return;
363
+ previews.setMode(mode, graph, Object.keys(plan.nodes));
364
+ if (mode === "off") set({ run: errorsRun(errors) });
365
+ },
366
+ frame() {
367
+ if (disposed) return;
368
+ const run = previews.draw(errors);
369
+ if (run !== void 0) set({ run });
370
+ },
371
+ drawn: previews.drawn,
372
+ reset() {
373
+ if (disposed) return;
374
+ graph.dispose();
375
+ graph = engine.graph(bus);
376
+ previews.restart();
377
+ sync(EMPTY_PLAN);
378
+ set({ playing: false });
379
+ },
380
+ subscribe(listener) {
381
+ listeners.add(listener);
382
+ return () => {
383
+ listeners.delete(listener);
384
+ };
385
+ },
386
+ dispose() {
387
+ if (disposed) return;
388
+ disposed = true;
389
+ previews.dispose();
390
+ graph.dispose();
391
+ }
392
+ };
393
+ }
394
+
395
+ // src/effect/audio/stageTypes.ts
396
+ var param = (id, control) => ({ id, label: id, value: formatParam(control), control });
397
+ var amount = (value, min, max, step, unit) => ({ kind: "number", value, min, max, step, ...unit !== void 0 ? { unit } : {} });
398
+ var choice = (value, ...options) => ({ kind: "enum", value, options: options.map((o) => ({ value: o, label: o[0].toUpperCase() + o.slice(1) })) });
399
+ var port = (id) => ({ id, type: "audio" });
400
+ var IN = [port("in")];
401
+ var OUT = [port("out")];
402
+ var AUDIO_STAGE_TYPES = [
403
+ { type: "audio.file", title: "File", kind: "source", outputs: OUT },
404
+ {
405
+ type: "audio.oscillator",
406
+ title: "Oscillator",
407
+ kind: "source",
408
+ outputs: OUT,
409
+ params: [
410
+ param("wave", choice("sine", "sine", "square", "sawtooth", "triangle")),
411
+ param("frequency", amount(440, 20, 2e4, 1, "Hz"))
412
+ ]
413
+ },
414
+ {
415
+ type: "audio.noise",
416
+ title: "Noise",
417
+ kind: "source",
418
+ outputs: OUT,
419
+ params: [param("color", choice("white", "white", "pink", "brown"))]
420
+ },
421
+ { type: "audio.gain", title: "Gain", inputs: IN, outputs: OUT, params: [param("gain", amount(1, 0, 2, 0.01))] },
422
+ { type: "audio.pan", title: "Pan", inputs: IN, outputs: OUT, params: [param("pan", amount(0, -1, 1, 0.01))] },
423
+ {
424
+ type: "audio.filter",
425
+ title: "Filter",
426
+ inputs: IN,
427
+ outputs: OUT,
428
+ params: [
429
+ param("type", choice("lowpass", "lowpass", "highpass", "bandpass")),
430
+ param("frequency", amount(1e3, 20, 2e4, 1, "Hz")),
431
+ param("Q", amount(1, 0.1, 20, 0.1))
432
+ ]
433
+ },
434
+ {
435
+ type: "audio.delay",
436
+ title: "Delay",
437
+ inputs: IN,
438
+ outputs: OUT,
439
+ params: [
440
+ param("time", amount(300, 0, 2e3, 10, "ms")),
441
+ param("feedback", amount(0.4, 0, 0.95, 0.01)),
442
+ param("mix", amount(0.5, 0, 1, 0.01))
443
+ ]
444
+ },
445
+ {
446
+ type: "audio.reverb",
447
+ title: "Reverb",
448
+ inputs: IN,
449
+ outputs: OUT,
450
+ params: [param("seconds", amount(2, 0.1, 10, 0.1, "s")), param("mix", amount(0.3, 0, 1, 0.01))]
451
+ },
452
+ {
453
+ type: "audio.compressor",
454
+ title: "Compressor",
455
+ inputs: IN,
456
+ outputs: OUT,
457
+ params: [param("threshold", amount(-24, -60, 0, 1, "dB")), param("ratio", amount(4, 1, 20, 0.5))]
458
+ },
459
+ {
460
+ type: "audio.mix",
461
+ title: "Mix",
462
+ inputs: [port("a"), port("b")],
463
+ outputs: OUT,
464
+ params: [param("a", amount(1, 0, 2, 0.01)), param("b", amount(1, 0, 2, 0.01))]
465
+ },
466
+ { type: "audio.output", title: "Output", kind: "sink", inputs: IN }
467
+ ];
468
+ var noSubscribe = () => () => {
469
+ };
470
+ function useFollowAudioRunner(runner, opts) {
471
+ const { pipeline, sources, previews, nodes, target, requestFrame, cancelFrame } = opts;
472
+ useEffect(() => {
473
+ runner?.setPipeline(pipeline);
474
+ }, [runner, pipeline]);
475
+ useEffect(() => {
476
+ if (sources !== void 0) runner?.setSources(sources);
477
+ }, [runner, sources]);
478
+ useEffect(() => {
479
+ runner?.setPreviews(previews);
480
+ }, [runner, previews]);
481
+ const live = useSyncExternalStore(runner?.subscribe ?? noSubscribe, () => runner?.state ?? null);
482
+ const planned = useMemo(
483
+ () => runner !== null ? void 0 : errorsRun(livePlan(pipeline, nodes ?? audioStageNodes, sources ?? {}).errors),
484
+ [runner, pipeline, nodes, sources]
485
+ );
486
+ const playing = live?.playing ?? false;
487
+ const loop = useVisibleRaf((_now, self) => {
488
+ if (runner === null || !runner.state.playing || previews === "off") return;
489
+ runner.frame();
490
+ self.request();
491
+ }, {
492
+ ...target !== void 0 ? { target } : {},
493
+ ...requestFrame !== void 0 ? { requestFrame } : {},
494
+ ...cancelFrame !== void 0 ? { cancelFrame } : {}
495
+ });
496
+ useEffect(() => {
497
+ if (playing && previews !== "off") loop.request();
498
+ else loop.cancel();
499
+ }, [playing, previews, loop]);
500
+ useEffect(() => {
501
+ runner?.drawn(live?.run);
502
+ }, [runner, live?.run]);
503
+ return live ?? { run: planned, playing: false };
504
+ }
505
+ function useLiveAudioRunner(opts) {
506
+ const { engine, bus, nodes } = opts;
507
+ const latest = useLatest(opts);
508
+ const [runner, setRunner] = useState(null);
509
+ useEffect(() => {
510
+ const { pipeline, sources } = latest.current;
511
+ const made = createLiveAudioRunner({
512
+ engine,
513
+ pipeline,
514
+ ...nodes !== void 0 ? { nodes } : {},
515
+ ...sources !== void 0 ? { sources } : {},
516
+ ...bus !== void 0 ? { bus } : {}
517
+ });
518
+ setRunner(made);
519
+ return () => made.dispose();
520
+ }, [engine, bus, nodes, latest]);
521
+ const state = useFollowAudioRunner(runner, opts);
522
+ return { ...state, runner };
523
+ }
524
+
525
+ export { AUDIO_STAGE_TYPES, EMPTY_PLAN, audioStageNodes, createLiveAudioRunner, diffPlans, drawScope, drawSpectrum, livePlan, useFollowAudioRunner, useLiveAudioRunner };
526
+ //# sourceMappingURL=effect-audio.js.map
527
+ //# sourceMappingURL=effect-audio.js.map