@pinkpixel/haku 0.0.0-stage → 0.1.0
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 +147 -2
- package/dist/LICENSE +202 -0
- package/dist/cli.mjs +134 -0
- package/dist/fonts/Figtree-Regular.ttf +0 -0
- package/dist/fonts/Figtree-SemiBold.ttf +0 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/mcp-DDuNECfs.mjs +71 -0
- package/dist/node-CVxAljK8.mjs +24 -0
- package/dist/samples/ambience/rain_roof.ogg +0 -0
- package/dist/samples/ambience/rain_window.ogg +0 -0
- package/dist/samples/drums/brush_v0_r1.ogg +0 -0
- package/dist/samples/drums/brush_v0_r2.ogg +0 -0
- package/dist/samples/drums/brush_v1_r1.ogg +0 -0
- package/dist/samples/drums/brush_v1_r2.ogg +0 -0
- package/dist/samples/drums/brushhat_v0_r1.ogg +0 -0
- package/dist/samples/drums/brushhat_v0_r2.ogg +0 -0
- package/dist/samples/drums/brushhat_v1_r1.ogg +0 -0
- package/dist/samples/drums/brushhat_v1_r2.ogg +0 -0
- package/dist/samples/drums/hat_v0_r1.ogg +0 -0
- package/dist/samples/drums/hat_v0_r2.ogg +0 -0
- package/dist/samples/drums/hat_v1_r1.ogg +0 -0
- package/dist/samples/drums/hat_v1_r2.ogg +0 -0
- package/dist/samples/drums/kick_v0_r1.ogg +0 -0
- package/dist/samples/drums/kick_v0_r2.ogg +0 -0
- package/dist/samples/drums/kick_v1_r1.ogg +0 -0
- package/dist/samples/drums/kick_v1_r2.ogg +0 -0
- package/dist/samples/drums/openhat_v0_r1.ogg +0 -0
- package/dist/samples/drums/openhat_v0_r2.ogg +0 -0
- package/dist/samples/drums/openhat_v1_r1.ogg +0 -0
- package/dist/samples/drums/openhat_v1_r2.ogg +0 -0
- package/dist/samples/drums/ride_v0_r1.ogg +0 -0
- package/dist/samples/drums/ride_v0_r2.ogg +0 -0
- package/dist/samples/drums/ride_v1_r1.ogg +0 -0
- package/dist/samples/drums/ride_v1_r2.ogg +0 -0
- package/dist/samples/drums/rim_v0_r1.ogg +0 -0
- package/dist/samples/drums/rim_v0_r2.ogg +0 -0
- package/dist/samples/drums/rim_v1_r1.ogg +0 -0
- package/dist/samples/drums/rim_v1_r2.ogg +0 -0
- package/dist/samples/drums/snare_v0_r1.ogg +0 -0
- package/dist/samples/drums/snare_v0_r2.ogg +0 -0
- package/dist/samples/drums/snare_v1_r1.ogg +0 -0
- package/dist/samples/drums/snare_v1_r2.ogg +0 -0
- package/dist/samples/drums/tom_v0_r1.ogg +0 -0
- package/dist/samples/drums/tom_v0_r2.ogg +0 -0
- package/dist/samples/drums/tom_v1_r1.ogg +0 -0
- package/dist/samples/drums/tom_v1_r2.ogg +0 -0
- package/dist/samples/glock/v0_103.ogg +0 -0
- package/dist/samples/glock/v0_108.ogg +0 -0
- package/dist/samples/glock/v0_79.ogg +0 -0
- package/dist/samples/glock/v0_84.ogg +0 -0
- package/dist/samples/glock/v0_91.ogg +0 -0
- package/dist/samples/glock/v0_96.ogg +0 -0
- package/dist/samples/harp/v0_101.ogg +0 -0
- package/dist/samples/harp/v0_28.ogg +0 -0
- package/dist/samples/harp/v0_31.ogg +0 -0
- package/dist/samples/harp/v0_35.ogg +0 -0
- package/dist/samples/harp/v0_38.ogg +0 -0
- package/dist/samples/harp/v0_41.ogg +0 -0
- package/dist/samples/harp/v0_45.ogg +0 -0
- package/dist/samples/harp/v0_48.ogg +0 -0
- package/dist/samples/harp/v0_52.ogg +0 -0
- package/dist/samples/harp/v0_55.ogg +0 -0
- package/dist/samples/harp/v0_59.ogg +0 -0
- package/dist/samples/harp/v0_62.ogg +0 -0
- package/dist/samples/harp/v0_65.ogg +0 -0
- package/dist/samples/harp/v0_69.ogg +0 -0
- package/dist/samples/harp/v0_72.ogg +0 -0
- package/dist/samples/harp/v0_76.ogg +0 -0
- package/dist/samples/harp/v0_79.ogg +0 -0
- package/dist/samples/harp/v0_83.ogg +0 -0
- package/dist/samples/harp/v0_86.ogg +0 -0
- package/dist/samples/harp/v0_89.ogg +0 -0
- package/dist/samples/harp/v0_93.ogg +0 -0
- package/dist/samples/harp/v0_95.ogg +0 -0
- package/dist/samples/harp/v0_98.ogg +0 -0
- package/dist/samples/manifest.json +368 -0
- package/dist/samples/marimba/v0_41.ogg +0 -0
- package/dist/samples/marimba/v0_48.ogg +0 -0
- package/dist/samples/marimba/v0_55.ogg +0 -0
- package/dist/samples/marimba/v0_59.ogg +0 -0
- package/dist/samples/marimba/v0_65.ogg +0 -0
- package/dist/samples/marimba/v0_72.ogg +0 -0
- package/dist/samples/marimba/v0_79.ogg +0 -0
- package/dist/samples/marimba/v0_83.ogg +0 -0
- package/dist/samples/marimba/v0_89.ogg +0 -0
- package/dist/samples/marimba/v0_96.ogg +0 -0
- package/dist/samples/piano/v0_29.ogg +0 -0
- package/dist/samples/piano/v0_33.ogg +0 -0
- package/dist/samples/piano/v0_37.ogg +0 -0
- package/dist/samples/piano/v0_41.ogg +0 -0
- package/dist/samples/piano/v0_45.ogg +0 -0
- package/dist/samples/piano/v0_49.ogg +0 -0
- package/dist/samples/piano/v0_53.ogg +0 -0
- package/dist/samples/piano/v0_57.ogg +0 -0
- package/dist/samples/piano/v0_61.ogg +0 -0
- package/dist/samples/piano/v0_65.ogg +0 -0
- package/dist/samples/piano/v0_69.ogg +0 -0
- package/dist/samples/piano/v0_73.ogg +0 -0
- package/dist/samples/piano/v0_77.ogg +0 -0
- package/dist/samples/piano/v0_81.ogg +0 -0
- package/dist/samples/piano/v0_85.ogg +0 -0
- package/dist/samples/piano/v0_89.ogg +0 -0
- package/dist/samples/piano/v0_93.ogg +0 -0
- package/dist/samples/piano/v0_97.ogg +0 -0
- package/dist/samples/piano/v1_29.ogg +0 -0
- package/dist/samples/piano/v1_33.ogg +0 -0
- package/dist/samples/piano/v1_37.ogg +0 -0
- package/dist/samples/piano/v1_41.ogg +0 -0
- package/dist/samples/piano/v1_45.ogg +0 -0
- package/dist/samples/piano/v1_49.ogg +0 -0
- package/dist/samples/piano/v1_53.ogg +0 -0
- package/dist/samples/piano/v1_57.ogg +0 -0
- package/dist/samples/piano/v1_61.ogg +0 -0
- package/dist/samples/piano/v1_65.ogg +0 -0
- package/dist/samples/piano/v1_69.ogg +0 -0
- package/dist/samples/piano/v1_73.ogg +0 -0
- package/dist/samples/piano/v1_77.ogg +0 -0
- package/dist/samples/piano/v1_81.ogg +0 -0
- package/dist/samples/piano/v1_85.ogg +0 -0
- package/dist/samples/piano/v1_89.ogg +0 -0
- package/dist/samples/piano/v1_93.ogg +0 -0
- package/dist/samples/piano/v1_97.ogg +0 -0
- package/dist/samples/wurli/v0_33.ogg +0 -0
- package/dist/samples/wurli/v0_40.ogg +0 -0
- package/dist/samples/wurli/v0_45.ogg +0 -0
- package/dist/samples/wurli/v0_51.ogg +0 -0
- package/dist/samples/wurli/v0_56.ogg +0 -0
- package/dist/samples/wurli/v0_61.ogg +0 -0
- package/dist/samples/wurli/v0_66.ogg +0 -0
- package/dist/samples/wurli/v0_71.ogg +0 -0
- package/dist/samples/wurli/v0_73.ogg +0 -0
- package/dist/samples/wurli/v0_79.ogg +0 -0
- package/dist/samples/wurli/v0_85.ogg +0 -0
- package/dist/samples/wurli/v0_92.ogg +0 -0
- package/dist/samples/wurli/v1_33.ogg +0 -0
- package/dist/samples/wurli/v1_36.ogg +0 -0
- package/dist/samples/wurli/v1_41.ogg +0 -0
- package/dist/samples/wurli/v1_47.ogg +0 -0
- package/dist/samples/wurli/v1_52.ogg +0 -0
- package/dist/samples/wurli/v1_56.ogg +0 -0
- package/dist/samples/wurli/v1_61.ogg +0 -0
- package/dist/samples/wurli/v1_68.ogg +0 -0
- package/dist/samples/wurli/v1_73.ogg +0 -0
- package/dist/samples/wurli/v1_79.ogg +0 -0
- package/dist/samples/wurli/v1_85.ogg +0 -0
- package/dist/samples/wurli/v1_92.ogg +0 -0
- package/dist/src-D5fW7AC1.mjs +2313 -0
- package/dist/src-JrRbTOJC.mjs +701 -0
- package/dist/version-DoZ2etXG.mjs +1742 -0
- package/package.json +59 -3
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { S as DRUM_PIECE_IDS, T as INSTRUMENTS, i as buildSceneScore, p as createRng, r as buildArrangementScore } from "./src-D5fW7AC1.mjs";
|
|
2
|
+
import * as Tone from "tone";
|
|
3
|
+
// ../../packages/core/src/engine/types.ts
|
|
4
|
+
function beatsToSeconds(beats, bpm) {
|
|
5
|
+
return beats * 60 / bpm;
|
|
6
|
+
}
|
|
7
|
+
// ../../packages/core/src/samples.ts
|
|
8
|
+
function pickLayer(layers, velocity) {
|
|
9
|
+
return layers.find((l) => velocity <= l.maxVelocity) ?? layers[layers.length - 1];
|
|
10
|
+
}
|
|
11
|
+
// ../../packages/audio/src/bank.ts
|
|
12
|
+
function manifestFiles(manifest) {
|
|
13
|
+
const files = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const inst of Object.values(manifest.instruments)) for (const layer of inst?.layers ?? []) for (const f of Object.values(layer.zones)) files.add(f);
|
|
15
|
+
for (const layers of Object.values(manifest.drums.pieces)) for (const layer of layers ?? []) for (const f of layer.files) files.add(f);
|
|
16
|
+
for (const a of Object.values(manifest.ambience)) files.add(a.file);
|
|
17
|
+
return [...files];
|
|
18
|
+
}
|
|
19
|
+
async function loadSampleBank(manifest, decode, onProgress) {
|
|
20
|
+
const files = manifestFiles(manifest);
|
|
21
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
22
|
+
let loaded = 0;
|
|
23
|
+
await Promise.all(files.map(async (file) => {
|
|
24
|
+
const audio = await decode(file);
|
|
25
|
+
buffers.set(file, new Tone.ToneAudioBuffer(audio));
|
|
26
|
+
onProgress?.(++loaded, files.length);
|
|
27
|
+
}));
|
|
28
|
+
return {
|
|
29
|
+
manifest,
|
|
30
|
+
buffers
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function bankBuffer(bank, file) {
|
|
34
|
+
const buf = bank.buffers.get(file);
|
|
35
|
+
if (!buf) throw new Error(`Sample ${file} isn't loaded`);
|
|
36
|
+
return buf;
|
|
37
|
+
}
|
|
38
|
+
// ../../packages/audio/src/levels.ts
|
|
39
|
+
/** Per-instrument trim in dB, applied after the voice. Brings samplers and synths to a common level. */
|
|
40
|
+
const INSTRUMENT_TRIM_DB = {
|
|
41
|
+
piano: 0,
|
|
42
|
+
wurli: -9,
|
|
43
|
+
"fm-ep": -2,
|
|
44
|
+
glock: -9,
|
|
45
|
+
marimba: -2,
|
|
46
|
+
harp: -3,
|
|
47
|
+
pad: -11,
|
|
48
|
+
pluck: -6,
|
|
49
|
+
bass: -19,
|
|
50
|
+
drums: 0
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Extra gain per velocity layer, softest first. The VSCO piano's soft layer sits about 12 dB under
|
|
54
|
+
* its loud one, which is realistic but too much for lofi comping.
|
|
55
|
+
*/
|
|
56
|
+
const SAMPLE_LAYER_LIFT_DB = { piano: [12, 0] };
|
|
57
|
+
/** Sampled layers already carry their own dynamics, so velocity only shapes level a little within one. */
|
|
58
|
+
const sampleVelocityGain = (v) => .45 + .55 * v;
|
|
59
|
+
/** Per-piece trim in dB inside the drum kit. The kit was normalized as a whole, so this is balance only. */
|
|
60
|
+
const DRUM_TRIM_DB = {
|
|
61
|
+
kick: 0,
|
|
62
|
+
snare: -3,
|
|
63
|
+
brush: 8,
|
|
64
|
+
rim: -4,
|
|
65
|
+
hat: -9,
|
|
66
|
+
brushhat: 2,
|
|
67
|
+
openhat: -12,
|
|
68
|
+
ride: -10,
|
|
69
|
+
tom: -4
|
|
70
|
+
};
|
|
71
|
+
/** Reverb return level at reverb = 1. */
|
|
72
|
+
const REVERB_RETURN = .55;
|
|
73
|
+
/** Faders (0 to 1) use a squared curve, which feels more even than linear. */
|
|
74
|
+
function faderToGain(v) {
|
|
75
|
+
return Math.max(0, Math.min(1, v)) ** 2;
|
|
76
|
+
}
|
|
77
|
+
const dbToGain = (db) => 10 ** (db / 20);
|
|
78
|
+
// ../../packages/audio/src/instruments.ts
|
|
79
|
+
/** Brush size sets brightness: 0 is a dark ~1 kHz low-pass, 1 is wide open. */
|
|
80
|
+
const brightnessHz = (b) => 1e3 * 2 ** (Math.max(0, Math.min(1, b)) * 4);
|
|
81
|
+
/** Wraps a playable thing with a brightness filter and the instrument trim. */
|
|
82
|
+
function filteredVoice(instrument, pick, nodes, filter) {
|
|
83
|
+
const output = new Tone.Gain(dbToGain(INSTRUMENT_TRIM_DB[instrument]));
|
|
84
|
+
filter.connect(output);
|
|
85
|
+
return {
|
|
86
|
+
output,
|
|
87
|
+
play(e, time, seconds) {
|
|
88
|
+
filter.frequency.setValueAtTime(brightnessHz(e.brightness), time);
|
|
89
|
+
pick(e).triggerAttackRelease(Tone.Frequency(e.midi, "midi").toFrequency(), seconds, time, e.velocity);
|
|
90
|
+
},
|
|
91
|
+
dispose() {
|
|
92
|
+
for (const n of nodes) n.dispose();
|
|
93
|
+
filter.dispose();
|
|
94
|
+
output.dispose();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function sampledVoice(instrument, bank) {
|
|
99
|
+
const def = bank.manifest.instruments[instrument];
|
|
100
|
+
if (!def) throw new Error(`No samples for ${instrument}`);
|
|
101
|
+
const filter = new Tone.Filter({
|
|
102
|
+
type: "lowpass",
|
|
103
|
+
frequency: 12e3,
|
|
104
|
+
rolloff: -12
|
|
105
|
+
});
|
|
106
|
+
const lifts = SAMPLE_LAYER_LIFT_DB[instrument] ?? [];
|
|
107
|
+
const nodes = [];
|
|
108
|
+
const layers = def.layers.map((layer, i) => {
|
|
109
|
+
const urls = {};
|
|
110
|
+
for (const [midi, file] of Object.entries(layer.zones)) urls[Number(midi)] = bankBuffer(bank, file);
|
|
111
|
+
const lift = new Tone.Gain(dbToGain(lifts[i] ?? 0)).connect(filter);
|
|
112
|
+
const sampler = new Tone.Sampler({
|
|
113
|
+
urls,
|
|
114
|
+
release: def.release
|
|
115
|
+
}).connect(lift);
|
|
116
|
+
nodes.push(sampler, lift);
|
|
117
|
+
return {
|
|
118
|
+
maxVelocity: layer.maxVelocity,
|
|
119
|
+
sampler
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
const play = (e) => {
|
|
123
|
+
const { sampler } = pickLayer(layers, e.velocity);
|
|
124
|
+
return {
|
|
125
|
+
triggerAttackRelease: (note, dur, time, velocity) => sampler.triggerAttackRelease(note, dur, time, sampleVelocityGain(velocity)),
|
|
126
|
+
dispose() {}
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
return filteredVoice(instrument, play, nodes, filter);
|
|
130
|
+
}
|
|
131
|
+
function synthVoice(instrument) {
|
|
132
|
+
const filter = new Tone.Filter({
|
|
133
|
+
type: "lowpass",
|
|
134
|
+
frequency: 8e3,
|
|
135
|
+
rolloff: -12
|
|
136
|
+
});
|
|
137
|
+
let synth;
|
|
138
|
+
switch (instrument) {
|
|
139
|
+
case "fm-ep":
|
|
140
|
+
synth = new Tone.PolySynth(Tone.FMSynth, {
|
|
141
|
+
harmonicity: 1,
|
|
142
|
+
modulationIndex: 3.5,
|
|
143
|
+
oscillator: { type: "sine" },
|
|
144
|
+
modulation: { type: "sine" },
|
|
145
|
+
envelope: {
|
|
146
|
+
attack: .002,
|
|
147
|
+
decay: 1.8,
|
|
148
|
+
sustain: .18,
|
|
149
|
+
release: .9
|
|
150
|
+
},
|
|
151
|
+
modulationEnvelope: {
|
|
152
|
+
attack: .002,
|
|
153
|
+
decay: .45,
|
|
154
|
+
sustain: .08,
|
|
155
|
+
release: .6
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
case "pad":
|
|
160
|
+
synth = new Tone.PolySynth(Tone.Synth, {
|
|
161
|
+
oscillator: {
|
|
162
|
+
type: "fatsawtooth",
|
|
163
|
+
count: 3,
|
|
164
|
+
spread: 16
|
|
165
|
+
},
|
|
166
|
+
envelope: {
|
|
167
|
+
attack: .9,
|
|
168
|
+
decay: .6,
|
|
169
|
+
sustain: .8,
|
|
170
|
+
release: 2.4
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
filter.Q.value = .5;
|
|
174
|
+
break;
|
|
175
|
+
case "pluck":
|
|
176
|
+
synth = new Tone.PolySynth(Tone.Synth, {
|
|
177
|
+
oscillator: { type: "triangle" },
|
|
178
|
+
envelope: {
|
|
179
|
+
attack: .003,
|
|
180
|
+
decay: .4,
|
|
181
|
+
sustain: 0,
|
|
182
|
+
release: .35
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
case "bass":
|
|
187
|
+
synth = new Tone.PolySynth(Tone.MonoSynth, {
|
|
188
|
+
oscillator: { type: "triangle" },
|
|
189
|
+
envelope: {
|
|
190
|
+
attack: .008,
|
|
191
|
+
decay: .35,
|
|
192
|
+
sustain: .65,
|
|
193
|
+
release: .35
|
|
194
|
+
},
|
|
195
|
+
filter: {
|
|
196
|
+
Q: 1,
|
|
197
|
+
type: "lowpass",
|
|
198
|
+
rolloff: -24
|
|
199
|
+
},
|
|
200
|
+
filterEnvelope: {
|
|
201
|
+
attack: .005,
|
|
202
|
+
decay: .25,
|
|
203
|
+
sustain: .35,
|
|
204
|
+
release: .4,
|
|
205
|
+
baseFrequency: 110,
|
|
206
|
+
octaves: 2.6
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
synth.maxPolyphony = 4;
|
|
210
|
+
break;
|
|
211
|
+
default: throw new Error(`${instrument} isn't a synth`);
|
|
212
|
+
}
|
|
213
|
+
synth.connect(filter);
|
|
214
|
+
return filteredVoice(instrument, () => synth, [synth], filter);
|
|
215
|
+
}
|
|
216
|
+
function drumVoice(bank) {
|
|
217
|
+
const output = new Tone.Gain(dbToGain(INSTRUMENT_TRIM_DB.drums));
|
|
218
|
+
const pieces = new Map(DRUM_PIECE_IDS.map((piece) => [piece, new Tone.Gain(dbToGain(DRUM_TRIM_DB[piece])).connect(output)]));
|
|
219
|
+
return {
|
|
220
|
+
output,
|
|
221
|
+
play(e, time) {
|
|
222
|
+
const piece = e.drum;
|
|
223
|
+
const layers = piece && bank.manifest.drums.pieces[piece];
|
|
224
|
+
if (!piece || !layers?.length) return;
|
|
225
|
+
const files = pickLayer(layers, e.velocity).files;
|
|
226
|
+
const file = files[Math.floor(e.time * 4 + .01) % files.length];
|
|
227
|
+
const src = new Tone.ToneBufferSource({
|
|
228
|
+
url: bankBuffer(bank, file),
|
|
229
|
+
context: output.context
|
|
230
|
+
}).connect(pieces.get(piece));
|
|
231
|
+
src.onended = () => src.dispose();
|
|
232
|
+
src.start(time, 0, void 0, e.velocity ** 1.2);
|
|
233
|
+
},
|
|
234
|
+
dispose() {
|
|
235
|
+
for (const g of pieces.values()) g.dispose();
|
|
236
|
+
output.dispose();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function createVoice(instrument, bank) {
|
|
241
|
+
if (INSTRUMENTS[instrument].kind === "drums") return drumVoice(bank);
|
|
242
|
+
if (bank.manifest.instruments[instrument]) return sampledVoice(instrument, bank);
|
|
243
|
+
return synthVoice(instrument);
|
|
244
|
+
}
|
|
245
|
+
// ../../packages/audio/src/master.ts
|
|
246
|
+
/** Seconds of generated vinyl crackle. Long enough that the loop isn't noticeable. */
|
|
247
|
+
const CRACKLE_SECONDS = 7;
|
|
248
|
+
function crackleBuffer(sampleRate) {
|
|
249
|
+
const rand = createRng(7);
|
|
250
|
+
const data = new Float32Array(Math.floor(sampleRate * CRACKLE_SECONDS));
|
|
251
|
+
let i = 0;
|
|
252
|
+
while (i < data.length) {
|
|
253
|
+
i += Math.floor(-Math.log(1 - rand()) * sampleRate / 38);
|
|
254
|
+
const pop = rand() < .04;
|
|
255
|
+
const amp = (pop ? .6 + rand() * .4 : .05 + rand() ** 3 * .35) * (rand() < .5 ? -1 : 1);
|
|
256
|
+
const len = pop ? 24 + Math.floor(rand() * 40) : 2 + Math.floor(rand() * 6);
|
|
257
|
+
for (let k = 0; k < len && i + k < data.length; k++) data[i + k] += amp * Math.exp(-k / (len / 3));
|
|
258
|
+
}
|
|
259
|
+
let prevIn = 0;
|
|
260
|
+
let prevOut = 0;
|
|
261
|
+
for (let k = 0; k < data.length; k++) {
|
|
262
|
+
const x = data[k];
|
|
263
|
+
prevOut = .97 * (prevOut + x - prevIn);
|
|
264
|
+
prevIn = x;
|
|
265
|
+
data[k] = prevOut;
|
|
266
|
+
}
|
|
267
|
+
return Tone.ToneAudioBuffer.fromArray(data);
|
|
268
|
+
}
|
|
269
|
+
/** tanh curve, normalized so full scale in stays near full scale out. */
|
|
270
|
+
function tapeCurve(drive = 2.2) {
|
|
271
|
+
const n = 2048;
|
|
272
|
+
const curve = new Float32Array(n);
|
|
273
|
+
const norm = Math.tanh(drive);
|
|
274
|
+
for (let i = 0; i < n; i++) {
|
|
275
|
+
const x = i / 2047 * 2 - 1;
|
|
276
|
+
curve[i] = Math.tanh(x * drive) / norm;
|
|
277
|
+
}
|
|
278
|
+
return curve;
|
|
279
|
+
}
|
|
280
|
+
var MasterChain = class {
|
|
281
|
+
input = new Tone.Gain(1);
|
|
282
|
+
ready;
|
|
283
|
+
drive = new Tone.Gain(1);
|
|
284
|
+
saturate = new Tone.WaveShaper(tapeCurve(), 2048);
|
|
285
|
+
trim = new Tone.Gain(1);
|
|
286
|
+
warmth = new Tone.Filter({
|
|
287
|
+
type: "lowpass",
|
|
288
|
+
frequency: 16e3,
|
|
289
|
+
rolloff: -12,
|
|
290
|
+
Q: .4
|
|
291
|
+
});
|
|
292
|
+
wobble = new Tone.Vibrato({
|
|
293
|
+
frequency: .55,
|
|
294
|
+
depth: 0,
|
|
295
|
+
maxDelay: .006,
|
|
296
|
+
type: "sine"
|
|
297
|
+
});
|
|
298
|
+
reverb = new Tone.Reverb({
|
|
299
|
+
decay: 3.2,
|
|
300
|
+
preDelay: .02,
|
|
301
|
+
wet: 1
|
|
302
|
+
});
|
|
303
|
+
reverbReturn = new Tone.Gain(0);
|
|
304
|
+
bus = new Tone.Gain(1);
|
|
305
|
+
highpass = new Tone.Filter({
|
|
306
|
+
type: "highpass",
|
|
307
|
+
frequency: 32,
|
|
308
|
+
rolloff: -12
|
|
309
|
+
});
|
|
310
|
+
comp = new Tone.Compressor({
|
|
311
|
+
threshold: -20,
|
|
312
|
+
ratio: 3,
|
|
313
|
+
attack: .015,
|
|
314
|
+
release: .25,
|
|
315
|
+
knee: 8
|
|
316
|
+
});
|
|
317
|
+
makeup = new Tone.Gain(dbToGain(6));
|
|
318
|
+
fader = new Tone.Gain(1);
|
|
319
|
+
limiter = new Tone.Limiter(-1);
|
|
320
|
+
crackle;
|
|
321
|
+
crackleGain = new Tone.Gain(0);
|
|
322
|
+
hiss = new Tone.Noise("pink");
|
|
323
|
+
hissGain = new Tone.Gain(0);
|
|
324
|
+
hissFilter = new Tone.Filter({
|
|
325
|
+
type: "bandpass",
|
|
326
|
+
frequency: 5e3,
|
|
327
|
+
Q: .4
|
|
328
|
+
});
|
|
329
|
+
rain = new Tone.Player({
|
|
330
|
+
loop: true,
|
|
331
|
+
fadeIn: 1.5,
|
|
332
|
+
fadeOut: .5
|
|
333
|
+
});
|
|
334
|
+
rainGain = new Tone.Gain(0);
|
|
335
|
+
rainFile = null;
|
|
336
|
+
bank;
|
|
337
|
+
constructor(bank) {
|
|
338
|
+
this.bank = bank;
|
|
339
|
+
this.input.chain(this.drive, this.saturate, this.trim, this.warmth, this.wobble);
|
|
340
|
+
this.wobble.connect(this.bus);
|
|
341
|
+
this.wobble.chain(this.reverb, this.reverbReturn, this.bus);
|
|
342
|
+
this.bus.chain(this.highpass, this.comp, this.makeup, this.fader, this.limiter, Tone.getDestination());
|
|
343
|
+
this.crackle = new Tone.Player({
|
|
344
|
+
url: crackleBuffer(Tone.getContext().sampleRate),
|
|
345
|
+
loop: true
|
|
346
|
+
});
|
|
347
|
+
this.crackle.chain(this.crackleGain, this.fader);
|
|
348
|
+
this.hiss.chain(this.hissFilter, this.hissGain, this.fader);
|
|
349
|
+
this.rain.chain(this.rainGain, this.fader);
|
|
350
|
+
this.ready = this.reverb.ready;
|
|
351
|
+
}
|
|
352
|
+
apply(mix) {
|
|
353
|
+
const w = mix.warmth;
|
|
354
|
+
this.drive.gain.value = dbToGain(w * 10);
|
|
355
|
+
this.trim.gain.value = dbToGain(-w * 8);
|
|
356
|
+
this.warmth.frequency.value = 16e3 * (2600 / 16e3) ** w;
|
|
357
|
+
this.wobble.depth.value = mix.wobble * .35;
|
|
358
|
+
this.wobble.frequency.value = .4 + mix.wobble * .5;
|
|
359
|
+
this.reverbReturn.gain.value = mix.reverb * REVERB_RETURN;
|
|
360
|
+
this.fader.gain.value = faderToGain(mix.master) * 1.56;
|
|
361
|
+
this.crackleGain.gain.value = mix.crackle > 0 ? dbToGain(-12) * mix.crackle ** 1.5 : 0;
|
|
362
|
+
this.hissGain.gain.value = mix.crackle > 0 ? dbToGain(-46) * mix.crackle : 0;
|
|
363
|
+
const file = this.bank.manifest.ambience[mix.rain.kind === "roof" ? "rain_roof" : "rain_window"].file;
|
|
364
|
+
if (file !== this.rainFile) {
|
|
365
|
+
this.rainFile = file;
|
|
366
|
+
this.rain.buffer = bankBuffer(this.bank, file);
|
|
367
|
+
}
|
|
368
|
+
this.rainGain.gain.value = mix.rain.enabled ? dbToGain(12) * faderToGain(mix.rain.level) : 0;
|
|
369
|
+
}
|
|
370
|
+
/** Starts crackle, hiss, and rain. They loop on their own clock, not the transport. */
|
|
371
|
+
startAmbience(time, rainOffset = 0) {
|
|
372
|
+
if (this.crackle.state !== "started") this.crackle.start(time);
|
|
373
|
+
if (this.hiss.state !== "started") this.hiss.start(time);
|
|
374
|
+
if (this.rain.state !== "started" && this.rain.loaded) this.rain.start(time, rainOffset);
|
|
375
|
+
}
|
|
376
|
+
stopAmbience(time) {
|
|
377
|
+
this.crackle.stop(time);
|
|
378
|
+
this.hiss.stop(time);
|
|
379
|
+
this.rain.stop(time);
|
|
380
|
+
}
|
|
381
|
+
dispose() {
|
|
382
|
+
for (const n of [
|
|
383
|
+
this.input,
|
|
384
|
+
this.drive,
|
|
385
|
+
this.saturate,
|
|
386
|
+
this.trim,
|
|
387
|
+
this.warmth,
|
|
388
|
+
this.wobble,
|
|
389
|
+
this.reverb,
|
|
390
|
+
this.reverbReturn,
|
|
391
|
+
this.bus,
|
|
392
|
+
this.highpass,
|
|
393
|
+
this.comp,
|
|
394
|
+
this.makeup,
|
|
395
|
+
this.fader,
|
|
396
|
+
this.limiter,
|
|
397
|
+
this.crackle,
|
|
398
|
+
this.crackleGain,
|
|
399
|
+
this.hiss,
|
|
400
|
+
this.hissGain,
|
|
401
|
+
this.hissFilter,
|
|
402
|
+
this.rain,
|
|
403
|
+
this.rainGain
|
|
404
|
+
]) n.dispose();
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
// ../../packages/audio/src/rig.ts
|
|
408
|
+
const BACKING = [
|
|
409
|
+
"chords",
|
|
410
|
+
"bass",
|
|
411
|
+
"drums"
|
|
412
|
+
];
|
|
413
|
+
/**
|
|
414
|
+
* Backing tracks are per scene, so each scene's band volume holds in a full-song render.
|
|
415
|
+
* Drawn layers already have unique ids.
|
|
416
|
+
*/
|
|
417
|
+
function trackKey(e, score) {
|
|
418
|
+
if (!e.track.startsWith("backing:")) return e.track;
|
|
419
|
+
const section = score.sections.findLast((s) => e.time >= s.start - .5) ?? score.sections[0];
|
|
420
|
+
return `${e.track}@${section?.sceneId ?? ""}`;
|
|
421
|
+
}
|
|
422
|
+
function trackSettings(project) {
|
|
423
|
+
const out = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const scene of project.scenes) {
|
|
425
|
+
for (const layer of scene.layers) out.set(layer.id, {
|
|
426
|
+
volume: layer.volume,
|
|
427
|
+
pan: layer.pan
|
|
428
|
+
});
|
|
429
|
+
for (const part of BACKING) out.set(`backing:${part}@${scene.id}`, {
|
|
430
|
+
volume: scene.backing[part].volume,
|
|
431
|
+
pan: 0
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
var Rig = class {
|
|
437
|
+
master;
|
|
438
|
+
ready;
|
|
439
|
+
bank;
|
|
440
|
+
tracks = /* @__PURE__ */ new Map();
|
|
441
|
+
settings = /* @__PURE__ */ new Map();
|
|
442
|
+
part = null;
|
|
443
|
+
constructor(bank) {
|
|
444
|
+
this.bank = bank;
|
|
445
|
+
this.master = new MasterChain(bank);
|
|
446
|
+
this.ready = this.master.ready;
|
|
447
|
+
}
|
|
448
|
+
/** Applies volumes, pans, and the mix. Cheap, so the UI can call it on every change. */
|
|
449
|
+
sync(project) {
|
|
450
|
+
this.settings = trackSettings(project);
|
|
451
|
+
this.master.apply(project.mix);
|
|
452
|
+
for (const [key, track] of this.tracks) {
|
|
453
|
+
const s = this.settings.get(key);
|
|
454
|
+
if (!s) {
|
|
455
|
+
this.disposeTrack(key, track);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
this.applySettings(track, s);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
applySettings(track, s) {
|
|
462
|
+
const gain = faderToGain(s.volume) * 1.56;
|
|
463
|
+
track.channel.volume.value = gain > 0 ? Tone.gainToDb(gain) : -Infinity;
|
|
464
|
+
track.channel.pan.value = s.pan;
|
|
465
|
+
}
|
|
466
|
+
voice(key, instrument) {
|
|
467
|
+
let track = this.tracks.get(key);
|
|
468
|
+
if (!track) {
|
|
469
|
+
track = {
|
|
470
|
+
channel: new Tone.Channel().connect(this.master.input),
|
|
471
|
+
voices: /* @__PURE__ */ new Map()
|
|
472
|
+
};
|
|
473
|
+
this.applySettings(track, this.settings.get(key) ?? {
|
|
474
|
+
volume: .8,
|
|
475
|
+
pan: 0
|
|
476
|
+
});
|
|
477
|
+
this.tracks.set(key, track);
|
|
478
|
+
}
|
|
479
|
+
let voice = track.voices.get(instrument);
|
|
480
|
+
if (!voice) {
|
|
481
|
+
voice = createVoice(instrument, this.bank);
|
|
482
|
+
voice.output.connect(track.channel);
|
|
483
|
+
track.voices.set(instrument, voice);
|
|
484
|
+
}
|
|
485
|
+
return voice;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Schedules a score on the current context's transport, replacing whatever was scheduled before.
|
|
489
|
+
* Voices are created up front so nothing gets built in the middle of playback.
|
|
490
|
+
*/
|
|
491
|
+
schedule(score) {
|
|
492
|
+
this.part?.dispose();
|
|
493
|
+
const events = score.events.map((e) => {
|
|
494
|
+
const key = trackKey(e, score);
|
|
495
|
+
this.voice(key, e.instrument);
|
|
496
|
+
return {
|
|
497
|
+
time: Math.max(0, beatsToSeconds(e.time, score.bpm)),
|
|
498
|
+
e,
|
|
499
|
+
key,
|
|
500
|
+
seconds: Math.max(.03, beatsToSeconds(e.duration, score.bpm))
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
this.part = new Tone.Part((time, ev) => this.voice(ev.key, ev.e.instrument).play(ev.e, time, ev.seconds), events);
|
|
504
|
+
this.part.start(0);
|
|
505
|
+
}
|
|
506
|
+
clear() {
|
|
507
|
+
this.part?.dispose();
|
|
508
|
+
this.part = null;
|
|
509
|
+
}
|
|
510
|
+
disposeTrack(key, track) {
|
|
511
|
+
for (const v of track.voices.values()) v.dispose();
|
|
512
|
+
track.channel.dispose();
|
|
513
|
+
this.tracks.delete(key);
|
|
514
|
+
}
|
|
515
|
+
dispose() {
|
|
516
|
+
this.clear();
|
|
517
|
+
for (const [key, track] of this.tracks) this.disposeTrack(key, track);
|
|
518
|
+
this.master.dispose();
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
// ../../packages/audio/src/render.ts
|
|
522
|
+
/** Humanize can nudge the first hit slightly before zero, so the transport starts a hair late. */
|
|
523
|
+
const LEAD_IN = .05;
|
|
524
|
+
function loopScore(score, loops) {
|
|
525
|
+
if (loops <= 1) return score;
|
|
526
|
+
const out = {
|
|
527
|
+
...score,
|
|
528
|
+
beats: score.beats * loops,
|
|
529
|
+
events: [],
|
|
530
|
+
chords: [],
|
|
531
|
+
sections: []
|
|
532
|
+
};
|
|
533
|
+
for (let i = 0; i < loops; i++) {
|
|
534
|
+
const at = i * score.beats;
|
|
535
|
+
out.events.push(...score.events.map((e) => ({
|
|
536
|
+
...e,
|
|
537
|
+
time: e.time + at
|
|
538
|
+
})));
|
|
539
|
+
out.chords.push(...score.chords.map((c) => ({
|
|
540
|
+
...c,
|
|
541
|
+
time: c.time + at
|
|
542
|
+
})));
|
|
543
|
+
out.sections.push(...score.sections.map((s) => ({
|
|
544
|
+
...s,
|
|
545
|
+
start: s.start + at
|
|
546
|
+
})));
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
function scoreFor(project, opts = {}) {
|
|
551
|
+
return opts.scope === "song" ? buildArrangementScore(project) : loopScore(buildSceneScore(project, opts.sceneId), opts.loops ?? 1);
|
|
552
|
+
}
|
|
553
|
+
async function renderProject(project, bank, opts = {}) {
|
|
554
|
+
const score = scoreFor(project, opts);
|
|
555
|
+
const sampleRate = opts.sampleRate ?? 44100;
|
|
556
|
+
const seconds = LEAD_IN + beatsToSeconds(score.beats, score.bpm) + (opts.tail ?? 3);
|
|
557
|
+
const buffer = (await Tone.Offline(async (ctx) => {
|
|
558
|
+
const rig = new Rig(bank);
|
|
559
|
+
rig.sync(project);
|
|
560
|
+
await rig.ready;
|
|
561
|
+
ctx.transport.bpm.value = score.bpm;
|
|
562
|
+
rig.schedule(score);
|
|
563
|
+
rig.master.startAmbience(0);
|
|
564
|
+
ctx.transport.start(LEAD_IN);
|
|
565
|
+
}, seconds, 2, sampleRate)).get();
|
|
566
|
+
if (!buffer) throw new Error("Render came back empty");
|
|
567
|
+
const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
|
|
568
|
+
return {
|
|
569
|
+
sampleRate: buffer.sampleRate,
|
|
570
|
+
channels
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/** Peak and RMS in dBFS, for checking the mix. */
|
|
574
|
+
function measure({ channels }) {
|
|
575
|
+
let peak = 0;
|
|
576
|
+
let sum = 0;
|
|
577
|
+
let n = 0;
|
|
578
|
+
for (const ch of channels) for (let i = 0; i < ch.length; i++) {
|
|
579
|
+
const v = ch[i];
|
|
580
|
+
if (!Number.isFinite(v)) return {
|
|
581
|
+
peakDb: NaN,
|
|
582
|
+
rmsDb: NaN
|
|
583
|
+
};
|
|
584
|
+
peak = Math.max(peak, Math.abs(v));
|
|
585
|
+
sum += v * v;
|
|
586
|
+
n++;
|
|
587
|
+
}
|
|
588
|
+
const db = (x) => x > 0 ? 20 * Math.log10(x) : -Infinity;
|
|
589
|
+
return {
|
|
590
|
+
peakDb: db(peak),
|
|
591
|
+
rmsDb: db(Math.sqrt(sum / Math.max(1, n)))
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// ../../packages/audio/src/wav.ts
|
|
595
|
+
function encodeWav({ sampleRate, channels }) {
|
|
596
|
+
const numCh = channels.length;
|
|
597
|
+
const frames = channels[0]?.length ?? 0;
|
|
598
|
+
const dataBytes = frames * numCh * 2;
|
|
599
|
+
const out = new Uint8Array(44 + dataBytes);
|
|
600
|
+
const view = new DataView(out.buffer);
|
|
601
|
+
const text = (at, s) => {
|
|
602
|
+
for (let i = 0; i < s.length; i++) view.setUint8(at + i, s.charCodeAt(i));
|
|
603
|
+
};
|
|
604
|
+
text(0, "RIFF");
|
|
605
|
+
view.setUint32(4, 36 + dataBytes, true);
|
|
606
|
+
text(8, "WAVE");
|
|
607
|
+
text(12, "fmt ");
|
|
608
|
+
view.setUint32(16, 16, true);
|
|
609
|
+
view.setUint16(20, 1, true);
|
|
610
|
+
view.setUint16(22, numCh, true);
|
|
611
|
+
view.setUint32(24, sampleRate, true);
|
|
612
|
+
view.setUint32(28, sampleRate * numCh * 2, true);
|
|
613
|
+
view.setUint16(32, numCh * 2, true);
|
|
614
|
+
view.setUint16(34, 16, true);
|
|
615
|
+
text(36, "data");
|
|
616
|
+
view.setUint32(40, dataBytes, true);
|
|
617
|
+
let at = 44;
|
|
618
|
+
for (let i = 0; i < frames; i++) for (let c = 0; c < numCh; c++) {
|
|
619
|
+
const dither = (Math.random() - Math.random()) / 32768;
|
|
620
|
+
const s = Math.max(-1, Math.min(1, channels[c][i] + dither));
|
|
621
|
+
view.setInt16(at, Math.round(s * 32767), true);
|
|
622
|
+
at += 2;
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
// ../../packages/audio/src/loudness.ts
|
|
627
|
+
/**
|
|
628
|
+
* The two K-weighting stages for any sample rate (the spec only lists 48 kHz). This is the bilinear
|
|
629
|
+
* form libebur128 uses; at 48 kHz it reproduces the spec's coefficients exactly.
|
|
630
|
+
*/
|
|
631
|
+
function kWeighting(fs) {
|
|
632
|
+
return [(() => {
|
|
633
|
+
const gain = 3.999843853973347;
|
|
634
|
+
const q = .7071752369554196;
|
|
635
|
+
const k = Math.tan(Math.PI * 1681.974450955533 / fs);
|
|
636
|
+
const vh = 10 ** (gain / 20);
|
|
637
|
+
const vb = vh ** .4996667741545416;
|
|
638
|
+
const a0 = 1 + k / q + k * k;
|
|
639
|
+
return {
|
|
640
|
+
b0: (vh + vb * k / q + k * k) / a0,
|
|
641
|
+
b1: 2 * (k * k - vh) / a0,
|
|
642
|
+
b2: (vh - vb * k / q + k * k) / a0,
|
|
643
|
+
a1: 2 * (k * k - 1) / a0,
|
|
644
|
+
a2: (1 - k / q + k * k) / a0
|
|
645
|
+
};
|
|
646
|
+
})(), (() => {
|
|
647
|
+
const q = .5003270373238773;
|
|
648
|
+
const k = Math.tan(Math.PI * 38.13547087602444 / fs);
|
|
649
|
+
const a0 = 1 + k / q + k * k;
|
|
650
|
+
return {
|
|
651
|
+
b0: 1,
|
|
652
|
+
b1: -2,
|
|
653
|
+
b2: 1,
|
|
654
|
+
a1: 2 * (k * k - 1) / a0,
|
|
655
|
+
a2: (1 - k / q + k * k) / a0
|
|
656
|
+
};
|
|
657
|
+
})()];
|
|
658
|
+
}
|
|
659
|
+
function filter(input, { b0, b1, b2, a1, a2 }) {
|
|
660
|
+
const out = new Float64Array(input.length);
|
|
661
|
+
let x1 = 0;
|
|
662
|
+
let x2 = 0;
|
|
663
|
+
let y1 = 0;
|
|
664
|
+
let y2 = 0;
|
|
665
|
+
for (let i = 0; i < input.length; i++) {
|
|
666
|
+
const x = input[i];
|
|
667
|
+
const y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
|
668
|
+
out[i] = y;
|
|
669
|
+
x2 = x1;
|
|
670
|
+
x1 = x;
|
|
671
|
+
y2 = y1;
|
|
672
|
+
y1 = y;
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
const toLufs = (energy) => -.691 + 10 * Math.log10(energy);
|
|
677
|
+
/** Integrated loudness in LUFS. -Infinity for silence or audio shorter than one 400 ms block. */
|
|
678
|
+
function integratedLoudness({ sampleRate, channels }) {
|
|
679
|
+
const [shelf, highpass] = kWeighting(sampleRate);
|
|
680
|
+
const weighted = channels.map((ch) => filter(filter(ch, shelf), highpass));
|
|
681
|
+
const block = Math.round(.4 * sampleRate);
|
|
682
|
+
const hop = Math.round(.1 * sampleRate);
|
|
683
|
+
const length = channels[0]?.length ?? 0;
|
|
684
|
+
const energies = [];
|
|
685
|
+
for (let start = 0; start + block <= length; start += hop) {
|
|
686
|
+
let sum = 0;
|
|
687
|
+
for (const ch of weighted) {
|
|
688
|
+
let s = 0;
|
|
689
|
+
for (let i = start; i < start + block; i++) s += ch[i] * ch[i];
|
|
690
|
+
sum += s / block;
|
|
691
|
+
}
|
|
692
|
+
energies.push(sum);
|
|
693
|
+
}
|
|
694
|
+
const mean = (xs) => xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
695
|
+
const loud = energies.filter((e) => toLufs(e) > -70);
|
|
696
|
+
if (!loud.length) return -Infinity;
|
|
697
|
+
const relative = toLufs(mean(loud)) - 10;
|
|
698
|
+
const gated = loud.filter((e) => toLufs(e) > relative);
|
|
699
|
+
return gated.length ? toLufs(mean(gated)) : -Infinity;
|
|
700
|
+
}
|
|
701
|
+
export { encodeWav, integratedLoudness, loadSampleBank, measure, renderProject };
|