@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.
- package/README.md +96 -53
- package/dist/bin.js +1212 -0
- package/dist/cc-DgCkkqq8.js +13 -0
- package/dist/cc-he3fHS3P.js +12 -0
- package/dist/index.d.ts +723 -3
- package/dist/index.js +1534 -126
- package/dist/react.d.ts +583 -0
- package/dist/react.js +1556 -0
- package/package.json +64 -39
- package/schemas/pack.schema.json +4 -0
- package/schemas/patch.schema.json +857 -0
- package/dist/codegen/theme-codegen.d.ts +0 -12
- package/dist/codegen/theme-codegen.d.ts.map +0 -1
- package/dist/codegen/theme-codegen.js +0 -153
- package/dist/codegen/theme-codegen.js.map +0 -1
- package/dist/commands/add.d.ts +0 -2
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/add.js +0 -120
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -2
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/diff.js +0 -103
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/generate.d.ts +0 -12
- package/dist/commands/generate.d.ts.map +0 -1
- package/dist/commands/generate.js +0 -96
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -79
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/list.d.ts +0 -14
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js +0 -93
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/remove.d.ts +0 -2
- package/dist/commands/remove.d.ts.map +0 -1
- package/dist/commands/remove.js +0 -71
- package/dist/commands/remove.js.map +0 -1
- package/dist/commands/theme.d.ts +0 -31
- package/dist/commands/theme.d.ts.map +0 -1
- package/dist/commands/theme.js +0 -142
- package/dist/commands/theme.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -123
- package/dist/commands/update.js.map +0 -1
- package/dist/core/alias-resolver.d.ts +0 -24
- package/dist/core/alias-resolver.d.ts.map +0 -1
- package/dist/core/alias-resolver.js +0 -87
- package/dist/core/alias-resolver.js.map +0 -1
- package/dist/core/config.d.ts +0 -21
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -43
- package/dist/core/config.js.map +0 -1
- package/dist/core/file-writer.d.ts +0 -14
- package/dist/core/file-writer.d.ts.map +0 -1
- package/dist/core/file-writer.js +0 -90
- package/dist/core/file-writer.js.map +0 -1
- package/dist/core/package-manager.d.ts +0 -3
- package/dist/core/package-manager.d.ts.map +0 -1
- package/dist/core/package-manager.js +0 -17
- package/dist/core/package-manager.js.map +0 -1
- package/dist/core/registry.d.ts +0 -18
- package/dist/core/registry.d.ts.map +0 -1
- package/dist/core/registry.js +0 -69
- package/dist/core/registry.js.map +0 -1
- package/dist/core/theme-manager.d.ts +0 -35
- package/dist/core/theme-manager.d.ts.map +0 -1
- package/dist/core/theme-manager.js +0 -94
- package/dist/core/theme-manager.js.map +0 -1
- package/dist/core/utils.d.ts +0 -22
- package/dist/core/utils.d.ts.map +0 -1
- package/dist/core/utils.js +0 -44
- package/dist/core/utils.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -116
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -43
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,126 +1,1534 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 };
|