@litlab/audx 0.0.1 → 0.5.5

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 (81) hide show
  1. package/README.md +96 -53
  2. package/dist/bin.js +1212 -0
  3. package/dist/cc-DgCkkqq8.js +13 -0
  4. package/dist/cc-he3fHS3P.js +12 -0
  5. package/dist/index.d.ts +723 -3
  6. package/dist/index.js +1534 -126
  7. package/dist/react.d.ts +583 -0
  8. package/dist/react.js +1556 -0
  9. package/package.json +64 -39
  10. package/schemas/pack.schema.json +4 -0
  11. package/schemas/patch.schema.json +857 -0
  12. package/dist/codegen/theme-codegen.d.ts +0 -12
  13. package/dist/codegen/theme-codegen.d.ts.map +0 -1
  14. package/dist/codegen/theme-codegen.js +0 -153
  15. package/dist/codegen/theme-codegen.js.map +0 -1
  16. package/dist/commands/add.d.ts +0 -2
  17. package/dist/commands/add.d.ts.map +0 -1
  18. package/dist/commands/add.js +0 -120
  19. package/dist/commands/add.js.map +0 -1
  20. package/dist/commands/diff.d.ts +0 -2
  21. package/dist/commands/diff.d.ts.map +0 -1
  22. package/dist/commands/diff.js +0 -103
  23. package/dist/commands/diff.js.map +0 -1
  24. package/dist/commands/generate.d.ts +0 -12
  25. package/dist/commands/generate.d.ts.map +0 -1
  26. package/dist/commands/generate.js +0 -96
  27. package/dist/commands/generate.js.map +0 -1
  28. package/dist/commands/init.d.ts +0 -2
  29. package/dist/commands/init.d.ts.map +0 -1
  30. package/dist/commands/init.js +0 -79
  31. package/dist/commands/init.js.map +0 -1
  32. package/dist/commands/list.d.ts +0 -14
  33. package/dist/commands/list.d.ts.map +0 -1
  34. package/dist/commands/list.js +0 -93
  35. package/dist/commands/list.js.map +0 -1
  36. package/dist/commands/remove.d.ts +0 -2
  37. package/dist/commands/remove.d.ts.map +0 -1
  38. package/dist/commands/remove.js +0 -71
  39. package/dist/commands/remove.js.map +0 -1
  40. package/dist/commands/theme.d.ts +0 -31
  41. package/dist/commands/theme.d.ts.map +0 -1
  42. package/dist/commands/theme.js +0 -142
  43. package/dist/commands/theme.js.map +0 -1
  44. package/dist/commands/update.d.ts +0 -2
  45. package/dist/commands/update.d.ts.map +0 -1
  46. package/dist/commands/update.js +0 -123
  47. package/dist/commands/update.js.map +0 -1
  48. package/dist/core/alias-resolver.d.ts +0 -24
  49. package/dist/core/alias-resolver.d.ts.map +0 -1
  50. package/dist/core/alias-resolver.js +0 -87
  51. package/dist/core/alias-resolver.js.map +0 -1
  52. package/dist/core/config.d.ts +0 -21
  53. package/dist/core/config.d.ts.map +0 -1
  54. package/dist/core/config.js +0 -43
  55. package/dist/core/config.js.map +0 -1
  56. package/dist/core/file-writer.d.ts +0 -14
  57. package/dist/core/file-writer.d.ts.map +0 -1
  58. package/dist/core/file-writer.js +0 -90
  59. package/dist/core/file-writer.js.map +0 -1
  60. package/dist/core/package-manager.d.ts +0 -3
  61. package/dist/core/package-manager.d.ts.map +0 -1
  62. package/dist/core/package-manager.js +0 -17
  63. package/dist/core/package-manager.js.map +0 -1
  64. package/dist/core/registry.d.ts +0 -18
  65. package/dist/core/registry.d.ts.map +0 -1
  66. package/dist/core/registry.js +0 -69
  67. package/dist/core/registry.js.map +0 -1
  68. package/dist/core/theme-manager.d.ts +0 -35
  69. package/dist/core/theme-manager.d.ts.map +0 -1
  70. package/dist/core/theme-manager.js +0 -94
  71. package/dist/core/theme-manager.js.map +0 -1
  72. package/dist/core/utils.d.ts +0 -22
  73. package/dist/core/utils.d.ts.map +0 -1
  74. package/dist/core/utils.js +0 -44
  75. package/dist/core/utils.js.map +0 -1
  76. package/dist/index.d.ts.map +0 -1
  77. package/dist/index.js.map +0 -1
  78. package/dist/types.d.ts +0 -116
  79. package/dist/types.d.ts.map +0 -1
  80. package/dist/types.js +0 -43
  81. package/dist/types.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,126 +1,1534 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- import { Command } from "commander";
4
- const require = createRequire(import.meta.url);
5
- const pkg = require("../package.json");
6
- const program = new Command();
7
- program.name("audx").version(pkg.version).description(pkg.description);
8
- // ── init ────────────────────────────────────────────────────────────────────
9
- program
10
- .command("init")
11
- .description("Initialize audx in the current project")
12
- .action(async () => {
13
- const { initCommand } = await import("./commands/init.js");
14
- await initCommand(process.cwd());
15
- });
16
- // ── add ─────────────────────────────────────────────────────────────────────
17
- program
18
- .command("add")
19
- .description("Add sounds from the audx registry")
20
- .argument("<sounds...>", "Sound names to add")
21
- .action(async (sounds) => {
22
- const { addCommand } = await import("./commands/add.js");
23
- await addCommand(sounds, process.cwd());
24
- });
25
- // ── list ────────────────────────────────────────────────────────────────────
26
- program
27
- .command("list")
28
- .description("List available sounds in the registry")
29
- .option("--tag <tag>", "Filter by tag")
30
- .option("--search <query>", "Search by name, description, or tags")
31
- .action(async (options) => {
32
- const { listCommand } = await import("./commands/list.js");
33
- await listCommand(process.cwd(), options);
34
- });
35
- // ── remove ──────────────────────────────────────────────────────────────────
36
- program
37
- .command("remove")
38
- .description("Remove installed sounds")
39
- .argument("<sounds...>", "Sound names to remove")
40
- .action(async (sounds) => {
41
- const { removeCommand } = await import("./commands/remove.js");
42
- await removeCommand(sounds, process.cwd());
43
- });
44
- // ── diff ────────────────────────────────────────────────────────────────────
45
- program
46
- .command("diff")
47
- .description("Check for updates to installed sounds")
48
- .action(async () => {
49
- const { diffCommand } = await import("./commands/diff.js");
50
- await diffCommand(process.cwd());
51
- });
52
- // ── update ──────────────────────────────────────────────────────────────────
53
- program
54
- .command("update")
55
- .description("Update installed sounds from the registry")
56
- .argument("[sound-name]", "Specific sound to update")
57
- .action(async (soundName) => {
58
- const { updateCommand } = await import("./commands/update.js");
59
- await updateCommand(soundName, process.cwd());
60
- });
61
- // ── generate ────────────────────────────────────────────────────────────────
62
- program
63
- .command("generate")
64
- .description("Generate a sound from a text prompt")
65
- .argument("<prompt>", "Text prompt describing the sound")
66
- .option("--name <name>", "Custom name for the generated sound")
67
- .option("--duration <seconds>", "Duration in seconds (0.5–22)")
68
- .action(async (prompt, options) => {
69
- const { generateCommand } = await import("./commands/generate.js");
70
- await generateCommand(prompt, options, process.cwd());
71
- });
72
- // ── theme (parent with subcommands) ────────────────────────────────────────
73
- const theme = program.command("theme").description("Manage sound themes");
74
- theme
75
- .command("init")
76
- .description("Initialize theme configuration")
77
- .action(async () => {
78
- const { themeInitCommand } = await import("./commands/theme.js");
79
- await themeInitCommand(process.cwd());
80
- });
81
- theme
82
- .command("set")
83
- .description("Set the active theme")
84
- .argument("<theme-name>", "Theme name to activate")
85
- .action(async (themeName) => {
86
- const { themeSetCommand } = await import("./commands/theme.js");
87
- themeSetCommand(themeName, process.cwd());
88
- });
89
- theme
90
- .command("map")
91
- .description("Map a semantic name to an installed sound")
92
- .argument("<semantic-name>", "Semantic sound name (e.g., success, error)")
93
- .argument("<sound-name>", "Installed sound name")
94
- .action(async (semanticName, soundName) => {
95
- const { themeMapCommand } = await import("./commands/theme.js");
96
- themeMapCommand(semanticName, soundName, process.cwd());
97
- });
98
- theme
99
- .command("create")
100
- .description("Create a new theme")
101
- .argument("<theme-name>", "Name for the new theme")
102
- .action(async (themeName) => {
103
- const { themeCreateCommand } = await import("./commands/theme.js");
104
- themeCreateCommand(themeName, process.cwd());
105
- });
106
- theme
107
- .command("list")
108
- .description("List all themes")
109
- .action(async () => {
110
- const { themeListCommand } = await import("./commands/theme.js");
111
- themeListCommand(process.cwd());
112
- });
113
- theme
114
- .command("generate")
115
- .description("Generate sound-theme.ts from theme configuration")
116
- .action(async () => {
117
- const { themeGenerateCommand } = await import("./commands/theme.js");
118
- themeGenerateCommand(process.cwd());
119
- });
120
- // ── Parse and run ───────────────────────────────────────────────────────────
121
- // Requirement 1.6 — display help when invoked without subcommand
122
- if (process.argv.length <= 2) {
123
- program.help();
124
- }
125
- program.parse();
126
- //# sourceMappingURL=index.js.map
1
+ let ctx = null;
2
+ let masterGain = null;
3
+ let storedOptions = {};
4
+ /**
5
+ * Returns the shared `AudioContext`, creating one if needed.
6
+ *
7
+ * If the context is suspended (e.g. before a user gesture), it will be
8
+ * resumed automatically. Pass `options` on first call to configure latency
9
+ * and sample rate.
10
+ *
11
+ * @param options - Context creation options (stored for future calls)
12
+ * @returns The shared `AudioContext`
13
+ */ function getContext(options) {
14
+ if (options) {
15
+ storedOptions = options;
16
+ }
17
+ if (!ctx || ctx.state === "closed") {
18
+ ctx = new AudioContext({
19
+ latencyHint: storedOptions.latencyHint,
20
+ sampleRate: storedOptions.sampleRate
21
+ });
22
+ masterGain = null;
23
+ }
24
+ if (ctx.state === "suspended") {
25
+ ctx.resume();
26
+ }
27
+ return ctx;
28
+ }
29
+ /**
30
+ * Ensures the `AudioContext` is running and ready for playback.
31
+ *
32
+ * Unlike {@link getContext}, this awaits the `resume()` promise so the
33
+ * caller can be certain audio output is active before proceeding.
34
+ *
35
+ * @param options - Context creation options
36
+ * @returns A promise that resolves to the active `AudioContext`
37
+ */ async function ensureReady(options) {
38
+ const audio = getContext(options);
39
+ if (audio.state === "suspended") {
40
+ await audio.resume();
41
+ }
42
+ return audio;
43
+ }
44
+ /**
45
+ * Closes the shared `AudioContext` and releases all associated resources.
46
+ *
47
+ * After calling this, the next call to {@link getContext} will create a
48
+ * fresh context.
49
+ */ function dispose() {
50
+ if (ctx) {
51
+ ctx.close();
52
+ ctx = null;
53
+ masterGain = null;
54
+ }
55
+ }
56
+ /**
57
+ * Returns the master bus `GainNode`, creating it on first access.
58
+ *
59
+ * The master bus sits between all sound output and `ctx.destination`,
60
+ * providing a single point to control global volume.
61
+ */ function getMasterBus() {
62
+ const c = getContext();
63
+ if (!masterGain || masterGain.context !== c) {
64
+ masterGain = c.createGain();
65
+ masterGain.connect(c.destination);
66
+ }
67
+ return masterGain;
68
+ }
69
+ /**
70
+ * Returns the appropriate destination node for sound output.
71
+ *
72
+ * If a master bus has been created, routes through it; otherwise falls
73
+ * back to `ctx.destination`.
74
+ */ function getDestination() {
75
+ const c = getContext();
76
+ if (masterGain && masterGain.context === c) {
77
+ return masterGain;
78
+ }
79
+ return c.destination;
80
+ }
81
+ /**
82
+ * Sets the master volume for all audio output.
83
+ *
84
+ * @param volume - Linear gain value (0 = silent, 1 = unity)
85
+ */ function setMasterVolume(volume) {
86
+ getMasterBus().gain.value = volume;
87
+ }
88
+ /**
89
+ * Configures the 3D audio listener position and orientation.
90
+ *
91
+ * @param listener - Position and orientation values
92
+ * @see {@link getListener}
93
+ */ function setListener(listener) {
94
+ var _listener_forwardX, _listener_forwardY, _listener_forwardZ, _listener_upX, _listener_upY, _listener_upZ;
95
+ const audio = getContext();
96
+ const l = audio.listener;
97
+ l.positionX.value = listener.positionX;
98
+ l.positionY.value = listener.positionY;
99
+ l.positionZ.value = listener.positionZ;
100
+ l.forwardX.value = (_listener_forwardX = listener.forwardX) != null ? _listener_forwardX : 0;
101
+ l.forwardY.value = (_listener_forwardY = listener.forwardY) != null ? _listener_forwardY : 0;
102
+ l.forwardZ.value = (_listener_forwardZ = listener.forwardZ) != null ? _listener_forwardZ : -1;
103
+ l.upX.value = (_listener_upX = listener.upX) != null ? _listener_upX : 0;
104
+ l.upY.value = (_listener_upY = listener.upY) != null ? _listener_upY : 1;
105
+ l.upZ.value = (_listener_upZ = listener.upZ) != null ? _listener_upZ : 0;
106
+ }
107
+ /**
108
+ * Reads the current 3D audio listener position and orientation.
109
+ *
110
+ * @returns A snapshot of the listener's spatial parameters
111
+ * @see {@link setListener}
112
+ */ function getListener() {
113
+ const audio = getContext();
114
+ const l = audio.listener;
115
+ return {
116
+ positionX: l.positionX.value,
117
+ positionY: l.positionY.value,
118
+ positionZ: l.positionZ.value,
119
+ forwardX: l.forwardX.value,
120
+ forwardY: l.forwardY.value,
121
+ forwardZ: l.forwardZ.value,
122
+ upX: l.upX.value,
123
+ upY: l.upY.value,
124
+ upZ: l.upZ.value
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Creates a standalone {@link AudioAnalyser}.
130
+ *
131
+ * The caller is responsible for connecting a source to `analyser.node`.
132
+ * Call `analyser.dispose()` when finished to disconnect.
133
+ *
134
+ * @param opts - FFT size, smoothing, and dB range overrides
135
+ */ function createAnalyser(opts) {
136
+ var _ref, _ref1;
137
+ const ctx = getContext();
138
+ const node = ctx.createAnalyser();
139
+ node.fftSize = (_ref = opts == null ? void 0 : opts.fftSize) != null ? _ref : 2048;
140
+ node.smoothingTimeConstant = (_ref1 = opts == null ? void 0 : opts.smoothingTimeConstant) != null ? _ref1 : 0.8;
141
+ if ((opts == null ? void 0 : opts.minDecibels) !== undefined) node.minDecibels = opts.minDecibels;
142
+ if ((opts == null ? void 0 : opts.maxDecibels) !== undefined) node.maxDecibels = opts.maxDecibels;
143
+ const freqData = new Uint8Array(node.frequencyBinCount);
144
+ const timeData = new Uint8Array(node.fftSize);
145
+ const floatFreqData = new Float32Array(node.frequencyBinCount);
146
+ const floatTimeData = new Float32Array(node.fftSize);
147
+ return {
148
+ node,
149
+ frequencyBinCount: node.frequencyBinCount,
150
+ getFrequencyData () {
151
+ node.getByteFrequencyData(freqData);
152
+ return freqData;
153
+ },
154
+ getTimeDomainData () {
155
+ node.getByteTimeDomainData(timeData);
156
+ return timeData;
157
+ },
158
+ getFloatFrequencyData () {
159
+ node.getFloatFrequencyData(floatFreqData);
160
+ return floatFreqData;
161
+ },
162
+ getFloatTimeDomainData () {
163
+ node.getFloatTimeDomainData(floatTimeData);
164
+ return floatTimeData;
165
+ },
166
+ dispose () {
167
+ try {
168
+ node.disconnect();
169
+ } catch (_) {}
170
+ }
171
+ };
172
+ }
173
+ /**
174
+ * Creates an {@link AudioAnalyser} that is pre-connected to the master bus.
175
+ *
176
+ * Useful for visualising the combined output of all sounds.
177
+ * The returned analyser automatically disconnects from the master bus on
178
+ * `dispose()`.
179
+ *
180
+ * @param opts - FFT size, smoothing, and dB range overrides
181
+ */ function createMasterAnalyser(opts) {
182
+ const bus = getMasterBus();
183
+ const analyser = createAnalyser(opts);
184
+ bus.connect(analyser.node);
185
+ const originalDispose = analyser.dispose;
186
+ analyser.dispose = ()=>{
187
+ try {
188
+ bus.disconnect(analyser.node);
189
+ } catch (_) {}
190
+ originalDispose();
191
+ };
192
+ return analyser;
193
+ }
194
+
195
+ function withMix(ctx, mix, // biome-ignore lint/suspicious/noConfusingVoidType: callers may omit return
196
+ create) {
197
+ const input = ctx.createGain();
198
+ const output = ctx.createGain();
199
+ const dry = ctx.createGain();
200
+ dry.gain.value = 1 - mix;
201
+ input.connect(dry);
202
+ dry.connect(output);
203
+ const wet = ctx.createGain();
204
+ wet.gain.value = mix;
205
+ input.connect(wet);
206
+ const wetOut = ctx.createGain();
207
+ wetOut.connect(output);
208
+ const result = create(wet, wetOut);
209
+ return {
210
+ input,
211
+ output,
212
+ dispose: result == null ? void 0 : result.dispose
213
+ };
214
+ }
215
+ function createReverb(ctx, opts) {
216
+ var _opts_decay, _opts_mix, _opts_preDelay, _opts_damping, _opts_roomSize;
217
+ const decay = (_opts_decay = opts.decay) != null ? _opts_decay : 0.5;
218
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.3;
219
+ const preDelay = (_opts_preDelay = opts.preDelay) != null ? _opts_preDelay : 0;
220
+ const damping = (_opts_damping = opts.damping) != null ? _opts_damping : 0;
221
+ const roomSize = (_opts_roomSize = opts.roomSize) != null ? _opts_roomSize : 1;
222
+ return withMix(ctx, mix, (wet, wetOut)=>{
223
+ const sampleRate = ctx.sampleRate;
224
+ const effectiveDecay = decay * roomSize;
225
+ const length = Math.ceil(sampleRate * effectiveDecay);
226
+ const buffer = ctx.createBuffer(2, length, sampleRate);
227
+ for(let ch = 0; ch < 2; ch++){
228
+ const data = buffer.getChannelData(ch);
229
+ for(let i = 0; i < length; i++){
230
+ data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (length * 0.28));
231
+ }
232
+ }
233
+ if (damping > 0) {
234
+ for(let ch = 0; ch < 2; ch++){
235
+ const data = buffer.getChannelData(ch);
236
+ const coeff = Math.min(damping, 0.99);
237
+ let prev = 0;
238
+ for(let i = 0; i < length; i++){
239
+ prev = data[i] * (1 - coeff) + prev * coeff;
240
+ data[i] = prev;
241
+ }
242
+ }
243
+ }
244
+ const convolver = ctx.createConvolver();
245
+ convolver.buffer = buffer;
246
+ if (preDelay > 0) {
247
+ const preDelayNode = ctx.createDelay(Math.max(preDelay + 0.01, 1));
248
+ preDelayNode.delayTime.value = preDelay;
249
+ wet.connect(preDelayNode);
250
+ preDelayNode.connect(convolver);
251
+ } else {
252
+ wet.connect(convolver);
253
+ }
254
+ convolver.connect(wetOut);
255
+ });
256
+ }
257
+ const irCache = new Map();
258
+ function createConvolver(ctx, opts) {
259
+ var _opts_mix;
260
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.5;
261
+ return withMix(ctx, mix, (wet, wetOut)=>{
262
+ const convolver = ctx.createConvolver();
263
+ if (opts.buffer) {
264
+ convolver.buffer = opts.buffer;
265
+ } else if (opts.url) {
266
+ const cached = irCache.get(opts.url);
267
+ if (cached) {
268
+ convolver.buffer = cached;
269
+ } else {
270
+ const url = opts.url;
271
+ fetch(url).then((res)=>res.arrayBuffer()).then((data)=>ctx.decodeAudioData(data)).then((decoded)=>{
272
+ irCache.set(url, decoded);
273
+ convolver.buffer = decoded;
274
+ });
275
+ }
276
+ }
277
+ wet.connect(convolver);
278
+ convolver.connect(wetOut);
279
+ });
280
+ }
281
+ function createDelay(ctx, opts) {
282
+ var _opts_time, _opts_feedback, _opts_mix;
283
+ const time = (_opts_time = opts.time) != null ? _opts_time : 0.25;
284
+ const feedback = (_opts_feedback = opts.feedback) != null ? _opts_feedback : 0.3;
285
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.3;
286
+ return withMix(ctx, mix, (wet, wetOut)=>{
287
+ const delay = ctx.createDelay(Math.max(time + 0.01, 1));
288
+ delay.delayTime.value = time;
289
+ const fb = ctx.createGain();
290
+ fb.gain.value = feedback;
291
+ wet.connect(delay);
292
+ delay.connect(fb);
293
+ if (opts.feedbackFilter) {
294
+ var _opts_feedbackFilter_Q;
295
+ const filter = ctx.createBiquadFilter();
296
+ filter.type = opts.feedbackFilter.type;
297
+ filter.frequency.value = opts.feedbackFilter.frequency;
298
+ filter.Q.value = (_opts_feedbackFilter_Q = opts.feedbackFilter.Q) != null ? _opts_feedbackFilter_Q : 1;
299
+ fb.connect(filter);
300
+ filter.connect(delay);
301
+ } else {
302
+ fb.connect(delay);
303
+ }
304
+ delay.connect(wetOut);
305
+ });
306
+ }
307
+ function createDistortion(ctx, opts) {
308
+ var _opts_amount, _opts_mix;
309
+ const amount = (_opts_amount = opts.amount) != null ? _opts_amount : 50;
310
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.5;
311
+ return withMix(ctx, mix, (wet, wetOut)=>{
312
+ const shaper = ctx.createWaveShaper();
313
+ const samples = 44100;
314
+ const curve = new Float32Array(samples);
315
+ const k = amount;
316
+ for(let i = 0; i < samples; i++){
317
+ const x = i * 2 / samples - 1;
318
+ curve[i] = Math.tanh(k * x);
319
+ }
320
+ shaper.curve = curve;
321
+ shaper.oversample = "4x";
322
+ wet.connect(shaper);
323
+ shaper.connect(wetOut);
324
+ });
325
+ }
326
+ function createChorus(ctx, opts) {
327
+ var _opts_rate, _opts_depth, _opts_mix;
328
+ const rate = (_opts_rate = opts.rate) != null ? _opts_rate : 1.5;
329
+ const depth = (_opts_depth = opts.depth) != null ? _opts_depth : 0.003;
330
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.3;
331
+ return withMix(ctx, mix, (wet, wetOut)=>{
332
+ const delayL = ctx.createDelay();
333
+ delayL.delayTime.value = 0.012;
334
+ const delayR = ctx.createDelay();
335
+ delayR.delayTime.value = 0.016;
336
+ const lfoL = ctx.createOscillator();
337
+ lfoL.type = "sine";
338
+ lfoL.frequency.value = rate;
339
+ const lfoR = ctx.createOscillator();
340
+ lfoR.type = "sine";
341
+ lfoR.frequency.value = rate * 1.1;
342
+ const lfoGainL = ctx.createGain();
343
+ lfoGainL.gain.value = depth;
344
+ const lfoGainR = ctx.createGain();
345
+ lfoGainR.gain.value = depth;
346
+ lfoL.connect(lfoGainL);
347
+ lfoGainL.connect(delayL.delayTime);
348
+ lfoL.start();
349
+ lfoR.connect(lfoGainR);
350
+ lfoGainR.connect(delayR.delayTime);
351
+ lfoR.start();
352
+ wet.connect(delayL);
353
+ wet.connect(delayR);
354
+ delayL.connect(wetOut);
355
+ delayR.connect(wetOut);
356
+ return {
357
+ dispose () {
358
+ try {
359
+ lfoL.stop();
360
+ } catch (_) {}
361
+ try {
362
+ lfoR.stop();
363
+ } catch (_) {}
364
+ }
365
+ };
366
+ });
367
+ }
368
+ function createFlanger(ctx, opts) {
369
+ var _opts_rate, _opts_depth, _opts_feedback, _opts_mix;
370
+ const rate = (_opts_rate = opts.rate) != null ? _opts_rate : 0.5;
371
+ const depth = (_opts_depth = opts.depth) != null ? _opts_depth : 0.002;
372
+ const feedback = (_opts_feedback = opts.feedback) != null ? _opts_feedback : 0.5;
373
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.5;
374
+ return withMix(ctx, mix, (wet, wetOut)=>{
375
+ const delay = ctx.createDelay();
376
+ delay.delayTime.value = 0.005;
377
+ const lfo = ctx.createOscillator();
378
+ lfo.type = "sine";
379
+ lfo.frequency.value = rate;
380
+ const lfoGain = ctx.createGain();
381
+ lfoGain.gain.value = depth;
382
+ lfo.connect(lfoGain);
383
+ lfoGain.connect(delay.delayTime);
384
+ lfo.start();
385
+ const fb = ctx.createGain();
386
+ fb.gain.value = feedback;
387
+ delay.connect(fb);
388
+ fb.connect(delay);
389
+ wet.connect(delay);
390
+ delay.connect(wetOut);
391
+ return {
392
+ dispose () {
393
+ try {
394
+ lfo.stop();
395
+ } catch (_) {}
396
+ }
397
+ };
398
+ });
399
+ }
400
+ function createPhaser(ctx, opts) {
401
+ var _opts_rate, _opts_depth, _opts_stages, _opts_feedback, _opts_mix;
402
+ const rate = (_opts_rate = opts.rate) != null ? _opts_rate : 0.5;
403
+ const depth = (_opts_depth = opts.depth) != null ? _opts_depth : 1000;
404
+ const stages = (_opts_stages = opts.stages) != null ? _opts_stages : 4;
405
+ const feedback = (_opts_feedback = opts.feedback) != null ? _opts_feedback : 0.5;
406
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 0.5;
407
+ return withMix(ctx, mix, (wet, wetOut)=>{
408
+ const filters = [];
409
+ const baseFreqs = [
410
+ 200,
411
+ 600,
412
+ 1200,
413
+ 2400,
414
+ 4800,
415
+ 8000
416
+ ];
417
+ for(let i = 0; i < stages; i++){
418
+ const f = ctx.createBiquadFilter();
419
+ f.type = "allpass";
420
+ f.frequency.value = baseFreqs[i % baseFreqs.length];
421
+ f.Q.value = 0.5;
422
+ filters.push(f);
423
+ }
424
+ for(let i = 0; i < filters.length - 1; i++){
425
+ filters[i].connect(filters[i + 1]);
426
+ }
427
+ const lfo = ctx.createOscillator();
428
+ lfo.type = "sine";
429
+ lfo.frequency.value = rate;
430
+ const lfoGain = ctx.createGain();
431
+ lfoGain.gain.value = depth;
432
+ lfo.connect(lfoGain);
433
+ for (const f of filters){
434
+ lfoGain.connect(f.frequency);
435
+ }
436
+ lfo.start();
437
+ const fb = ctx.createGain();
438
+ fb.gain.value = feedback;
439
+ filters[filters.length - 1].connect(fb);
440
+ fb.connect(filters[0]);
441
+ wet.connect(filters[0]);
442
+ filters[filters.length - 1].connect(wetOut);
443
+ return {
444
+ dispose () {
445
+ try {
446
+ lfo.stop();
447
+ } catch (_) {}
448
+ }
449
+ };
450
+ });
451
+ }
452
+ function createTremolo(ctx, opts) {
453
+ var _opts_rate, _opts_depth;
454
+ const rate = (_opts_rate = opts.rate) != null ? _opts_rate : 4;
455
+ const depth = (_opts_depth = opts.depth) != null ? _opts_depth : 0.5;
456
+ const input = ctx.createGain();
457
+ const output = ctx.createGain();
458
+ const tremGain = ctx.createGain();
459
+ tremGain.gain.value = 1 - depth / 2;
460
+ input.connect(tremGain);
461
+ tremGain.connect(output);
462
+ const lfo = ctx.createOscillator();
463
+ lfo.type = "sine";
464
+ lfo.frequency.value = rate;
465
+ const lfoGain = ctx.createGain();
466
+ lfoGain.gain.value = depth / 2;
467
+ lfo.connect(lfoGain);
468
+ lfoGain.connect(tremGain.gain);
469
+ lfo.start();
470
+ return {
471
+ input,
472
+ output,
473
+ dispose () {
474
+ try {
475
+ lfo.stop();
476
+ } catch (_) {}
477
+ }
478
+ };
479
+ }
480
+ function createVibrato(ctx, opts) {
481
+ var _opts_rate, _opts_depth;
482
+ const rate = (_opts_rate = opts.rate) != null ? _opts_rate : 5;
483
+ const depth = (_opts_depth = opts.depth) != null ? _opts_depth : 0.002;
484
+ const input = ctx.createGain();
485
+ const output = ctx.createGain();
486
+ const delay = ctx.createDelay();
487
+ delay.delayTime.value = depth;
488
+ const lfo = ctx.createOscillator();
489
+ lfo.type = "sine";
490
+ lfo.frequency.value = rate;
491
+ const lfoGain = ctx.createGain();
492
+ lfoGain.gain.value = depth;
493
+ lfo.connect(lfoGain);
494
+ lfoGain.connect(delay.delayTime);
495
+ lfo.start();
496
+ input.connect(delay);
497
+ delay.connect(output);
498
+ return {
499
+ input,
500
+ output,
501
+ dispose () {
502
+ try {
503
+ lfo.stop();
504
+ } catch (_) {}
505
+ }
506
+ };
507
+ }
508
+ function createBitcrusher(ctx, opts) {
509
+ var _opts_bits, _opts_mix, _opts_sampleRateReduction;
510
+ const bits = (_opts_bits = opts.bits) != null ? _opts_bits : 8;
511
+ const mix = (_opts_mix = opts.mix) != null ? _opts_mix : 1;
512
+ const srReduction = (_opts_sampleRateReduction = opts.sampleRateReduction) != null ? _opts_sampleRateReduction : 1;
513
+ return withMix(ctx, mix, (wet, wetOut)=>{
514
+ const shaper = ctx.createWaveShaper();
515
+ const steps = 2 ** bits;
516
+ const samples = 65536;
517
+ const curve = new Float32Array(samples);
518
+ for(let i = 0; i < samples; i++){
519
+ const x = i * 2 / samples - 1;
520
+ if (srReduction > 1) {
521
+ const blockIndex = Math.floor(i / srReduction) * srReduction;
522
+ const blockX = blockIndex * 2 / samples - 1;
523
+ curve[i] = Math.round(blockX * steps) / steps;
524
+ } else {
525
+ curve[i] = Math.round(x * steps) / steps;
526
+ }
527
+ }
528
+ shaper.curve = curve;
529
+ wet.connect(shaper);
530
+ shaper.connect(wetOut);
531
+ });
532
+ }
533
+ function createCompressor(ctx, opts) {
534
+ var _opts_threshold, _opts_knee, _opts_ratio, _opts_attack, _opts_release;
535
+ const comp = ctx.createDynamicsCompressor();
536
+ comp.threshold.value = (_opts_threshold = opts.threshold) != null ? _opts_threshold : -24;
537
+ comp.knee.value = (_opts_knee = opts.knee) != null ? _opts_knee : 30;
538
+ comp.ratio.value = (_opts_ratio = opts.ratio) != null ? _opts_ratio : 4;
539
+ comp.attack.value = (_opts_attack = opts.attack) != null ? _opts_attack : 0.003;
540
+ comp.release.value = (_opts_release = opts.release) != null ? _opts_release : 0.25;
541
+ return {
542
+ input: comp,
543
+ output: comp
544
+ };
545
+ }
546
+ function createEQ(ctx, opts) {
547
+ const input = ctx.createGain();
548
+ const output = ctx.createGain();
549
+ if (opts.bands.length === 0) {
550
+ input.connect(output);
551
+ return {
552
+ input,
553
+ output
554
+ };
555
+ }
556
+ const filters = opts.bands.map((band)=>{
557
+ var _band_Q;
558
+ const f = ctx.createBiquadFilter();
559
+ f.type = band.type;
560
+ f.frequency.value = band.frequency;
561
+ f.gain.value = band.gain;
562
+ f.Q.value = (_band_Q = band.Q) != null ? _band_Q : 1;
563
+ return f;
564
+ });
565
+ input.connect(filters[0]);
566
+ for(let i = 0; i < filters.length - 1; i++){
567
+ filters[i].connect(filters[i + 1]);
568
+ }
569
+ filters[filters.length - 1].connect(output);
570
+ return {
571
+ input,
572
+ output
573
+ };
574
+ }
575
+ function createGainEffect(ctx, opts) {
576
+ const gain = ctx.createGain();
577
+ gain.gain.value = opts.value;
578
+ return {
579
+ input: gain,
580
+ output: gain
581
+ };
582
+ }
583
+ function createPanEffect(ctx, opts) {
584
+ const panner = ctx.createStereoPanner();
585
+ panner.pan.value = opts.value;
586
+ return {
587
+ input: panner,
588
+ output: panner
589
+ };
590
+ }
591
+ /**
592
+ * Instantiates an {@link EffectNode} from an {@link Effect} descriptor.
593
+ *
594
+ * This is the main factory used by the engine to build effect chains.
595
+ * It dispatches to the appropriate `create*` function based on `effect.type`.
596
+ *
597
+ * @param ctx - The audio context to create nodes in
598
+ * @param effect - The effect descriptor
599
+ * @returns A connectable effect node with `input`, `output`, and optional `dispose`
600
+ */ function createEffect(ctx, effect) {
601
+ switch(effect.type){
602
+ case "reverb":
603
+ return createReverb(ctx, effect);
604
+ case "convolver":
605
+ return createConvolver(ctx, effect);
606
+ case "delay":
607
+ return createDelay(ctx, effect);
608
+ case "distortion":
609
+ return createDistortion(ctx, effect);
610
+ case "chorus":
611
+ return createChorus(ctx, effect);
612
+ case "flanger":
613
+ return createFlanger(ctx, effect);
614
+ case "phaser":
615
+ return createPhaser(ctx, effect);
616
+ case "tremolo":
617
+ return createTremolo(ctx, effect);
618
+ case "vibrato":
619
+ return createVibrato(ctx, effect);
620
+ case "bitcrusher":
621
+ return createBitcrusher(ctx, effect);
622
+ case "compressor":
623
+ return createCompressor(ctx, effect);
624
+ case "eq":
625
+ return createEQ(ctx, effect);
626
+ case "gain":
627
+ return createGainEffect(ctx, effect);
628
+ case "pan":
629
+ return createPanEffect(ctx, effect);
630
+ }
631
+ }
632
+
633
+ const SILENCE = 0.0001;
634
+ function isMultiLayer(def) {
635
+ return "layers" in def;
636
+ }
637
+ function normalize(def) {
638
+ if (isMultiLayer(def)) return def;
639
+ return {
640
+ layers: [
641
+ def
642
+ ],
643
+ effects: []
644
+ };
645
+ }
646
+ function generateWhiteNoise(data) {
647
+ for(let i = 0; i < data.length; i++){
648
+ data[i] = Math.random() * 2 - 1;
649
+ }
650
+ }
651
+ function generatePinkNoise(data) {
652
+ let b0 = 0;
653
+ let b1 = 0;
654
+ let b2 = 0;
655
+ let b3 = 0;
656
+ let b4 = 0;
657
+ let b5 = 0;
658
+ let b6 = 0;
659
+ for(let i = 0; i < data.length; i++){
660
+ const white = Math.random() * 2 - 1;
661
+ b0 = 0.99886 * b0 + white * 0.0555179;
662
+ b1 = 0.99332 * b1 + white * 0.0750759;
663
+ b2 = 0.969 * b2 + white * 0.153852;
664
+ b3 = 0.8665 * b3 + white * 0.3104856;
665
+ b4 = 0.55 * b4 + white * 0.5329522;
666
+ b5 = -0.7616 * b5 - white * 0.016898;
667
+ data[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362) * 0.11;
668
+ b6 = white * 0.115926;
669
+ }
670
+ }
671
+ function generateBrownNoise(data) {
672
+ let last = 0;
673
+ for(let i = 0; i < data.length; i++){
674
+ const white = Math.random() * 2 - 1;
675
+ last = (last + 0.02 * white) / 1.02;
676
+ data[i] = last * 3.5;
677
+ }
678
+ }
679
+ function createNoiseBuffer(ctx, color, duration) {
680
+ const length = ctx.sampleRate * duration;
681
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
682
+ const data = buffer.getChannelData(0);
683
+ switch(color){
684
+ case "pink":
685
+ generatePinkNoise(data);
686
+ break;
687
+ case "brown":
688
+ generateBrownNoise(data);
689
+ break;
690
+ default:
691
+ generateWhiteNoise(data);
692
+ break;
693
+ }
694
+ return buffer;
695
+ }
696
+ const sampleCache = new Map();
697
+ async function loadSample(ctx, url) {
698
+ const cached = sampleCache.get(url);
699
+ if (cached) return cached;
700
+ const response = await fetch(url);
701
+ const data = await response.arrayBuffer();
702
+ const decoded = await ctx.decodeAudioData(data);
703
+ sampleCache.set(url, decoded);
704
+ return decoded;
705
+ }
706
+ function buildOscillatorSource(ctx, src, t, duration) {
707
+ const osc = ctx.createOscillator();
708
+ osc.type = src.type;
709
+ if (typeof src.frequency === "number") {
710
+ osc.frequency.setValueAtTime(src.frequency, t);
711
+ } else {
712
+ osc.frequency.setValueAtTime(src.frequency.start, t);
713
+ osc.frequency.exponentialRampToValueAtTime(Math.max(src.frequency.end, 1), t + duration);
714
+ }
715
+ if (src.detune) {
716
+ osc.detune.value = src.detune;
717
+ }
718
+ osc.start(t);
719
+ osc.stop(t + duration + 0.1);
720
+ let fmMod;
721
+ if (src.fm) {
722
+ const carrierFreq = typeof src.frequency === "number" ? src.frequency : src.frequency.start;
723
+ fmMod = ctx.createOscillator();
724
+ fmMod.type = "sine";
725
+ fmMod.frequency.value = carrierFreq * src.fm.ratio;
726
+ const modGain = ctx.createGain();
727
+ modGain.gain.value = src.fm.depth;
728
+ fmMod.connect(modGain);
729
+ modGain.connect(osc.frequency);
730
+ fmMod.start(t);
731
+ fmMod.stop(t + duration + 0.1);
732
+ }
733
+ return {
734
+ node: osc,
735
+ scheduled: osc,
736
+ frequencyParam: osc.frequency,
737
+ detuneParam: osc.detune
738
+ };
739
+ }
740
+ function buildNoiseSource(ctx, src, t, duration) {
741
+ var _src_color;
742
+ const color = (_src_color = src.color) != null ? _src_color : "white";
743
+ const buffer = createNoiseBuffer(ctx, color, duration + 0.1);
744
+ const node = ctx.createBufferSource();
745
+ node.buffer = buffer;
746
+ node.start(t);
747
+ node.stop(t + duration + 0.1);
748
+ return {
749
+ node,
750
+ scheduled: node
751
+ };
752
+ }
753
+ function buildWavetableSource(ctx, src, t, duration) {
754
+ const real = new Float32Array(src.harmonics.length + 1);
755
+ const imag = new Float32Array(src.harmonics.length + 1);
756
+ real[0] = 0;
757
+ imag[0] = 0;
758
+ for(let i = 0; i < src.harmonics.length; i++){
759
+ real[i + 1] = 0;
760
+ imag[i + 1] = src.harmonics[i];
761
+ }
762
+ const wave = ctx.createPeriodicWave(real, imag, {
763
+ disableNormalization: false
764
+ });
765
+ const osc = ctx.createOscillator();
766
+ osc.setPeriodicWave(wave);
767
+ if (typeof src.frequency === "number") {
768
+ osc.frequency.setValueAtTime(src.frequency, t);
769
+ } else {
770
+ osc.frequency.setValueAtTime(src.frequency.start, t);
771
+ osc.frequency.exponentialRampToValueAtTime(Math.max(src.frequency.end, 1), t + duration);
772
+ }
773
+ osc.start(t);
774
+ osc.stop(t + duration + 0.1);
775
+ return {
776
+ node: osc,
777
+ scheduled: osc,
778
+ frequencyParam: osc.frequency,
779
+ detuneParam: osc.detune
780
+ };
781
+ }
782
+ function buildSampleSource(ctx, src, t) {
783
+ const node = ctx.createBufferSource();
784
+ if (src.playbackRate !== undefined) {
785
+ node.playbackRate.value = src.playbackRate;
786
+ }
787
+ if (src.detune !== undefined) {
788
+ node.detune.value = src.detune;
789
+ }
790
+ if (src.loop) {
791
+ node.loop = true;
792
+ if (src.loopStart !== undefined) node.loopStart = src.loopStart;
793
+ if (src.loopEnd !== undefined) node.loopEnd = src.loopEnd;
794
+ }
795
+ if (src.buffer) {
796
+ node.buffer = src.buffer;
797
+ node.start(t);
798
+ } else if (src.url) {
799
+ loadSample(ctx, src.url).then((buf)=>{
800
+ node.buffer = buf;
801
+ node.start(Math.max(t, ctx.currentTime));
802
+ });
803
+ }
804
+ return {
805
+ node,
806
+ scheduled: node,
807
+ detuneParam: node.detune,
808
+ playbackRateParam: node.playbackRate
809
+ };
810
+ }
811
+ function buildStreamSource(ctx, src) {
812
+ const node = ctx.createMediaStreamSource(src.stream);
813
+ return {
814
+ node
815
+ };
816
+ }
817
+ function buildConstantSource(ctx, src, t, duration) {
818
+ var _src_offset;
819
+ const node = ctx.createConstantSource();
820
+ node.offset.value = (_src_offset = src.offset) != null ? _src_offset : 1;
821
+ node.start(t);
822
+ node.stop(t + duration + 0.1);
823
+ return {
824
+ node,
825
+ scheduled: node
826
+ };
827
+ }
828
+ function buildSource(ctx, src, t, duration) {
829
+ switch(src.type){
830
+ case "sine":
831
+ case "triangle":
832
+ case "square":
833
+ case "sawtooth":
834
+ return buildOscillatorSource(ctx, src, t, duration);
835
+ case "noise":
836
+ return buildNoiseSource(ctx, src, t, duration);
837
+ case "wavetable":
838
+ return buildWavetableSource(ctx, src, t, duration);
839
+ case "sample":
840
+ return buildSampleSource(ctx, src, t);
841
+ case "stream":
842
+ return buildStreamSource(ctx, src);
843
+ case "constant":
844
+ return buildConstantSource(ctx, src, t, duration);
845
+ }
846
+ }
847
+ function buildBiquadFilter(ctx, filter, t) {
848
+ var _filter_resonance;
849
+ const node = ctx.createBiquadFilter();
850
+ node.type = filter.type;
851
+ node.frequency.setValueAtTime(filter.frequency, t);
852
+ node.Q.value = (_filter_resonance = filter.resonance) != null ? _filter_resonance : 1;
853
+ if (filter.gain !== undefined) {
854
+ node.gain.value = filter.gain;
855
+ }
856
+ if (filter.envelope) {
857
+ var _env_attack;
858
+ const env = filter.envelope;
859
+ const attackEnd = t + ((_env_attack = env.attack) != null ? _env_attack : 0);
860
+ node.frequency.setValueAtTime(filter.frequency, t);
861
+ node.frequency.linearRampToValueAtTime(env.peak, attackEnd);
862
+ node.frequency.exponentialRampToValueAtTime(Math.max(filter.frequency, 1), attackEnd + env.decay);
863
+ }
864
+ return {
865
+ node,
866
+ frequencyParam: node.frequency
867
+ };
868
+ }
869
+ function buildIIRFilter(ctx, filter) {
870
+ const node = ctx.createIIRFilter(filter.feedforward, filter.feedback);
871
+ return {
872
+ node
873
+ };
874
+ }
875
+ function buildSingleFilter(ctx, filter, t) {
876
+ if (filter.type === "iir") {
877
+ const { node } = buildIIRFilter(ctx, filter);
878
+ return {
879
+ node
880
+ };
881
+ }
882
+ const { node, frequencyParam } = buildBiquadFilter(ctx, filter, t);
883
+ return {
884
+ node,
885
+ frequencyParam,
886
+ detuneParam: node.detune,
887
+ QParam: node.Q,
888
+ gainParam: node.gain
889
+ };
890
+ }
891
+ function buildFilters(ctx, filters, t) {
892
+ const arr = Array.isArray(filters) ? filters : [
893
+ filters
894
+ ];
895
+ return arr.map((f)=>buildSingleFilter(ctx, f, t));
896
+ }
897
+ function buildEnvelope(ctx, envelope, gain, t) {
898
+ var _envelope_attack, _envelope_sustain, _envelope_release;
899
+ const node = ctx.createGain();
900
+ if (!envelope) {
901
+ node.gain.setValueAtTime(gain, t);
902
+ node.gain.setTargetAtTime(SILENCE, t, 0.15);
903
+ return {
904
+ node,
905
+ duration: 0.5
906
+ };
907
+ }
908
+ const attack = (_envelope_attack = envelope.attack) != null ? _envelope_attack : 0;
909
+ const decay = envelope.decay;
910
+ const sustain = (_envelope_sustain = envelope.sustain) != null ? _envelope_sustain : 0;
911
+ const release = (_envelope_release = envelope.release) != null ? _envelope_release : 0;
912
+ const sustainLevel = Math.max(sustain * gain, SILENCE);
913
+ const decayTC = decay / 3;
914
+ node.gain.setValueAtTime(SILENCE, t);
915
+ if (attack > 0) {
916
+ node.gain.linearRampToValueAtTime(gain, t + attack);
917
+ } else {
918
+ node.gain.setValueAtTime(gain, t);
919
+ }
920
+ if (sustain > 0) {
921
+ node.gain.setTargetAtTime(sustainLevel, t + attack, decayTC);
922
+ if (release > 0) {
923
+ const releaseTC = release / 3;
924
+ node.gain.setTargetAtTime(SILENCE, t + attack + decay, releaseTC);
925
+ }
926
+ } else {
927
+ node.gain.setTargetAtTime(SILENCE, t + attack, decayTC);
928
+ }
929
+ return {
930
+ node,
931
+ duration: attack + decay + release
932
+ };
933
+ }
934
+ function buildLFO(ctx, lfo, t, duration, targets) {
935
+ const osc = ctx.createOscillator();
936
+ osc.type = lfo.type;
937
+ osc.frequency.value = lfo.frequency;
938
+ const gain = ctx.createGain();
939
+ gain.gain.value = lfo.depth;
940
+ osc.connect(gain);
941
+ let target = null;
942
+ switch(lfo.target){
943
+ case "frequency":
944
+ var _targets_source_frequencyParam;
945
+ target = (_targets_source_frequencyParam = targets.source.frequencyParam) != null ? _targets_source_frequencyParam : null;
946
+ break;
947
+ case "detune":
948
+ var _targets_source_detuneParam;
949
+ target = (_targets_source_detuneParam = targets.source.detuneParam) != null ? _targets_source_detuneParam : null;
950
+ break;
951
+ case "gain":
952
+ target = targets.envNode.gain;
953
+ break;
954
+ case "pan":
955
+ var _ref;
956
+ var _targets_panner;
957
+ target = (_ref = (_targets_panner = targets.panner) == null ? void 0 : _targets_panner.pan) != null ? _ref : null;
958
+ break;
959
+ case "playbackRate":
960
+ var _targets_source_playbackRateParam;
961
+ target = (_targets_source_playbackRateParam = targets.source.playbackRateParam) != null ? _targets_source_playbackRateParam : null;
962
+ break;
963
+ case "filter.frequency":
964
+ var _ref1;
965
+ var _targets_filters_;
966
+ target = (_ref1 = (_targets_filters_ = targets.filters[0]) == null ? void 0 : _targets_filters_.frequencyParam) != null ? _ref1 : null;
967
+ break;
968
+ case "filter.detune":
969
+ var _ref2;
970
+ var _targets_filters_1;
971
+ target = (_ref2 = (_targets_filters_1 = targets.filters[0]) == null ? void 0 : _targets_filters_1.detuneParam) != null ? _ref2 : null;
972
+ break;
973
+ case "filter.Q":
974
+ var _ref3;
975
+ var _targets_filters_2;
976
+ target = (_ref3 = (_targets_filters_2 = targets.filters[0]) == null ? void 0 : _targets_filters_2.QParam) != null ? _ref3 : null;
977
+ break;
978
+ case "filter.gain":
979
+ var _ref4;
980
+ var _targets_filters_3;
981
+ target = (_ref4 = (_targets_filters_3 = targets.filters[0]) == null ? void 0 : _targets_filters_3.gainParam) != null ? _ref4 : null;
982
+ break;
983
+ }
984
+ if (target) {
985
+ gain.connect(target);
986
+ osc.start(t);
987
+ osc.stop(t + duration + 0.1);
988
+ return osc;
989
+ }
990
+ return null;
991
+ }
992
+ function buildPanner3D(ctx, config) {
993
+ var _config_panningModel, _config_distanceModel;
994
+ const panner = ctx.createPanner();
995
+ panner.panningModel = (_config_panningModel = config.panningModel) != null ? _config_panningModel : "HRTF";
996
+ panner.distanceModel = (_config_distanceModel = config.distanceModel) != null ? _config_distanceModel : "inverse";
997
+ panner.positionX.value = config.positionX;
998
+ panner.positionY.value = config.positionY;
999
+ panner.positionZ.value = config.positionZ;
1000
+ if (config.orientationX !== undefined) panner.orientationX.value = config.orientationX;
1001
+ if (config.orientationY !== undefined) panner.orientationY.value = config.orientationY;
1002
+ if (config.orientationZ !== undefined) panner.orientationZ.value = config.orientationZ;
1003
+ if (config.maxDistance !== undefined) panner.maxDistance = config.maxDistance;
1004
+ if (config.refDistance !== undefined) panner.refDistance = config.refDistance;
1005
+ if (config.rolloffFactor !== undefined) panner.rolloffFactor = config.rolloffFactor;
1006
+ if (config.coneInnerAngle !== undefined) panner.coneInnerAngle = config.coneInnerAngle;
1007
+ if (config.coneOuterAngle !== undefined) panner.coneOuterAngle = config.coneOuterAngle;
1008
+ if (config.coneOuterGain !== undefined) panner.coneOuterGain = config.coneOuterGain;
1009
+ return panner;
1010
+ }
1011
+ function buildEffectsChain(ctx, effects, destination) {
1012
+ if (effects.length === 0) {
1013
+ return {
1014
+ input: destination,
1015
+ output: destination,
1016
+ dispose () {}
1017
+ };
1018
+ }
1019
+ const nodes = effects.map((e)=>createEffect(ctx, e));
1020
+ for(let i = 0; i < nodes.length - 1; i++){
1021
+ nodes[i].output.connect(nodes[i + 1].input);
1022
+ }
1023
+ nodes[nodes.length - 1].output.connect(destination);
1024
+ return {
1025
+ input: nodes[0].input,
1026
+ output: nodes[nodes.length - 1].output,
1027
+ dispose () {
1028
+ for (const n of nodes)n.dispose == null ? void 0 : n.dispose.call(n);
1029
+ }
1030
+ };
1031
+ }
1032
+ /**
1033
+ * Renders a {@link SoundDefinition} into the Web Audio graph and starts playback.
1034
+ *
1035
+ * Builds sources, filters, envelopes, LFOs, panners, and effects for every
1036
+ * layer, connects them to `destination`, and returns a {@link VoiceHandle}
1037
+ * that can stop the sound mid-flight.
1038
+ *
1039
+ * @param ctx - The `BaseAudioContext` to build nodes in
1040
+ * @param definition - A single-layer or multi-layer sound definition
1041
+ * @param opts - Runtime overrides (volume, pan, detune, velocity, etc.)
1042
+ * @param baseTime - Scheduled start time in seconds (`ctx.currentTime` if omitted)
1043
+ * @param destination - Target node to connect to (`ctx.destination` if omitted)
1044
+ * @returns A handle with a `stop()` method for cancelling the voice
1045
+ */ function render(ctx, definition, opts, baseTime, destination) {
1046
+ var _ref;
1047
+ const { layers, effects } = normalize(definition);
1048
+ const dest = destination != null ? destination : ctx.destination;
1049
+ const chain = buildEffectsChain(ctx, effects != null ? effects : [], dest);
1050
+ const t0 = baseTime != null ? baseTime : ctx.currentTime;
1051
+ const velocity = (_ref = opts == null ? void 0 : opts.velocity) != null ? _ref : 1;
1052
+ const jitter = opts == null ? void 0 : opts.jitter;
1053
+ const detuneJitter = (jitter == null ? void 0 : jitter.detune) ? (Math.random() * 2 - 1) * jitter.detune : 0;
1054
+ const volumeJitter = (jitter == null ? void 0 : jitter.volume) ? 1 + (Math.random() * 2 - 1) * jitter.volume : 1;
1055
+ const rateJitter = (jitter == null ? void 0 : jitter.playbackRate) ? 1 + (Math.random() * 2 - 1) * jitter.playbackRate : 1;
1056
+ const allDisposers = [
1057
+ chain.dispose
1058
+ ];
1059
+ const allSourceNodes = [];
1060
+ const allEnvNodes = [];
1061
+ for (const layer of layers){
1062
+ var _layer_delay, _layer_gain, _ref1, _ref2;
1063
+ const layerStart = t0 + ((_layer_delay = layer.delay) != null ? _layer_delay : 0);
1064
+ const baseGain = ((_layer_gain = layer.gain) != null ? _layer_gain : 0.5) * ((_ref1 = opts == null ? void 0 : opts.volume) != null ? _ref1 : 1) * velocity * volumeJitter;
1065
+ const { node: envNode, duration: envDuration } = buildEnvelope(ctx, layer.envelope, baseGain, layerStart);
1066
+ allEnvNodes.push(envNode);
1067
+ const sourceResult = buildSource(ctx, layer.source, layerStart, envDuration);
1068
+ if (sourceResult.detuneParam && ((opts == null ? void 0 : opts.detune) || detuneJitter !== 0)) {
1069
+ var _ref3;
1070
+ sourceResult.detuneParam.value += ((_ref3 = opts == null ? void 0 : opts.detune) != null ? _ref3 : 0) + detuneJitter;
1071
+ }
1072
+ if (sourceResult.playbackRateParam && ((opts == null ? void 0 : opts.playbackRate) || rateJitter !== 1)) {
1073
+ var _ref4;
1074
+ sourceResult.playbackRateParam.value *= ((_ref4 = opts == null ? void 0 : opts.playbackRate) != null ? _ref4 : 1) * rateJitter;
1075
+ }
1076
+ let tail = sourceResult.node;
1077
+ const filterResults = [];
1078
+ if (layer.filter) {
1079
+ const builtFilters = buildFilters(ctx, layer.filter, layerStart);
1080
+ for (const f of builtFilters){
1081
+ tail.connect(f.node);
1082
+ tail = f.node;
1083
+ filterResults.push(f);
1084
+ if (velocity < 1 && f.frequencyParam) {
1085
+ const baseFreq = f.frequencyParam.value;
1086
+ f.frequencyParam.setValueAtTime(baseFreq * (0.5 + 0.5 * velocity), layerStart);
1087
+ }
1088
+ }
1089
+ }
1090
+ tail.connect(envNode);
1091
+ let cursor = envNode;
1092
+ const layerDisposers = [];
1093
+ if (layer.effects && layer.effects.length > 0) {
1094
+ const layerFxNodes = layer.effects.map((e)=>createEffect(ctx, e));
1095
+ for(let i = 0; i < layerFxNodes.length - 1; i++){
1096
+ layerFxNodes[i].output.connect(layerFxNodes[i + 1].input);
1097
+ }
1098
+ cursor.connect(layerFxNodes[0].input);
1099
+ cursor = layerFxNodes[layerFxNodes.length - 1].output;
1100
+ for (const n of layerFxNodes){
1101
+ if (n.dispose) layerDisposers.push(n.dispose);
1102
+ }
1103
+ }
1104
+ let stereoPanner;
1105
+ const effectivePan = (_ref2 = opts == null ? void 0 : opts.pan) != null ? _ref2 : layer.pan;
1106
+ if (layer.panner) {
1107
+ const panner3d = buildPanner3D(ctx, layer.panner);
1108
+ cursor.connect(panner3d);
1109
+ cursor = panner3d;
1110
+ } else if (effectivePan !== undefined && effectivePan !== 0) {
1111
+ stereoPanner = ctx.createStereoPanner();
1112
+ stereoPanner.pan.value = effectivePan;
1113
+ cursor.connect(stereoPanner);
1114
+ cursor = stereoPanner;
1115
+ }
1116
+ cursor.connect(chain.input);
1117
+ if (layer.lfo) {
1118
+ const lfos = Array.isArray(layer.lfo) ? layer.lfo : [
1119
+ layer.lfo
1120
+ ];
1121
+ for (const l of lfos){
1122
+ buildLFO(ctx, l, layerStart, envDuration, {
1123
+ source: sourceResult,
1124
+ filters: filterResults,
1125
+ envNode,
1126
+ panner: stereoPanner
1127
+ });
1128
+ }
1129
+ }
1130
+ if (sourceResult.scheduled) {
1131
+ allSourceNodes.push(sourceResult.scheduled);
1132
+ const nodesToDisconnect = [
1133
+ sourceResult.node,
1134
+ envNode,
1135
+ ...filterResults.map((f)=>f.node),
1136
+ ...stereoPanner ? [
1137
+ stereoPanner
1138
+ ] : []
1139
+ ];
1140
+ sourceResult.scheduled.onended = ()=>{
1141
+ for (const n of nodesToDisconnect){
1142
+ try {
1143
+ n.disconnect();
1144
+ } catch (_) {}
1145
+ }
1146
+ for (const d of layerDisposers)d();
1147
+ };
1148
+ }
1149
+ allDisposers.push(...layerDisposers);
1150
+ }
1151
+ return {
1152
+ stop (releaseTime) {
1153
+ const now = ctx.currentTime;
1154
+ const fade = releaseTime != null ? releaseTime : 0.015;
1155
+ for (const env of allEnvNodes){
1156
+ env.gain.cancelScheduledValues(now);
1157
+ env.gain.setValueAtTime(env.gain.value, now);
1158
+ env.gain.setTargetAtTime(SILENCE, now, fade / 3);
1159
+ }
1160
+ for (const src of allSourceNodes){
1161
+ try {
1162
+ src.stop(now + fade + 0.05);
1163
+ } catch (_) {}
1164
+ }
1165
+ }
1166
+ };
1167
+ }
1168
+
1169
+ /**
1170
+ * Renders a sound definition to an `AudioBuffer` using `OfflineAudioContext`.
1171
+ *
1172
+ * No speakers are involved — the entire render happens in memory.
1173
+ *
1174
+ * @param definition - The sound to render
1175
+ * @param options - Duration, sample rate, and channel count
1176
+ * @param playOpts - Runtime overrides (volume, detune, etc.)
1177
+ * @returns A promise resolving to the rendered `AudioBuffer`
1178
+ */ async function renderToBuffer(definition, options, playOpts) {
1179
+ var _options_sampleRate, _options_numberOfChannels;
1180
+ const sampleRate = (_options_sampleRate = options.sampleRate) != null ? _options_sampleRate : 44100;
1181
+ const channels = (_options_numberOfChannels = options.numberOfChannels) != null ? _options_numberOfChannels : 2;
1182
+ const length = Math.ceil(options.duration * sampleRate);
1183
+ const offline = new OfflineAudioContext(channels, length, sampleRate);
1184
+ render(offline, definition, playOpts, 0, offline.destination);
1185
+ return offline.startRendering();
1186
+ }
1187
+ /**
1188
+ * Encodes an `AudioBuffer` as a 16-bit PCM WAV `Blob`.
1189
+ *
1190
+ * @param buffer - The audio buffer to encode
1191
+ * @returns A `Blob` with MIME type `audio/wav`
1192
+ */ function bufferToWav(buffer) {
1193
+ const numChannels = buffer.numberOfChannels;
1194
+ const sampleRate = buffer.sampleRate;
1195
+ const length = buffer.length;
1196
+ const bytesPerSample = 2;
1197
+ const blockAlign = numChannels * bytesPerSample;
1198
+ const dataSize = length * blockAlign;
1199
+ const headerSize = 44;
1200
+ const arrayBuffer = new ArrayBuffer(headerSize + dataSize);
1201
+ const view = new DataView(arrayBuffer);
1202
+ writeString(view, 0, "RIFF");
1203
+ view.setUint32(4, 36 + dataSize, true);
1204
+ writeString(view, 8, "WAVE");
1205
+ writeString(view, 12, "fmt ");
1206
+ view.setUint32(16, 16, true);
1207
+ view.setUint16(20, 1, true);
1208
+ view.setUint16(22, numChannels, true);
1209
+ view.setUint32(24, sampleRate, true);
1210
+ view.setUint32(28, sampleRate * blockAlign, true);
1211
+ view.setUint16(32, blockAlign, true);
1212
+ view.setUint16(34, bytesPerSample * 8, true);
1213
+ writeString(view, 36, "data");
1214
+ view.setUint32(40, dataSize, true);
1215
+ const channels = [];
1216
+ for(let ch = 0; ch < numChannels; ch++){
1217
+ channels.push(buffer.getChannelData(ch));
1218
+ }
1219
+ let offset = headerSize;
1220
+ for(let i = 0; i < length; i++){
1221
+ for(let ch = 0; ch < numChannels; ch++){
1222
+ const sample = Math.max(-1, Math.min(1, channels[ch][i]));
1223
+ const int16 = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
1224
+ view.setInt16(offset, int16, true);
1225
+ offset += bytesPerSample;
1226
+ }
1227
+ }
1228
+ return new Blob([
1229
+ arrayBuffer
1230
+ ], {
1231
+ type: "audio/wav"
1232
+ });
1233
+ }
1234
+ function writeString(view, offset, str) {
1235
+ for(let i = 0; i < str.length; i++){
1236
+ view.setUint8(offset + i, str.charCodeAt(i));
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Convenience wrapper that renders a sound and encodes it as a WAV `Blob`.
1241
+ *
1242
+ * Equivalent to calling {@link renderToBuffer} followed by {@link bufferToWav}.
1243
+ *
1244
+ * @param definition - The sound to render
1245
+ * @param options - Duration, sample rate, and channel count
1246
+ * @param playOpts - Runtime overrides
1247
+ * @returns A promise resolving to a WAV `Blob`
1248
+ */ async function renderToWav(definition, options, playOpts) {
1249
+ const buffer = await renderToBuffer(definition, options, playOpts);
1250
+ return bufferToWav(buffer);
1251
+ }
1252
+
1253
+ function createPatchInstance(data) {
1254
+ const soundNames = Object.keys(data.sounds);
1255
+ return {
1256
+ ready: true,
1257
+ name: data.name,
1258
+ author: data.author,
1259
+ version: data.version,
1260
+ description: data.description,
1261
+ tags: data.tags,
1262
+ sounds: soundNames,
1263
+ play (name, opts) {
1264
+ const def = data.sounds[name];
1265
+ if (!def) throw new Error(`Sound "${name}" not found in patch "${data.name}"`);
1266
+ const ctx = getContext();
1267
+ return render(ctx, def, opts, undefined, getDestination());
1268
+ },
1269
+ get (name) {
1270
+ return data.sounds[name];
1271
+ },
1272
+ toJSON () {
1273
+ return structuredClone(data);
1274
+ }
1275
+ };
1276
+ }
1277
+ /**
1278
+ * Creates an {@link AudioPatch} from an in-memory {@link SoundPatch} object.
1279
+ *
1280
+ * @param data - The sound patch data
1281
+ * @returns A ready-to-play `AudioPatch`
1282
+ */ function definePatch(data) {
1283
+ return createPatchInstance(data);
1284
+ }
1285
+ /**
1286
+ * Loads a sound patch from a URL or an in-memory object.
1287
+ *
1288
+ * When `source` is a string, it is fetched as JSON and decoded into a
1289
+ * {@link SoundPatch}. When it is already a `SoundPatch`, it is used directly.
1290
+ *
1291
+ * @param source - URL string or `SoundPatch` object
1292
+ * @returns A promise that resolves to a ready-to-play {@link AudioPatch}
1293
+ * @throws {Error} If the network request fails
1294
+ */ async function loadPatch(source) {
1295
+ if (typeof source === "string") {
1296
+ const response = await fetch(source);
1297
+ if (!response.ok) throw new Error(`Failed to load patch from ${source}: ${response.status}`);
1298
+ const data = await response.json();
1299
+ return createPatchInstance(data);
1300
+ }
1301
+ return createPatchInstance(source);
1302
+ }
1303
+
1304
+ function isDefinition(sound) {
1305
+ return typeof sound !== "function";
1306
+ }
1307
+ function resolveStepTimes(steps) {
1308
+ const times = [];
1309
+ let cursor = 0;
1310
+ for(let i = 0; i < steps.length; i++){
1311
+ const step = steps[i];
1312
+ if (step.at !== undefined) {
1313
+ cursor = step.at;
1314
+ } else if (step.wait !== undefined) {
1315
+ cursor += step.wait;
1316
+ } else if (i === 0) {
1317
+ cursor = 0;
1318
+ }
1319
+ times.push(cursor);
1320
+ }
1321
+ return times;
1322
+ }
1323
+ const LOOKAHEAD_MS = 25;
1324
+ const SCHEDULE_AHEAD = 0.1;
1325
+ function scheduleOnce(ctx, steps, times, opts, baseTime, scheduled) {
1326
+ const handles = [];
1327
+ for(let i = 0; i < steps.length; i++){
1328
+ var _step_volume;
1329
+ if (scheduled.has(i)) continue;
1330
+ const stepTime = baseTime + times[i];
1331
+ if (stepTime > ctx.currentTime + SCHEDULE_AHEAD) continue;
1332
+ scheduled.add(i);
1333
+ const step = steps[i];
1334
+ const volume = (_step_volume = step.volume) != null ? _step_volume : opts == null ? void 0 : opts.volume;
1335
+ if (isDefinition(step.sound)) {
1336
+ const handle = render(ctx, step.sound, volume !== undefined ? {
1337
+ volume
1338
+ } : opts, stepTime, getDestination());
1339
+ handles.push(handle);
1340
+ } else {
1341
+ const fn = step.sound;
1342
+ const delay = (stepTime - ctx.currentTime) * 1000;
1343
+ if (delay <= 0) {
1344
+ const result = fn(volume !== undefined ? {
1345
+ volume
1346
+ } : opts);
1347
+ if (result) handles.push(result);
1348
+ } else {
1349
+ setTimeout(()=>fn(volume !== undefined ? {
1350
+ volume
1351
+ } : opts), delay);
1352
+ }
1353
+ }
1354
+ }
1355
+ return handles;
1356
+ }
1357
+ /**
1358
+ * Schedules and plays a sequence of sounds using a lookahead timer.
1359
+ *
1360
+ * Steps are positioned in time via `at` (absolute) or `wait` (relative)
1361
+ * fields. When `options.loop` is true the sequence repeats indefinitely
1362
+ * using `options.duration` as the loop length.
1363
+ *
1364
+ * @param ctx - The real-time `AudioContext`
1365
+ * @param steps - Ordered list of {@link SequenceStep}s
1366
+ * @param options - Loop and duration settings
1367
+ * @param opts - Runtime overrides applied to every step
1368
+ * @returns A stop function that halts playback, or `undefined` if empty
1369
+ */ function playSequence(ctx, steps, options, opts) {
1370
+ var _options_duration;
1371
+ const times = resolveStepTimes(steps);
1372
+ if (!(options == null ? void 0 : options.loop)) {
1373
+ const scheduled = new Set();
1374
+ const handles = [];
1375
+ const tick = ()=>{
1376
+ const h = scheduleOnce(ctx, steps, times, opts, ctx.currentTime, scheduled);
1377
+ handles.push(...h);
1378
+ if (scheduled.size < steps.length) {
1379
+ timerId = setTimeout(tick, LOOKAHEAD_MS);
1380
+ }
1381
+ };
1382
+ let timerId = null;
1383
+ tick();
1384
+ return ()=>{
1385
+ if (timerId !== null) clearTimeout(timerId);
1386
+ for (const h of handles)h.stop();
1387
+ };
1388
+ }
1389
+ const duration = (_options_duration = options.duration) != null ? _options_duration : 1;
1390
+ let stopped = false;
1391
+ let timerId = null;
1392
+ let loopBase = ctx.currentTime;
1393
+ let scheduled = new Set();
1394
+ const handles = [];
1395
+ const tick = ()=>{
1396
+ if (stopped) return;
1397
+ const h = scheduleOnce(ctx, steps, times, opts, loopBase, scheduled);
1398
+ handles.push(...h);
1399
+ if (scheduled.size >= steps.length) {
1400
+ if (ctx.currentTime >= loopBase + duration - SCHEDULE_AHEAD) {
1401
+ loopBase += duration;
1402
+ scheduled = new Set();
1403
+ }
1404
+ }
1405
+ };
1406
+ timerId = setInterval(tick, LOOKAHEAD_MS);
1407
+ tick();
1408
+ return ()=>{
1409
+ stopped = true;
1410
+ if (timerId !== null) clearInterval(timerId);
1411
+ for (const h of handles)h.stop();
1412
+ };
1413
+ }
1414
+
1415
+ /**
1416
+ * Binds a {@link SoundDefinition} into a reusable play function.
1417
+ *
1418
+ * The returned function creates a new voice each time it is called,
1419
+ * routing through the master bus.
1420
+ *
1421
+ * @param definition - The sound to bind
1422
+ * @returns A function that plays the sound and returns a {@link VoiceHandle}
1423
+ *
1424
+ * @example
1425
+ * ```typescript
1426
+ * import { defineSound } from "@litlab/audx";
1427
+ *
1428
+ * const click = defineSound({
1429
+ * source: { type: "sine", frequency: { start: 1800, end: 400 } },
1430
+ * envelope: { attack: 0, decay: 0.08 },
1431
+ * gain: 0.3,
1432
+ * });
1433
+ *
1434
+ * click(); // plays the sound
1435
+ * ```
1436
+ */ function defineSound(definition) {
1437
+ return (opts)=>{
1438
+ const ctx = getContext();
1439
+ return render(ctx, definition, opts, undefined, getDestination());
1440
+ };
1441
+ }
1442
+ /**
1443
+ * Binds a list of {@link SequenceStep}s into a reusable play function.
1444
+ *
1445
+ * @param steps - Ordered list of sequence steps
1446
+ * @param options - Loop and duration settings
1447
+ * @returns A function that starts the sequence and returns a stop callback
1448
+ *
1449
+ * @example
1450
+ * ```typescript
1451
+ * const melody = defineSequence([
1452
+ * { sound: noteC, at: 0 },
1453
+ * { sound: noteE, at: 0.25 },
1454
+ * { sound: noteG, at: 0.5 },
1455
+ * ], { loop: true, duration: 1 });
1456
+ *
1457
+ * const stop = melody();
1458
+ * // later...
1459
+ * stop?.();
1460
+ * ```
1461
+ */ function defineSequence(steps, options) {
1462
+ return (opts)=>{
1463
+ const ctx = getContext();
1464
+ return playSequence(ctx, steps, options, opts);
1465
+ };
1466
+ }
1467
+ function osc(type, frequency, decay, gain = 0.4) {
1468
+ return defineSound({
1469
+ source: {
1470
+ type,
1471
+ frequency
1472
+ },
1473
+ envelope: {
1474
+ decay
1475
+ },
1476
+ gain
1477
+ });
1478
+ }
1479
+ /**
1480
+ * Shortcut: creates a sine-wave sound with the given frequency and decay.
1481
+ *
1482
+ * @param frequency - Fixed Hz or `{ start, end }` sweep
1483
+ * @param decay - Envelope decay time in seconds
1484
+ * @param gain - Output gain (0 – 1). @defaultValue `0.4`
1485
+ */ function sine(frequency, decay, gain) {
1486
+ return osc("sine", frequency, decay, gain);
1487
+ }
1488
+ /**
1489
+ * Shortcut: creates a triangle-wave sound with the given frequency and decay.
1490
+ *
1491
+ * @param frequency - Fixed Hz or `{ start, end }` sweep
1492
+ * @param decay - Envelope decay time in seconds
1493
+ * @param gain - Output gain (0 – 1). @defaultValue `0.4`
1494
+ */ function triangle(frequency, decay, gain) {
1495
+ return osc("triangle", frequency, decay, gain);
1496
+ }
1497
+ /**
1498
+ * Shortcut: creates a square-wave sound with the given frequency and decay.
1499
+ *
1500
+ * @param frequency - Fixed Hz or `{ start, end }` sweep
1501
+ * @param decay - Envelope decay time in seconds
1502
+ * @param gain - Output gain (0 – 1). @defaultValue `0.4`
1503
+ */ function square(frequency, decay, gain) {
1504
+ return osc("square", frequency, decay, gain);
1505
+ }
1506
+ /**
1507
+ * Shortcut: creates a sawtooth-wave sound with the given frequency and decay.
1508
+ *
1509
+ * @param frequency - Fixed Hz or `{ start, end }` sweep
1510
+ * @param decay - Envelope decay time in seconds
1511
+ * @param gain - Output gain (0 – 1). @defaultValue `0.4`
1512
+ */ function sawtooth(frequency, decay, gain) {
1513
+ return osc("sawtooth", frequency, decay, gain);
1514
+ }
1515
+ /**
1516
+ * Shortcut: creates a noise burst with the given color and decay.
1517
+ *
1518
+ * @param color - Noise spectrum. @defaultValue `"white"`
1519
+ * @param decay - Envelope decay time in seconds. @defaultValue `0.05`
1520
+ * @param gain - Output gain (0 – 1). @defaultValue `0.4`
1521
+ */ function noise(color = "white", decay = 0.05, gain = 0.4) {
1522
+ return defineSound({
1523
+ source: {
1524
+ type: "noise",
1525
+ color
1526
+ },
1527
+ envelope: {
1528
+ decay
1529
+ },
1530
+ gain
1531
+ });
1532
+ }
1533
+
1534
+ export { bufferToWav, createAnalyser, createMasterAnalyser, createPatchInstance, definePatch, defineSequence, defineSound, dispose, ensureReady, getDestination, getListener, getMasterBus, loadPatch, noise, renderToBuffer, renderToWav, sawtooth, setListener, setMasterVolume, sine, square, triangle };