@weasel-js/audio 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/index.d.ts +441 -2
- package/dist/index.js +995 -109
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -70,6 +70,33 @@ function createAnalyserTap(ctx, source, opts = {}) {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// src/audioTime.ts
|
|
74
|
+
var defaultTimers = {
|
|
75
|
+
setTimer: (cb, ms) => setTimeout(cb, ms),
|
|
76
|
+
clearTimer: (h) => clearTimeout(h)
|
|
77
|
+
};
|
|
78
|
+
function atAudioTime(ctx, timers, at, cb) {
|
|
79
|
+
let handle = null;
|
|
80
|
+
let live = true;
|
|
81
|
+
const check = () => {
|
|
82
|
+
handle = null;
|
|
83
|
+
if (!live) return;
|
|
84
|
+
const left = at - ctx.currentTime;
|
|
85
|
+
if (ctx.state === "running" && left > 1e-6) {
|
|
86
|
+
handle = timers.setTimer(check, Math.max(1, left * 1e3));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
live = false;
|
|
90
|
+
cb();
|
|
91
|
+
};
|
|
92
|
+
check();
|
|
93
|
+
return () => {
|
|
94
|
+
live = false;
|
|
95
|
+
if (handle !== null) timers.clearTimer(handle);
|
|
96
|
+
handle = null;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
// src/param.ts
|
|
74
101
|
function writeParam(ctx, param, value, rampMs) {
|
|
75
102
|
const now = ctx.currentTime;
|
|
@@ -82,15 +109,224 @@ function writeParam(ctx, param, value, rampMs) {
|
|
|
82
109
|
}
|
|
83
110
|
}
|
|
84
111
|
|
|
112
|
+
// src/inserts.ts
|
|
113
|
+
function createInsertChain(ctx, head, tail, opts = {}) {
|
|
114
|
+
const fadeMs = opts.fadeMs ?? 15;
|
|
115
|
+
const timers = {
|
|
116
|
+
setTimer: opts.setTimer ?? defaultTimers.setTimer,
|
|
117
|
+
clearTimer: opts.clearTimer ?? defaultTimers.clearTimer
|
|
118
|
+
};
|
|
119
|
+
const used = /* @__PURE__ */ new WeakSet();
|
|
120
|
+
const order = [];
|
|
121
|
+
const live = [];
|
|
122
|
+
const links = [];
|
|
123
|
+
let busy = false;
|
|
124
|
+
let cancel = null;
|
|
125
|
+
let disposed = false;
|
|
126
|
+
let waiters = [];
|
|
127
|
+
const fade = () => ctx.state === "running" ? fadeMs : 0;
|
|
128
|
+
const link = (from, to, gain) => {
|
|
129
|
+
const g = ctx.createGain();
|
|
130
|
+
g.gain.value = gain;
|
|
131
|
+
writeParam(ctx, g.gain, gain);
|
|
132
|
+
from.connect(g);
|
|
133
|
+
g.connect(to);
|
|
134
|
+
const l = { from, to, gain: g };
|
|
135
|
+
links.push(l);
|
|
136
|
+
return l;
|
|
137
|
+
};
|
|
138
|
+
const unlink = (l) => {
|
|
139
|
+
l.from.disconnect(l.gain);
|
|
140
|
+
l.gain.disconnect();
|
|
141
|
+
links.splice(links.indexOf(l), 1);
|
|
142
|
+
};
|
|
143
|
+
const find = (from, to) => links.find((l) => l.from === from && l.to === to);
|
|
144
|
+
const inOf = (s) => s ? s.nodes.in : tail;
|
|
145
|
+
const outOf = (s) => s ? s.nodes.out : head;
|
|
146
|
+
const setBypass = (s, ms) => {
|
|
147
|
+
if (!s.nodes) return;
|
|
148
|
+
writeParam(ctx, s.nodes.wet.gain, s.bypassed ? 0 : 1, ms);
|
|
149
|
+
writeParam(ctx, s.nodes.dry.gain, s.bypassed ? 1 : 0, ms);
|
|
150
|
+
};
|
|
151
|
+
const build = (s) => {
|
|
152
|
+
if (s.nodes) return;
|
|
153
|
+
const n = {
|
|
154
|
+
in: ctx.createGain(),
|
|
155
|
+
out: ctx.createGain(),
|
|
156
|
+
dry: ctx.createGain(),
|
|
157
|
+
wet: ctx.createGain()
|
|
158
|
+
};
|
|
159
|
+
n.in.connect(s.effect.input);
|
|
160
|
+
s.effect.output.connect(n.wet);
|
|
161
|
+
n.wet.connect(n.out);
|
|
162
|
+
n.in.connect(n.dry);
|
|
163
|
+
n.dry.connect(n.out);
|
|
164
|
+
s.nodes = n;
|
|
165
|
+
n.wet.gain.value = s.bypassed ? 0 : 1;
|
|
166
|
+
n.dry.gain.value = s.bypassed ? 1 : 0;
|
|
167
|
+
setBypass(s, 0);
|
|
168
|
+
};
|
|
169
|
+
const destroy = (s) => {
|
|
170
|
+
const n = s.nodes;
|
|
171
|
+
if (!n) return;
|
|
172
|
+
n.in.disconnect();
|
|
173
|
+
try {
|
|
174
|
+
s.effect.output.disconnect(n.wet);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
n.wet.disconnect();
|
|
178
|
+
n.dry.disconnect();
|
|
179
|
+
n.out.disconnect();
|
|
180
|
+
s.nodes = null;
|
|
181
|
+
};
|
|
182
|
+
const plan = () => {
|
|
183
|
+
const stale = live.find((s) => s.removed);
|
|
184
|
+
if (stale) return { kind: "out", slot: stale };
|
|
185
|
+
const wanted = order.filter((s) => live.includes(s));
|
|
186
|
+
for (let i2 = 0; i2 < wanted.length; i2 += 1) {
|
|
187
|
+
if (live[i2] !== wanted[i2]) return { kind: "out", slot: wanted[i2] };
|
|
188
|
+
}
|
|
189
|
+
const i = order.findIndex((s) => !live.includes(s));
|
|
190
|
+
if (i < 0) return null;
|
|
191
|
+
let at = 0;
|
|
192
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
193
|
+
const k = live.indexOf(order[j]);
|
|
194
|
+
if (k >= 0) {
|
|
195
|
+
at = k + 1;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { kind: "in", slot: order[i], at };
|
|
200
|
+
};
|
|
201
|
+
const begin = (step, ms) => {
|
|
202
|
+
const s = step.slot;
|
|
203
|
+
if (step.kind === "in") {
|
|
204
|
+
build(s);
|
|
205
|
+
const prev2 = live[step.at - 1] ?? null;
|
|
206
|
+
const next2 = live[step.at] ?? null;
|
|
207
|
+
const old = find(outOf(prev2), inOf(next2));
|
|
208
|
+
link(outOf(prev2), inOf(s), 1);
|
|
209
|
+
const incoming = link(outOf(s), inOf(next2), 0);
|
|
210
|
+
live.splice(step.at, 0, s);
|
|
211
|
+
writeParam(ctx, old.gain.gain, 0, ms);
|
|
212
|
+
writeParam(ctx, incoming.gain.gain, 1, ms);
|
|
213
|
+
return () => unlink(old);
|
|
214
|
+
}
|
|
215
|
+
const i = live.indexOf(s);
|
|
216
|
+
const prev = live[i - 1] ?? null;
|
|
217
|
+
const next = live[i + 1] ?? null;
|
|
218
|
+
const into = find(outOf(prev), inOf(s));
|
|
219
|
+
const from = find(outOf(s), inOf(next));
|
|
220
|
+
const direct = link(outOf(prev), inOf(next), 0);
|
|
221
|
+
live.splice(i, 1);
|
|
222
|
+
writeParam(ctx, from.gain.gain, 0, ms);
|
|
223
|
+
writeParam(ctx, direct.gain.gain, 1, ms);
|
|
224
|
+
return () => {
|
|
225
|
+
unlink(into);
|
|
226
|
+
unlink(from);
|
|
227
|
+
if (s.removed) {
|
|
228
|
+
destroy(s);
|
|
229
|
+
s.effect.dispose?.();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
const pump = () => {
|
|
234
|
+
while (!busy && !disposed) {
|
|
235
|
+
const step = plan();
|
|
236
|
+
if (!step) {
|
|
237
|
+
const done = waiters;
|
|
238
|
+
waiters = [];
|
|
239
|
+
for (const w of done) w();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const ms = fade();
|
|
243
|
+
const finish = begin(step, ms);
|
|
244
|
+
if (ms === 0) {
|
|
245
|
+
finish();
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
busy = true;
|
|
249
|
+
cancel = atAudioTime(ctx, timers, ctx.currentTime + ms / 1e3, () => {
|
|
250
|
+
cancel = null;
|
|
251
|
+
busy = false;
|
|
252
|
+
finish();
|
|
253
|
+
pump();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
link(head, tail, 1);
|
|
258
|
+
const slotOf = (effect, bypassed) => {
|
|
259
|
+
const s = { effect, bypassed, removed: false, nodes: null, handle: null };
|
|
260
|
+
s.handle = {
|
|
261
|
+
effect,
|
|
262
|
+
bypass(on) {
|
|
263
|
+
if (s.bypassed === on) return;
|
|
264
|
+
s.bypassed = on;
|
|
265
|
+
setBypass(s, fade());
|
|
266
|
+
},
|
|
267
|
+
bypassed: () => s.bypassed,
|
|
268
|
+
moveTo(index) {
|
|
269
|
+
const from = order.indexOf(s);
|
|
270
|
+
if (from < 0) return;
|
|
271
|
+
order.splice(from, 1);
|
|
272
|
+
order.splice(Math.max(0, Math.min(index, order.length)), 0, s);
|
|
273
|
+
pump();
|
|
274
|
+
},
|
|
275
|
+
remove() {
|
|
276
|
+
const at = order.indexOf(s);
|
|
277
|
+
if (at < 0) return;
|
|
278
|
+
order.splice(at, 1);
|
|
279
|
+
s.removed = true;
|
|
280
|
+
pump();
|
|
281
|
+
},
|
|
282
|
+
index: () => order.indexOf(s)
|
|
283
|
+
};
|
|
284
|
+
return s;
|
|
285
|
+
};
|
|
286
|
+
const chain = {
|
|
287
|
+
add(effect, addOpts = {}) {
|
|
288
|
+
if (used.has(effect)) {
|
|
289
|
+
throw new Error("@weasel-js/audio: that effect is already in an insert slot");
|
|
290
|
+
}
|
|
291
|
+
used.add(effect);
|
|
292
|
+
const s = slotOf(effect, addOpts.bypassed ?? false);
|
|
293
|
+
const index = addOpts.index ?? order.length;
|
|
294
|
+
order.splice(Math.max(0, Math.min(index, order.length)), 0, s);
|
|
295
|
+
pump();
|
|
296
|
+
return s.handle;
|
|
297
|
+
},
|
|
298
|
+
slots: () => order.map((s) => s.handle),
|
|
299
|
+
clear() {
|
|
300
|
+
for (const s of [...order]) s.handle.remove();
|
|
301
|
+
},
|
|
302
|
+
settled() {
|
|
303
|
+
if (!busy && plan() === null) return Promise.resolve();
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
waiters.push(resolve);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
chain,
|
|
311
|
+
dispose() {
|
|
312
|
+
disposed = true;
|
|
313
|
+
cancel?.();
|
|
314
|
+
cancel = null;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
85
319
|
// src/buses.ts
|
|
86
|
-
function createBusGraph(ctx, names) {
|
|
320
|
+
function createBusGraph(ctx, names, opts = {}) {
|
|
87
321
|
const master = ctx.createGain();
|
|
88
322
|
master.connect(ctx.destination);
|
|
89
323
|
const states = /* @__PURE__ */ new Map();
|
|
90
324
|
for (const name of names) {
|
|
325
|
+
const input = ctx.createGain();
|
|
91
326
|
const node = ctx.createGain();
|
|
92
327
|
node.connect(master);
|
|
93
|
-
|
|
328
|
+
const inserts = createInsertChain(ctx, input, node, opts);
|
|
329
|
+
states.set(name, { input, node, inserts, gain: 1, muted: false, soloed: false });
|
|
94
330
|
}
|
|
95
331
|
const anySoloed = () => {
|
|
96
332
|
for (const s of states.values()) if (s.soloed) return true;
|
|
@@ -111,8 +347,12 @@ function createBusGraph(ctx, names) {
|
|
|
111
347
|
};
|
|
112
348
|
return {
|
|
113
349
|
master,
|
|
350
|
+
input: (name) => get(name).input,
|
|
114
351
|
node: (name) => get(name).node,
|
|
115
352
|
names: () => [...states.keys()],
|
|
353
|
+
dispose() {
|
|
354
|
+
for (const s of states.values()) s.inserts.dispose();
|
|
355
|
+
},
|
|
116
356
|
bus(name) {
|
|
117
357
|
const s = get(name);
|
|
118
358
|
return {
|
|
@@ -131,7 +371,8 @@ function createBusGraph(ctx, names) {
|
|
|
131
371
|
gain: () => s.gain,
|
|
132
372
|
muted: () => s.muted,
|
|
133
373
|
soloed: () => s.soloed,
|
|
134
|
-
audible: () => audibleUnder(s, anySoloed())
|
|
374
|
+
audible: () => audibleUnder(s, anySoloed()),
|
|
375
|
+
inserts: s.inserts.chain
|
|
135
376
|
};
|
|
136
377
|
}
|
|
137
378
|
};
|
|
@@ -348,6 +589,212 @@ function createTickTimer() {
|
|
|
348
589
|
};
|
|
349
590
|
}
|
|
350
591
|
|
|
592
|
+
// src/envelope.ts
|
|
593
|
+
function resolveEnvelope(env = {}) {
|
|
594
|
+
return {
|
|
595
|
+
attack: Math.max(0, env.attack ?? 5),
|
|
596
|
+
decay: Math.max(0, env.decay ?? 0),
|
|
597
|
+
sustain: Math.min(1, Math.max(0, env.sustain ?? 1)),
|
|
598
|
+
release: Math.max(0, env.release ?? 30)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
var heldLevel = (e, t) => {
|
|
602
|
+
if (t < e.attack) return t / e.attack;
|
|
603
|
+
if (t < e.attack + e.decay) return 1 - (1 - e.sustain) * (t - e.attack) / e.decay;
|
|
604
|
+
return e.sustain;
|
|
605
|
+
};
|
|
606
|
+
function envelopeLevel(e, t, gate) {
|
|
607
|
+
if (gate === void 0 || t <= gate) return heldLevel(e, t);
|
|
608
|
+
if (e.release === 0) return 0;
|
|
609
|
+
return Math.max(0, heldLevel(e, gate) * (1 - (t - gate) / e.release));
|
|
610
|
+
}
|
|
611
|
+
function envelopePoints(e, gate) {
|
|
612
|
+
const end = gate ?? Infinity;
|
|
613
|
+
const points = [{ at: 0, value: e.attack > 0 ? 0 : 1 }];
|
|
614
|
+
for (const at of [e.attack, e.attack + e.decay]) {
|
|
615
|
+
if (at <= points[points.length - 1].at) continue;
|
|
616
|
+
if (at >= end) break;
|
|
617
|
+
points.push({ at, value: heldLevel(e, at) });
|
|
618
|
+
}
|
|
619
|
+
if (gate === void 0) return points;
|
|
620
|
+
if (gate > points[points.length - 1].at) points.push({ at: gate, value: heldLevel(e, gate) });
|
|
621
|
+
points.push({ at: gate + e.release, value: 0 });
|
|
622
|
+
return points;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/pitch.ts
|
|
626
|
+
function midiToFrequency(midi) {
|
|
627
|
+
return 440 * 2 ** ((midi - 69) / 12);
|
|
628
|
+
}
|
|
629
|
+
var LETTERS = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
|
630
|
+
function noteToMidi(name) {
|
|
631
|
+
const m = /^([A-Ga-g])([#b]*)(-?\d+)$/.exec(name.trim());
|
|
632
|
+
if (!m) throw new Error(`@weasel-js/audio: cannot read "${name}" as a note name`);
|
|
633
|
+
let semitone = LETTERS[m[1].toLowerCase()];
|
|
634
|
+
for (const acc of m[2]) semitone += acc === "#" ? 1 : -1;
|
|
635
|
+
return (Number(m[3]) + 1) * 12 + semitone;
|
|
636
|
+
}
|
|
637
|
+
function toFrequency(pitch) {
|
|
638
|
+
const hz = typeof pitch === "number" ? pitch : typeof pitch === "string" ? midiToFrequency(noteToMidi(pitch)) : midiToFrequency(pitch.midi);
|
|
639
|
+
if (!(hz > 0) || !Number.isFinite(hz)) {
|
|
640
|
+
throw new RangeError(`@weasel-js/audio: a pitch must be a positive frequency, got ${hz}`);
|
|
641
|
+
}
|
|
642
|
+
return hz;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/synthVoice.ts
|
|
646
|
+
var NOISE_SECONDS = 2;
|
|
647
|
+
var NOISE_RMS = 0.25;
|
|
648
|
+
var noiseCache = /* @__PURE__ */ new WeakMap();
|
|
649
|
+
function noiseSamples(color, length) {
|
|
650
|
+
const out = new Float32Array(length);
|
|
651
|
+
let s = 2654435769;
|
|
652
|
+
const white = () => {
|
|
653
|
+
s = Math.imul(s, 1664525) + 1013904223 >>> 0;
|
|
654
|
+
return s / 4294967296 * 2 - 1;
|
|
655
|
+
};
|
|
656
|
+
if (color === "white") {
|
|
657
|
+
for (let i = 0; i < length; i += 1) out[i] = white();
|
|
658
|
+
} else if (color === "pink") {
|
|
659
|
+
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
|
|
660
|
+
for (let i = 0; i < length; i += 1) {
|
|
661
|
+
const w = white();
|
|
662
|
+
b0 = 0.99886 * b0 + w * 0.0555179;
|
|
663
|
+
b1 = 0.99332 * b1 + w * 0.0750759;
|
|
664
|
+
b2 = 0.969 * b2 + w * 0.153852;
|
|
665
|
+
b3 = 0.8665 * b3 + w * 0.3104856;
|
|
666
|
+
b4 = 0.55 * b4 + w * 0.5329522;
|
|
667
|
+
b5 = -0.7616 * b5 - w * 0.016898;
|
|
668
|
+
out[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362;
|
|
669
|
+
b6 = w * 0.115926;
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
let y = 0;
|
|
673
|
+
for (let i = 0; i < length; i += 1) {
|
|
674
|
+
y = (y + 0.02 * white()) / 1.02;
|
|
675
|
+
out[i] = y;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const step = (out[length - 1] - out[0]) / Math.max(1, length - 1);
|
|
679
|
+
let mean = 0;
|
|
680
|
+
for (let i = 0; i < length; i += 1) {
|
|
681
|
+
out[i] -= step * i;
|
|
682
|
+
mean += out[i];
|
|
683
|
+
}
|
|
684
|
+
mean /= length;
|
|
685
|
+
let sq = 0;
|
|
686
|
+
for (let i = 0; i < length; i += 1) {
|
|
687
|
+
out[i] -= mean;
|
|
688
|
+
sq += out[i] * out[i];
|
|
689
|
+
}
|
|
690
|
+
const scale = sq > 0 ? NOISE_RMS / Math.sqrt(sq / length) : 0;
|
|
691
|
+
for (let i = 0; i < length; i += 1) out[i] *= scale;
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
function noiseBuffer(ctx, color) {
|
|
695
|
+
let byColor = noiseCache.get(ctx);
|
|
696
|
+
if (!byColor) {
|
|
697
|
+
byColor = /* @__PURE__ */ new Map();
|
|
698
|
+
noiseCache.set(ctx, byColor);
|
|
699
|
+
}
|
|
700
|
+
let buffer = byColor.get(color);
|
|
701
|
+
if (!buffer) {
|
|
702
|
+
const length = Math.round(ctx.sampleRate * NOISE_SECONDS);
|
|
703
|
+
buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
704
|
+
buffer.getChannelData(0).set(noiseSamples(color, length));
|
|
705
|
+
byColor.set(color, buffer);
|
|
706
|
+
}
|
|
707
|
+
return buffer;
|
|
708
|
+
}
|
|
709
|
+
var isInharmonic = (wave) => typeof wave === "object" && !Array.isArray(wave);
|
|
710
|
+
function resolvePartials(wave) {
|
|
711
|
+
if (wave.partials.length === 0) {
|
|
712
|
+
throw new RangeError("@weasel-js/audio: an inharmonic wave needs at least one partial");
|
|
713
|
+
}
|
|
714
|
+
let total = 0;
|
|
715
|
+
for (const p of wave.partials) {
|
|
716
|
+
if (!(p.ratio > 0) || !Number.isFinite(p.ratio)) {
|
|
717
|
+
throw new RangeError(`@weasel-js/audio: partial ratio must be a positive number, got ${p.ratio}`);
|
|
718
|
+
}
|
|
719
|
+
if (p.decay !== void 0 && !(p.decay > 0)) {
|
|
720
|
+
throw new RangeError(`@weasel-js/audio: partial decay must be a positive number of ms, got ${p.decay}`);
|
|
721
|
+
}
|
|
722
|
+
total += Math.abs(p.gain ?? 1);
|
|
723
|
+
}
|
|
724
|
+
return wave.partials.map((p) => ({
|
|
725
|
+
ratio: p.ratio,
|
|
726
|
+
gain: total > 0 ? (p.gain ?? 1) / total : 0,
|
|
727
|
+
decay: p.decay
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
function envelopeOnParam(param, env, gate, t0, map) {
|
|
731
|
+
const [first, ...rest] = envelopePoints(env, gate);
|
|
732
|
+
param.setValueAtTime(map(first.value), t0);
|
|
733
|
+
for (const p of rest) param.linearRampToValueAtTime(map(p.value), t0 + p.at / 1e3);
|
|
734
|
+
return {
|
|
735
|
+
release(at) {
|
|
736
|
+
const elapsed = Math.max(0, (at - t0) * 1e3);
|
|
737
|
+
param.cancelScheduledValues(at);
|
|
738
|
+
param.setValueAtTime(map(envelopeLevel(env, elapsed)), at);
|
|
739
|
+
param.linearRampToValueAtTime(map(0), at + env.release / 1e3);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function voiceFilter(ctx, spec, gate, t0) {
|
|
744
|
+
const node = ctx.createBiquadFilter();
|
|
745
|
+
node.type = spec.type ?? "lowpass";
|
|
746
|
+
node.Q.setValueAtTime(spec.Q ?? 1, t0);
|
|
747
|
+
if (spec.gain !== void 0) node.gain.setValueAtTime(spec.gain, t0);
|
|
748
|
+
const amount = spec.amount ?? 0;
|
|
749
|
+
if (!spec.envelope || amount === 0) {
|
|
750
|
+
node.frequency.setValueAtTime(spec.frequency, t0);
|
|
751
|
+
return { node, release: () => {
|
|
752
|
+
} };
|
|
753
|
+
}
|
|
754
|
+
const cutoff = envelopeOnParam(
|
|
755
|
+
node.frequency,
|
|
756
|
+
resolveEnvelope(spec.envelope),
|
|
757
|
+
gate,
|
|
758
|
+
t0,
|
|
759
|
+
(v) => spec.frequency + amount * v
|
|
760
|
+
);
|
|
761
|
+
return { node, release: cutoff.release };
|
|
762
|
+
}
|
|
763
|
+
var partialPresets = {
|
|
764
|
+
/** A church bell's hum, prime, tierce, quint and nominal, and above. */
|
|
765
|
+
bell: {
|
|
766
|
+
partials: [
|
|
767
|
+
{ ratio: 0.5, gain: 0.6, decay: 1800 },
|
|
768
|
+
{ ratio: 1, gain: 1, decay: 1200 },
|
|
769
|
+
{ ratio: 1.183, gain: 0.7, decay: 900 },
|
|
770
|
+
{ ratio: 1.506, gain: 0.4, decay: 700 },
|
|
771
|
+
{ ratio: 2, gain: 0.8, decay: 600 },
|
|
772
|
+
{ ratio: 2.514, gain: 0.3, decay: 400 },
|
|
773
|
+
{ ratio: 2.662, gain: 0.25, decay: 350 },
|
|
774
|
+
{ ratio: 3.011, gain: 0.2, decay: 300 }
|
|
775
|
+
]
|
|
776
|
+
},
|
|
777
|
+
/** A tuned bar: its overtones sit near 4 and 10 times the fundamental and die fast. */
|
|
778
|
+
marimba: {
|
|
779
|
+
partials: [
|
|
780
|
+
{ ratio: 1, gain: 1, decay: 350 },
|
|
781
|
+
{ ratio: 3.984, gain: 0.35, decay: 90 },
|
|
782
|
+
{ ratio: 9.95, gain: 0.12, decay: 30 }
|
|
783
|
+
]
|
|
784
|
+
},
|
|
785
|
+
/** Struck sheet metal: modes nowhere near the harmonic series. */
|
|
786
|
+
metal: {
|
|
787
|
+
partials: [
|
|
788
|
+
{ ratio: 1, gain: 1, decay: 160 },
|
|
789
|
+
{ ratio: 1.71, gain: 0.53, decay: 120 },
|
|
790
|
+
{ ratio: 2.43, gain: 0.36, decay: 94 },
|
|
791
|
+
{ ratio: 3.19, gain: 0.27, decay: 78 },
|
|
792
|
+
{ ratio: 4.61, gain: 0.22, decay: 66 },
|
|
793
|
+
{ ratio: 5.87, gain: 0.18, decay: 58 }
|
|
794
|
+
]
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
351
798
|
// src/voicePool.ts
|
|
352
799
|
function createVoicePool(opts) {
|
|
353
800
|
if (!(opts.limit >= 1)) {
|
|
@@ -398,6 +845,25 @@ function createVoicePool(opts) {
|
|
|
398
845
|
}
|
|
399
846
|
|
|
400
847
|
// src/createAudioEngine.ts
|
|
848
|
+
var sourceGroup = (nodes) => ({
|
|
849
|
+
get onended() {
|
|
850
|
+
return nodes[0].onended;
|
|
851
|
+
},
|
|
852
|
+
set onended(fn) {
|
|
853
|
+
nodes[0].onended = fn;
|
|
854
|
+
},
|
|
855
|
+
stop(when) {
|
|
856
|
+
for (const n of nodes) {
|
|
857
|
+
try {
|
|
858
|
+
n.stop(when);
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
disconnect() {
|
|
864
|
+
for (const n of nodes) n.disconnect();
|
|
865
|
+
}
|
|
866
|
+
});
|
|
401
867
|
function createAudioEngine(opts = {}) {
|
|
402
868
|
const ctx = opts.context ?? new AudioContext();
|
|
403
869
|
const ownsContext = opts.context === void 0;
|
|
@@ -405,7 +871,6 @@ function createAudioEngine(opts = {}) {
|
|
|
405
871
|
if (busNames.length === 0) {
|
|
406
872
|
throw new Error("@weasel-js/audio: an engine needs at least one bus");
|
|
407
873
|
}
|
|
408
|
-
const graph = createBusGraph(ctx, busNames);
|
|
409
874
|
const sounds = createSoundCache(ctx, opts.fetchFn);
|
|
410
875
|
const slotsByBus = /* @__PURE__ */ new Map();
|
|
411
876
|
for (const name of busNames) {
|
|
@@ -421,17 +886,22 @@ function createAudioEngine(opts = {}) {
|
|
|
421
886
|
};
|
|
422
887
|
const now = () => ctx.currentTime * 1e3;
|
|
423
888
|
const tickTimer = opts.setTimer === void 0 && opts.clearTimer === void 0 ? createTickTimer() : null;
|
|
889
|
+
const timers = {
|
|
890
|
+
setTimer: tickTimer?.setTimer ?? opts.setTimer ?? defaultTimers.setTimer,
|
|
891
|
+
clearTimer: tickTimer?.clearTimer ?? opts.clearTimer ?? defaultTimers.clearTimer
|
|
892
|
+
};
|
|
424
893
|
const scheduler = createScheduler({
|
|
425
894
|
now,
|
|
426
895
|
// One-shot, not repeating: the scheduler re-arms at the end of every pass,
|
|
427
896
|
// so an interval would leave the previous one running and double the live
|
|
428
897
|
// timer count per tick.
|
|
429
|
-
setTimer:
|
|
430
|
-
clearTimer:
|
|
898
|
+
setTimer: timers.setTimer,
|
|
899
|
+
clearTimer: timers.clearTimer,
|
|
431
900
|
lookahead: opts.lookahead,
|
|
432
901
|
interval: opts.tickInterval
|
|
433
902
|
});
|
|
434
903
|
scheduler.start();
|
|
904
|
+
const graph = createBusGraph(ctx, busNames, { ...timers, fadeMs: opts.insertFadeMs });
|
|
435
905
|
let listener = { x: 0, y: 0 };
|
|
436
906
|
let spatialOpts = {};
|
|
437
907
|
let nextVoiceId = 1;
|
|
@@ -524,6 +994,10 @@ function createAudioEngine(opts = {}) {
|
|
|
524
994
|
voice.source.disconnect();
|
|
525
995
|
}
|
|
526
996
|
voice.source = null;
|
|
997
|
+
voice.retune = null;
|
|
998
|
+
voice.release = null;
|
|
999
|
+
voice.env?.disconnect();
|
|
1000
|
+
voice.env = null;
|
|
527
1001
|
if (voice.chain) {
|
|
528
1002
|
voice.chain.gain.disconnect();
|
|
529
1003
|
if (idleChains.length < idleChainLimit) idleChains.push(voice.chain);
|
|
@@ -539,6 +1013,8 @@ function createAudioEngine(opts = {}) {
|
|
|
539
1013
|
id,
|
|
540
1014
|
stop: () => {
|
|
541
1015
|
},
|
|
1016
|
+
release: () => {
|
|
1017
|
+
},
|
|
542
1018
|
setGain: () => {
|
|
543
1019
|
},
|
|
544
1020
|
setRate: () => {
|
|
@@ -551,6 +1027,207 @@ function createAudioEngine(opts = {}) {
|
|
|
551
1027
|
},
|
|
552
1028
|
isPlaying: () => false
|
|
553
1029
|
});
|
|
1030
|
+
const launch = (common, starter) => {
|
|
1031
|
+
const id = nextVoiceId++;
|
|
1032
|
+
if (disposed) {
|
|
1033
|
+
warnDisposed();
|
|
1034
|
+
return dropped(id);
|
|
1035
|
+
}
|
|
1036
|
+
if (ctx.state !== "running") {
|
|
1037
|
+
warnLocked();
|
|
1038
|
+
return dropped(id);
|
|
1039
|
+
}
|
|
1040
|
+
const busName = common.bus ?? busNames[0];
|
|
1041
|
+
const slots = slotsFor(busName);
|
|
1042
|
+
const bookedAt = now();
|
|
1043
|
+
const when = common.when ?? bookedAt;
|
|
1044
|
+
const baseGain = common.gain ?? 1;
|
|
1045
|
+
const spatial = common.position ? spatialize(common.position, listener, spatialOpts) : { gain: 1, pan: common.pan ?? 0 };
|
|
1046
|
+
const voice = {
|
|
1047
|
+
id,
|
|
1048
|
+
slots,
|
|
1049
|
+
slot: null,
|
|
1050
|
+
token: 0,
|
|
1051
|
+
key: common.cancelKey,
|
|
1052
|
+
source: null,
|
|
1053
|
+
env: null,
|
|
1054
|
+
retune: null,
|
|
1055
|
+
release: null,
|
|
1056
|
+
releasing: false,
|
|
1057
|
+
chain: takeChain(graph.input(busName), baseGain * spatial.gain, spatial.pan),
|
|
1058
|
+
baseGain,
|
|
1059
|
+
spatialGain: spatial.gain,
|
|
1060
|
+
rate: common.rate ?? 1,
|
|
1061
|
+
detune: common.detune ?? 0,
|
|
1062
|
+
position: common.position,
|
|
1063
|
+
playing: true,
|
|
1064
|
+
cancelled: false
|
|
1065
|
+
};
|
|
1066
|
+
live.set(id, voice);
|
|
1067
|
+
scheduler.schedule(when, (scheduledWhen) => {
|
|
1068
|
+
if (voice.cancelled) return;
|
|
1069
|
+
if (bookedAt < staleFloor && scheduledWhen < staleFloor) {
|
|
1070
|
+
teardown(voice);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (!starter.ready()) {
|
|
1074
|
+
teardown(voice);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const acquired = slots.pool.acquire({
|
|
1078
|
+
startedAt: scheduledWhen,
|
|
1079
|
+
gain: voice.baseGain * voice.spatialGain
|
|
1080
|
+
});
|
|
1081
|
+
voice.slot = acquired.slot;
|
|
1082
|
+
voice.token = acquired.token;
|
|
1083
|
+
slots.byToken.set(acquired.token, voice);
|
|
1084
|
+
if (acquired.stolen !== null) {
|
|
1085
|
+
const victim = slots.byToken.get(acquired.stolen);
|
|
1086
|
+
if (victim) teardown(victim);
|
|
1087
|
+
}
|
|
1088
|
+
const source = starter.start(voice, scheduledWhen);
|
|
1089
|
+
source.onended = () => {
|
|
1090
|
+
teardown(voice);
|
|
1091
|
+
common.onDone?.();
|
|
1092
|
+
};
|
|
1093
|
+
voice.source = source;
|
|
1094
|
+
}, common.cancelKey);
|
|
1095
|
+
return {
|
|
1096
|
+
id,
|
|
1097
|
+
stop(fadeMs) {
|
|
1098
|
+
if (fadeMs && fadeMs > 0 && voice.source && voice.chain && !voice.cancelled) {
|
|
1099
|
+
voice.cancelled = true;
|
|
1100
|
+
writeParam(ctx, voice.chain.gain.gain, 0, fadeMs);
|
|
1101
|
+
try {
|
|
1102
|
+
voice.source.stop(ctx.currentTime + fadeMs / 1e3);
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
teardown(voice);
|
|
1108
|
+
},
|
|
1109
|
+
release() {
|
|
1110
|
+
if (voice.releasing) return;
|
|
1111
|
+
if (voice.release && voice.source && !voice.cancelled) {
|
|
1112
|
+
voice.releasing = true;
|
|
1113
|
+
voice.cancelled = true;
|
|
1114
|
+
voice.release();
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
teardown(voice);
|
|
1118
|
+
},
|
|
1119
|
+
setGain(value, rampMs) {
|
|
1120
|
+
voice.baseGain = value;
|
|
1121
|
+
if (!voice.chain) return;
|
|
1122
|
+
writeParam(ctx, voice.chain.gain.gain, value * voice.spatialGain, rampMs);
|
|
1123
|
+
poolGain(voice);
|
|
1124
|
+
},
|
|
1125
|
+
setRate(value) {
|
|
1126
|
+
voice.rate = value;
|
|
1127
|
+
voice.retune?.();
|
|
1128
|
+
},
|
|
1129
|
+
setDetune(cents) {
|
|
1130
|
+
voice.detune = cents;
|
|
1131
|
+
voice.retune?.();
|
|
1132
|
+
},
|
|
1133
|
+
setPan(value) {
|
|
1134
|
+
voice.position = void 0;
|
|
1135
|
+
if (voice.chain) voice.chain.panner.pan.value = value;
|
|
1136
|
+
},
|
|
1137
|
+
setPosition(p) {
|
|
1138
|
+
voice.position = p;
|
|
1139
|
+
applySpatial(voice);
|
|
1140
|
+
},
|
|
1141
|
+
isPlaying: () => voice.playing
|
|
1142
|
+
};
|
|
1143
|
+
};
|
|
1144
|
+
const synthStart = (voice, whenMs, patch, make) => {
|
|
1145
|
+
const t0 = whenMs / 1e3;
|
|
1146
|
+
const env = resolveEnvelope(patch.envelope);
|
|
1147
|
+
const gate = patch.duration;
|
|
1148
|
+
const shaper = ctx.createGain();
|
|
1149
|
+
const amp = envelopeOnParam(shaper.gain, env, gate, t0, (v) => v);
|
|
1150
|
+
const filter = patch.filter ? voiceFilter(ctx, patch.filter, gate, t0) : null;
|
|
1151
|
+
filter?.node.connect(shaper);
|
|
1152
|
+
shaper.connect(voice.chain.panner);
|
|
1153
|
+
voice.env = shaper;
|
|
1154
|
+
const { nodes, retune } = make(t0, filter?.node ?? shaper);
|
|
1155
|
+
voice.retune = retune;
|
|
1156
|
+
retune();
|
|
1157
|
+
const stopAll = (at) => {
|
|
1158
|
+
for (const n of nodes) n.stop(at);
|
|
1159
|
+
};
|
|
1160
|
+
voice.release = () => {
|
|
1161
|
+
const at = ctx.currentTime;
|
|
1162
|
+
if (gate !== void 0 && at * 1e3 - whenMs >= gate) return;
|
|
1163
|
+
amp.release(at);
|
|
1164
|
+
filter?.release(at);
|
|
1165
|
+
stopAll(at + env.release / 1e3);
|
|
1166
|
+
};
|
|
1167
|
+
if (gate !== void 0) stopAll(t0 + (gate + env.release) / 1e3);
|
|
1168
|
+
return nodes.length === 1 ? nodes[0] : sourceGroup(nodes);
|
|
1169
|
+
};
|
|
1170
|
+
const waves = /* @__PURE__ */ new Map();
|
|
1171
|
+
const applyWave = (osc, wave) => {
|
|
1172
|
+
if (typeof wave === "string") {
|
|
1173
|
+
osc.type = wave;
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const key = wave.join(",");
|
|
1177
|
+
let periodic = waves.get(key);
|
|
1178
|
+
if (!periodic) {
|
|
1179
|
+
const imag = new Float32Array(wave.length + 1);
|
|
1180
|
+
imag.set(wave, 1);
|
|
1181
|
+
periodic = ctx.createPeriodicWave(new Float32Array(wave.length + 1), imag);
|
|
1182
|
+
waves.set(key, periodic);
|
|
1183
|
+
}
|
|
1184
|
+
osc.setPeriodicWave(periodic);
|
|
1185
|
+
};
|
|
1186
|
+
const mediaSources = /* @__PURE__ */ new WeakMap();
|
|
1187
|
+
const streaming = /* @__PURE__ */ new WeakMap();
|
|
1188
|
+
const openMedia = (url, crossOrigin) => {
|
|
1189
|
+
const el = new Audio();
|
|
1190
|
+
if (crossOrigin !== void 0) el.crossOrigin = crossOrigin;
|
|
1191
|
+
el.preload = "auto";
|
|
1192
|
+
el.src = url;
|
|
1193
|
+
return el;
|
|
1194
|
+
};
|
|
1195
|
+
const mediaSource = (el, node, into) => {
|
|
1196
|
+
let ended = false;
|
|
1197
|
+
let cancelWait = null;
|
|
1198
|
+
const finish = () => {
|
|
1199
|
+
if (ended) return;
|
|
1200
|
+
ended = true;
|
|
1201
|
+
cancelWait?.();
|
|
1202
|
+
el.pause();
|
|
1203
|
+
el.removeEventListener("ended", finish);
|
|
1204
|
+
el.removeEventListener("error", finish);
|
|
1205
|
+
queueMicrotask(() => shim.onended?.());
|
|
1206
|
+
};
|
|
1207
|
+
const shim = {
|
|
1208
|
+
onended: null,
|
|
1209
|
+
stop(when) {
|
|
1210
|
+
if (ended) return;
|
|
1211
|
+
cancelWait?.();
|
|
1212
|
+
cancelWait = when !== void 0 && when > ctx.currentTime ? atAudioTime(ctx, timers, when, finish) : null;
|
|
1213
|
+
if (!cancelWait) finish();
|
|
1214
|
+
},
|
|
1215
|
+
disconnect() {
|
|
1216
|
+
try {
|
|
1217
|
+
node.disconnect(into);
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
el.addEventListener("ended", finish);
|
|
1223
|
+
el.addEventListener("error", finish);
|
|
1224
|
+
el.play().catch((err) => {
|
|
1225
|
+
if (ended) return;
|
|
1226
|
+
console.warn(`@weasel-js/audio: a stream would not start \u2014 ${String(err)}`);
|
|
1227
|
+
finish();
|
|
1228
|
+
});
|
|
1229
|
+
return shim;
|
|
1230
|
+
};
|
|
554
1231
|
const engine = {
|
|
555
1232
|
context: ctx,
|
|
556
1233
|
state: () => ctx.state,
|
|
@@ -564,112 +1241,119 @@ function createAudioEngine(opts = {}) {
|
|
|
564
1241
|
decode: sounds.decode,
|
|
565
1242
|
register: sounds.register,
|
|
566
1243
|
play(sound, playOpts = {}) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return dropped(id);
|
|
571
|
-
}
|
|
572
|
-
if (ctx.state !== "running") {
|
|
573
|
-
warnLocked();
|
|
574
|
-
return dropped(id);
|
|
575
|
-
}
|
|
576
|
-
const busName = playOpts.bus ?? busNames[0];
|
|
577
|
-
const slots = slotsFor(busName);
|
|
578
|
-
const bookedAt = now();
|
|
579
|
-
const when = playOpts.when ?? bookedAt;
|
|
580
|
-
const baseGain = playOpts.gain ?? 1;
|
|
581
|
-
const spatial = playOpts.position ? spatialize(playOpts.position, listener, spatialOpts) : { gain: 1, pan: playOpts.pan ?? 0 };
|
|
582
|
-
const voice = {
|
|
583
|
-
id,
|
|
584
|
-
slots,
|
|
585
|
-
slot: null,
|
|
586
|
-
token: 0,
|
|
587
|
-
key: playOpts.cancelKey,
|
|
588
|
-
source: null,
|
|
589
|
-
chain: takeChain(graph.node(busName), baseGain * spatial.gain, spatial.pan),
|
|
590
|
-
baseGain,
|
|
591
|
-
spatialGain: spatial.gain,
|
|
592
|
-
rate: playOpts.rate ?? 1,
|
|
593
|
-
detune: playOpts.detune ?? 0,
|
|
594
|
-
position: playOpts.position,
|
|
595
|
-
playing: true,
|
|
596
|
-
cancelled: false
|
|
597
|
-
};
|
|
598
|
-
live.set(id, voice);
|
|
599
|
-
scheduler.schedule(when, (scheduledWhen) => {
|
|
600
|
-
if (voice.cancelled) return;
|
|
601
|
-
if (bookedAt < staleFloor && scheduledWhen < staleFloor) {
|
|
602
|
-
teardown(voice);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
const buffer = sounds.buffer(sound);
|
|
606
|
-
if (!buffer) {
|
|
1244
|
+
return launch(playOpts, {
|
|
1245
|
+
ready() {
|
|
1246
|
+
if (sounds.buffer(sound)) return true;
|
|
607
1247
|
warnUnknownSound(sound.id);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1248
|
+
return false;
|
|
1249
|
+
},
|
|
1250
|
+
start(voice, when) {
|
|
1251
|
+
const source = ctx.createBufferSource();
|
|
1252
|
+
source.buffer = sounds.buffer(sound);
|
|
1253
|
+
source.loop = playOpts.loop ?? false;
|
|
1254
|
+
voice.retune = () => {
|
|
1255
|
+
source.playbackRate.value = voice.rate;
|
|
1256
|
+
source.detune.value = voice.detune;
|
|
1257
|
+
};
|
|
1258
|
+
voice.retune();
|
|
1259
|
+
source.connect(voice.chain.panner);
|
|
1260
|
+
source.start(when / 1e3);
|
|
1261
|
+
return source;
|
|
621
1262
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
voice.cancelled = true;
|
|
640
|
-
writeParam(ctx, voice.chain.gain.gain, 0, fadeMs);
|
|
641
|
-
try {
|
|
642
|
-
voice.source.stop(ctx.currentTime + fadeMs / 1e3);
|
|
643
|
-
} catch {
|
|
1263
|
+
});
|
|
1264
|
+
},
|
|
1265
|
+
playNote(note) {
|
|
1266
|
+
const from = toFrequency(note.pitch);
|
|
1267
|
+
const glideTo = note.glide ? toFrequency(note.glide.to) : void 0;
|
|
1268
|
+
const wave = note.wave ?? "sine";
|
|
1269
|
+
const partials = isInharmonic(wave) ? resolvePartials(wave) : null;
|
|
1270
|
+
return launch(note, {
|
|
1271
|
+
ready: () => true,
|
|
1272
|
+
start: (voice, whenMs) => synthStart(voice, whenMs, note, (t0, into) => {
|
|
1273
|
+
const tone = (ratio) => {
|
|
1274
|
+
const osc = ctx.createOscillator();
|
|
1275
|
+
osc.frequency.setValueAtTime(from * ratio, t0);
|
|
1276
|
+
if (glideTo !== void 0) {
|
|
1277
|
+
const end = t0 + (note.glide.ms ?? note.duration ?? 100) / 1e3;
|
|
1278
|
+
if (note.glide.curve === "linear") osc.frequency.linearRampToValueAtTime(glideTo * ratio, end);
|
|
1279
|
+
else osc.frequency.exponentialRampToValueAtTime(glideTo * ratio, end);
|
|
644
1280
|
}
|
|
645
|
-
return;
|
|
1281
|
+
return osc;
|
|
1282
|
+
};
|
|
1283
|
+
let oscs;
|
|
1284
|
+
if (partials) {
|
|
1285
|
+
oscs = partials.map((p) => {
|
|
1286
|
+
const osc = tone(p.ratio);
|
|
1287
|
+
const level = ctx.createGain();
|
|
1288
|
+
level.gain.setValueAtTime(p.gain, t0);
|
|
1289
|
+
if (p.decay !== void 0) level.gain.setTargetAtTime(0, t0, p.decay / 1e3);
|
|
1290
|
+
osc.connect(level);
|
|
1291
|
+
level.connect(into);
|
|
1292
|
+
return osc;
|
|
1293
|
+
});
|
|
1294
|
+
} else {
|
|
1295
|
+
const osc = tone(1);
|
|
1296
|
+
applyWave(osc, wave);
|
|
1297
|
+
osc.connect(into);
|
|
1298
|
+
oscs = [osc];
|
|
646
1299
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1300
|
+
for (const osc of oscs) osc.start(t0);
|
|
1301
|
+
return {
|
|
1302
|
+
nodes: oscs,
|
|
1303
|
+
retune: () => {
|
|
1304
|
+
for (const osc of oscs) osc.detune.value = voice.detune + 1200 * Math.log2(voice.rate);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
})
|
|
1308
|
+
});
|
|
1309
|
+
},
|
|
1310
|
+
playNoise(noise) {
|
|
1311
|
+
return launch(noise, {
|
|
1312
|
+
ready: () => true,
|
|
1313
|
+
start: (voice, whenMs) => synthStart(voice, whenMs, noise, (t0, into) => {
|
|
1314
|
+
const source = ctx.createBufferSource();
|
|
1315
|
+
source.buffer = noiseBuffer(ctx, noise.noise);
|
|
1316
|
+
source.loop = true;
|
|
1317
|
+
source.connect(into);
|
|
1318
|
+
source.start(t0, Math.random() * source.buffer.duration);
|
|
1319
|
+
return {
|
|
1320
|
+
nodes: [source],
|
|
1321
|
+
retune: () => {
|
|
1322
|
+
source.playbackRate.value = voice.rate;
|
|
1323
|
+
source.detune.value = voice.detune;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
})
|
|
1327
|
+
});
|
|
1328
|
+
},
|
|
1329
|
+
stream(media, streamOpts = {}) {
|
|
1330
|
+
const el = typeof media === "string" ? openMedia(media, streamOpts.crossOrigin) : media;
|
|
1331
|
+
streaming.get(el)?.stop();
|
|
1332
|
+
const handle = launch(streamOpts, {
|
|
1333
|
+
ready: () => true,
|
|
1334
|
+
start(voice) {
|
|
1335
|
+
let node = mediaSources.get(el);
|
|
1336
|
+
if (!node) {
|
|
1337
|
+
node = ctx.createMediaElementSource(el);
|
|
1338
|
+
mediaSources.set(el, node);
|
|
1339
|
+
}
|
|
1340
|
+
el.loop = streamOpts.loop ?? false;
|
|
1341
|
+
if (streamOpts.offset !== void 0) el.currentTime = streamOpts.offset / 1e3;
|
|
1342
|
+
voice.retune = () => {
|
|
1343
|
+
el.playbackRate = voice.rate;
|
|
1344
|
+
};
|
|
1345
|
+
voice.retune();
|
|
1346
|
+
const into = voice.chain.panner;
|
|
1347
|
+
node.connect(into);
|
|
1348
|
+
return mediaSource(el, node, into);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
streaming.set(el, handle);
|
|
1352
|
+
return handle;
|
|
1353
|
+
},
|
|
1354
|
+
schedule(when, fire, key) {
|
|
1355
|
+
if (disposed) return;
|
|
1356
|
+
scheduler.schedule(when, fire, key);
|
|
673
1357
|
},
|
|
674
1358
|
stopKey(key) {
|
|
675
1359
|
scheduler.cancelKey(key);
|
|
@@ -716,6 +1400,7 @@ function createAudioEngine(opts = {}) {
|
|
|
716
1400
|
taps.clear();
|
|
717
1401
|
disarmGestures();
|
|
718
1402
|
ctx.removeEventListener("statechange", onStateChange);
|
|
1403
|
+
graph.dispose();
|
|
719
1404
|
graph.master.disconnect();
|
|
720
1405
|
if (ownsContext) void ctx.close().catch(() => void 0);
|
|
721
1406
|
}
|
|
@@ -723,6 +1408,207 @@ function createAudioEngine(opts = {}) {
|
|
|
723
1408
|
return engine;
|
|
724
1409
|
}
|
|
725
1410
|
|
|
726
|
-
|
|
1411
|
+
// src/effects.ts
|
|
1412
|
+
function createFilterEffect(ctx, opts = {}) {
|
|
1413
|
+
const filter = ctx.createBiquadFilter();
|
|
1414
|
+
filter.type = opts.type ?? "lowpass";
|
|
1415
|
+
if (opts.frequency !== void 0) writeParam(ctx, filter.frequency, opts.frequency);
|
|
1416
|
+
if (opts.Q !== void 0) writeParam(ctx, filter.Q, opts.Q);
|
|
1417
|
+
if (opts.gain !== void 0) writeParam(ctx, filter.gain, opts.gain);
|
|
1418
|
+
return { input: filter, output: filter, filter };
|
|
1419
|
+
}
|
|
1420
|
+
var checkMix = (mix) => {
|
|
1421
|
+
if (!(mix >= 0 && mix <= 1)) throw new RangeError(`@weasel-js/audio: mix ${mix} is outside 0..1`);
|
|
1422
|
+
return mix;
|
|
1423
|
+
};
|
|
1424
|
+
function wetDry(ctx, wetIn, wetOut, initial) {
|
|
1425
|
+
const input = ctx.createGain();
|
|
1426
|
+
const output = ctx.createGain();
|
|
1427
|
+
const dry = ctx.createGain();
|
|
1428
|
+
const wet = ctx.createGain();
|
|
1429
|
+
input.connect(dry);
|
|
1430
|
+
dry.connect(output);
|
|
1431
|
+
input.connect(wetIn);
|
|
1432
|
+
wetOut.connect(wet);
|
|
1433
|
+
wet.connect(output);
|
|
1434
|
+
let mix = checkMix(initial);
|
|
1435
|
+
const write = (rampMs) => {
|
|
1436
|
+
writeParam(ctx, dry.gain, 1 - mix, rampMs);
|
|
1437
|
+
writeParam(ctx, wet.gain, mix, rampMs);
|
|
1438
|
+
};
|
|
1439
|
+
write();
|
|
1440
|
+
return {
|
|
1441
|
+
input,
|
|
1442
|
+
output,
|
|
1443
|
+
mix: () => mix,
|
|
1444
|
+
setMix(value, rampMs) {
|
|
1445
|
+
mix = checkMix(value);
|
|
1446
|
+
write(rampMs);
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function createReverbEffect(ctx, opts) {
|
|
1451
|
+
const convolver = ctx.createConvolver();
|
|
1452
|
+
convolver.normalize = opts.normalize ?? true;
|
|
1453
|
+
convolver.buffer = opts.impulse;
|
|
1454
|
+
const m = wetDry(ctx, convolver, convolver, opts.mix ?? 0.3);
|
|
1455
|
+
return { input: m.input, output: m.output, convolver, mix: m.mix, setMix: m.setMix };
|
|
1456
|
+
}
|
|
1457
|
+
var checkFeedback = (value) => {
|
|
1458
|
+
if (!(value > -1 && value < 1)) {
|
|
1459
|
+
throw new RangeError(`@weasel-js/audio: feedback ${value} must be inside -1..1, or the repeats grow`);
|
|
1460
|
+
}
|
|
1461
|
+
return value;
|
|
1462
|
+
};
|
|
1463
|
+
function createDelayEffect(ctx, opts = {}) {
|
|
1464
|
+
const time = opts.time ?? 250;
|
|
1465
|
+
const maxTime = opts.maxTime ?? Math.max(1e3, time);
|
|
1466
|
+
let fb = checkFeedback(opts.feedback ?? 0.35);
|
|
1467
|
+
const delay = ctx.createDelay(maxTime / 1e3);
|
|
1468
|
+
const loop = ctx.createGain();
|
|
1469
|
+
const m = wetDry(ctx, delay, delay, opts.mix ?? 0.35);
|
|
1470
|
+
delay.connect(loop);
|
|
1471
|
+
loop.connect(delay);
|
|
1472
|
+
writeParam(ctx, delay.delayTime, time / 1e3);
|
|
1473
|
+
writeParam(ctx, loop.gain, fb);
|
|
1474
|
+
return {
|
|
1475
|
+
input: m.input,
|
|
1476
|
+
output: m.output,
|
|
1477
|
+
delay,
|
|
1478
|
+
mix: m.mix,
|
|
1479
|
+
setMix: m.setMix,
|
|
1480
|
+
setTime(ms, rampMs) {
|
|
1481
|
+
if (!(ms >= 0 && ms <= maxTime)) {
|
|
1482
|
+
throw new RangeError(`@weasel-js/audio: delay time ${ms} is outside 0..${maxTime} ms`);
|
|
1483
|
+
}
|
|
1484
|
+
writeParam(ctx, delay.delayTime, ms / 1e3, rampMs);
|
|
1485
|
+
},
|
|
1486
|
+
feedback: () => fb,
|
|
1487
|
+
setFeedback(value, rampMs) {
|
|
1488
|
+
fb = checkFeedback(value);
|
|
1489
|
+
writeParam(ctx, loop.gain, fb, rampMs);
|
|
1490
|
+
},
|
|
1491
|
+
dispose() {
|
|
1492
|
+
delay.disconnect();
|
|
1493
|
+
loop.disconnect();
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function createCompressorEffect(ctx, opts = {}) {
|
|
1498
|
+
const compressor = ctx.createDynamicsCompressor();
|
|
1499
|
+
const makeup = ctx.createGain();
|
|
1500
|
+
compressor.connect(makeup);
|
|
1501
|
+
const set = (param, value) => {
|
|
1502
|
+
if (value !== void 0) writeParam(ctx, param, value);
|
|
1503
|
+
};
|
|
1504
|
+
set(compressor.threshold, opts.threshold);
|
|
1505
|
+
set(compressor.knee, opts.knee);
|
|
1506
|
+
set(compressor.ratio, opts.ratio);
|
|
1507
|
+
set(compressor.attack, opts.attack === void 0 ? void 0 : opts.attack / 1e3);
|
|
1508
|
+
set(compressor.release, opts.release === void 0 ? void 0 : opts.release / 1e3);
|
|
1509
|
+
set(makeup.gain, opts.makeup ?? 1);
|
|
1510
|
+
return { input: compressor, output: makeup, compressor, makeup };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/patternPlayer.ts
|
|
1514
|
+
var nextPlayer = 1;
|
|
1515
|
+
var checkTempo = (bpm) => {
|
|
1516
|
+
if (!(bpm > 0) || !Number.isFinite(bpm)) {
|
|
1517
|
+
throw new RangeError(`@weasel-js/audio: tempo must be a positive number of bpm, got ${bpm}`);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
function createPatternPlayer(engine, opts) {
|
|
1521
|
+
checkTempo(opts.tempo);
|
|
1522
|
+
const stepsPerBeat = opts.stepsPerBeat ?? 4;
|
|
1523
|
+
const loop = opts.loop ?? true;
|
|
1524
|
+
const key = `\0pattern:${nextPlayer++}`;
|
|
1525
|
+
let bpm = opts.tempo;
|
|
1526
|
+
let length = 1;
|
|
1527
|
+
let byStep = /* @__PURE__ */ new Map();
|
|
1528
|
+
const setEvents = (events, len) => {
|
|
1529
|
+
byStep = /* @__PURE__ */ new Map();
|
|
1530
|
+
let last = -1;
|
|
1531
|
+
for (const e of events) {
|
|
1532
|
+
const list = byStep.get(e.step);
|
|
1533
|
+
if (list) list.push(e);
|
|
1534
|
+
else byStep.set(e.step, [e]);
|
|
1535
|
+
last = Math.max(last, e.step);
|
|
1536
|
+
}
|
|
1537
|
+
length = len ?? Math.max(1, Math.ceil((last + 1) / stepsPerBeat)) * stepsPerBeat;
|
|
1538
|
+
};
|
|
1539
|
+
setEvents(opts.events ?? [], opts.length);
|
|
1540
|
+
let running = false;
|
|
1541
|
+
let count = 0;
|
|
1542
|
+
let lastAt = null;
|
|
1543
|
+
let nextAt = 0;
|
|
1544
|
+
const voices = /* @__PURE__ */ new Set();
|
|
1545
|
+
const stepMs = () => 6e4 / bpm / stepsPerBeat;
|
|
1546
|
+
const book = () => {
|
|
1547
|
+
engine.schedule(nextAt, fire, key);
|
|
1548
|
+
};
|
|
1549
|
+
const fire = (scheduled) => {
|
|
1550
|
+
if (!running) return;
|
|
1551
|
+
let when = scheduled;
|
|
1552
|
+
const behind = engine.now() - when;
|
|
1553
|
+
if (behind > stepMs()) {
|
|
1554
|
+
const skip = Math.ceil(behind / stepMs());
|
|
1555
|
+
count += skip;
|
|
1556
|
+
when += skip * stepMs();
|
|
1557
|
+
}
|
|
1558
|
+
if (!loop && count >= length) {
|
|
1559
|
+
running = false;
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const step = count % length;
|
|
1563
|
+
for (const v of voices) if (!v.isPlaying()) voices.delete(v);
|
|
1564
|
+
for (const e of byStep.get(step) ?? []) {
|
|
1565
|
+
if ("sound" in e) {
|
|
1566
|
+
const { step: _s, sound, ...play } = e;
|
|
1567
|
+
voices.add(engine.play(sound, { ...play, when }));
|
|
1568
|
+
} else if ("noise" in e) {
|
|
1569
|
+
const { step: _s, length: steps = 1, ...noise } = e;
|
|
1570
|
+
voices.add(engine.playNoise({ ...noise, when, duration: steps * stepMs() }));
|
|
1571
|
+
} else {
|
|
1572
|
+
const { step: _s, length: steps = 1, ...note } = e;
|
|
1573
|
+
voices.add(engine.playNote({ ...note, when, duration: steps * stepMs() }));
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
opts.onStep?.(step, when);
|
|
1577
|
+
count += 1;
|
|
1578
|
+
lastAt = when;
|
|
1579
|
+
nextAt = when + stepMs();
|
|
1580
|
+
book();
|
|
1581
|
+
};
|
|
1582
|
+
return {
|
|
1583
|
+
start(when) {
|
|
1584
|
+
if (running) return;
|
|
1585
|
+
running = true;
|
|
1586
|
+
count = 0;
|
|
1587
|
+
lastAt = null;
|
|
1588
|
+
nextAt = when ?? engine.now();
|
|
1589
|
+
book();
|
|
1590
|
+
},
|
|
1591
|
+
stop() {
|
|
1592
|
+
if (!running) return;
|
|
1593
|
+
running = false;
|
|
1594
|
+
engine.stopKey(key);
|
|
1595
|
+
for (const v of voices) v.release();
|
|
1596
|
+
voices.clear();
|
|
1597
|
+
},
|
|
1598
|
+
playing: () => running,
|
|
1599
|
+
tempo: () => bpm,
|
|
1600
|
+
setTempo(next) {
|
|
1601
|
+
checkTempo(next);
|
|
1602
|
+
bpm = next;
|
|
1603
|
+
if (!running || lastAt === null) return;
|
|
1604
|
+
engine.stopKey(key);
|
|
1605
|
+
nextAt = Math.max(lastAt + stepMs(), engine.now());
|
|
1606
|
+
book();
|
|
1607
|
+
},
|
|
1608
|
+
setEvents
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
export { createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, resolveEnvelope, spatialize, toFrequency };
|
|
727
1613
|
//# sourceMappingURL=index.js.map
|
|
728
1614
|
//# sourceMappingURL=index.js.map
|