opennote-cli 1.3.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/LICENSE +22 -0
- package/README.md +272 -0
- package/package.json +30 -0
- package/src/arrangement.js +282 -0
- package/src/arrangement.ts +317 -0
- package/src/assets/cover.png +0 -0
- package/src/cli.js +1398 -0
- package/src/cli.ts +1709 -0
- package/src/exportAudio.js +459 -0
- package/src/exportAudio.ts +499 -0
- package/src/exportMidi.js +85 -0
- package/src/exportMidi.ts +103 -0
- package/src/ffmpeg-static.d.ts +5 -0
- package/src/fx.js +28 -0
- package/src/fx.ts +49 -0
- package/src/generator.js +15 -0
- package/src/generator.ts +29 -0
- package/src/index.js +511 -0
- package/src/index.ts +642 -0
- package/src/instrument.js +35 -0
- package/src/instrument.ts +51 -0
- package/src/midi.js +167 -0
- package/src/midi.ts +218 -0
- package/src/openExport.js +22 -0
- package/src/openExport.ts +24 -0
- package/src/prompt.js +22 -0
- package/src/prompt.ts +25 -0
- package/src/providers/auth.js +23 -0
- package/src/providers/auth.ts +30 -0
- package/src/providers/claudeProvider.js +46 -0
- package/src/providers/claudeProvider.ts +50 -0
- package/src/providers/factory.js +39 -0
- package/src/providers/factory.ts +43 -0
- package/src/providers/geminiProvider.js +55 -0
- package/src/providers/geminiProvider.ts +71 -0
- package/src/providers/grokProvider.js +57 -0
- package/src/providers/grokProvider.ts +69 -0
- package/src/providers/groqProvider.js +57 -0
- package/src/providers/groqProvider.ts +69 -0
- package/src/providers/mockProvider.js +13 -0
- package/src/providers/mockProvider.ts +15 -0
- package/src/providers/openaiProvider.js +45 -0
- package/src/providers/openaiProvider.ts +49 -0
- package/src/providers/retry.js +46 -0
- package/src/providers/retry.ts +54 -0
- package/src/types.js +1 -0
- package/src/types.ts +17 -0
- package/src/validate.js +10 -0
- package/src/validate.ts +13 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { access, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { writeFile } from 'node:fs/promises';
|
|
5
|
+
const SAMPLE_RATE = 44100;
|
|
6
|
+
function pitchToFrequency(pitch) {
|
|
7
|
+
return 440 * Math.pow(2, (pitch - 69) / 12);
|
|
8
|
+
}
|
|
9
|
+
async function ensureParent(path) {
|
|
10
|
+
await mkdir(dirname(path), { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
function i16(n) {
|
|
13
|
+
return Math.max(-32768, Math.min(32767, Math.round(n)));
|
|
14
|
+
}
|
|
15
|
+
function waveSample(type, phase) {
|
|
16
|
+
const s = Math.sin(phase);
|
|
17
|
+
if (type === 'sine')
|
|
18
|
+
return s;
|
|
19
|
+
if (type === 'square')
|
|
20
|
+
return s >= 0 ? 1 : -1;
|
|
21
|
+
// saw, normalized [-1..1]
|
|
22
|
+
return 2 * ((phase / (2 * Math.PI)) % 1) - 1;
|
|
23
|
+
}
|
|
24
|
+
function drumSample(pitch, i, count) {
|
|
25
|
+
const t = i / SAMPLE_RATE;
|
|
26
|
+
const n = Math.random() * 2 - 1;
|
|
27
|
+
// Kick
|
|
28
|
+
if (pitch === 35 || pitch === 36) {
|
|
29
|
+
const f = 120 * Math.exp(-t * 14) + 42;
|
|
30
|
+
const phase = 2 * Math.PI * f * t;
|
|
31
|
+
const env = Math.exp(-t * 8);
|
|
32
|
+
return Math.sin(phase) * env;
|
|
33
|
+
}
|
|
34
|
+
// Snare / clap-ish
|
|
35
|
+
if (pitch === 38 || pitch === 40) {
|
|
36
|
+
const tone = Math.sin(2 * Math.PI * 210 * t) * Math.exp(-t * 14) * 0.35;
|
|
37
|
+
const noise = n * Math.exp(-t * 18) * 0.85;
|
|
38
|
+
return tone + noise;
|
|
39
|
+
}
|
|
40
|
+
// Closed/open hats
|
|
41
|
+
if (pitch === 42 || pitch === 44 || pitch === 46) {
|
|
42
|
+
const bright = n - (i > 0 ? Math.sin(2 * Math.PI * 1800 * ((i - 1) / SAMPLE_RATE)) * 0.2 : 0);
|
|
43
|
+
const env = pitch === 46 ? Math.exp(-t * 36) : Math.exp(-t * 58);
|
|
44
|
+
return bright * env;
|
|
45
|
+
}
|
|
46
|
+
// Generic percussion fallback
|
|
47
|
+
const env = Math.exp((-28 * i) / Math.max(1, count));
|
|
48
|
+
return n * env;
|
|
49
|
+
}
|
|
50
|
+
function applyPostFx(samples, fx) {
|
|
51
|
+
const out = new Float32Array(samples.length);
|
|
52
|
+
const driveGain = 1 + fx.drive * 14;
|
|
53
|
+
// bitcrush: quantize + hold
|
|
54
|
+
const bitDepth = Math.max(4, Math.round(16 - fx.bitcrush * 10));
|
|
55
|
+
const levels = Math.pow(2, bitDepth - 1);
|
|
56
|
+
const holdEvery = Math.max(1, Math.round(1 + fx.bitcrush * 14));
|
|
57
|
+
let held = 0;
|
|
58
|
+
for (let i = 0; i < samples.length; i++) {
|
|
59
|
+
if (i % holdEvery === 0) {
|
|
60
|
+
const driven = Math.tanh(samples[i] * driveGain);
|
|
61
|
+
held = Math.round(driven * levels) / levels;
|
|
62
|
+
}
|
|
63
|
+
out[i] = held;
|
|
64
|
+
}
|
|
65
|
+
const wetMix = Math.max(0, Math.min(1, fx.reverb));
|
|
66
|
+
if (wetMix <= 0.001)
|
|
67
|
+
return out;
|
|
68
|
+
// simple feedback delay as lightweight reverb flavor
|
|
69
|
+
const delaySamples = Math.max(1, Math.round(SAMPLE_RATE * (0.12 + 0.2 * wetMix)));
|
|
70
|
+
const feedback = 0.2 + 0.45 * wetMix;
|
|
71
|
+
const wet = new Float32Array(out);
|
|
72
|
+
for (let i = delaySamples; i < wet.length; i++) {
|
|
73
|
+
wet[i] += wet[i - delaySamples] * feedback;
|
|
74
|
+
}
|
|
75
|
+
const mixed = new Float32Array(out.length);
|
|
76
|
+
for (let i = 0; i < mixed.length; i++) {
|
|
77
|
+
const m = out[i] * (1 - wetMix) + wet[i] * wetMix;
|
|
78
|
+
mixed[i] = Math.max(-1, Math.min(1, m));
|
|
79
|
+
}
|
|
80
|
+
return mixed;
|
|
81
|
+
}
|
|
82
|
+
function buildWavPcm(sequence, fx) {
|
|
83
|
+
const events = sequence[0]?.startMs != null
|
|
84
|
+
? sequence
|
|
85
|
+
: (() => {
|
|
86
|
+
const out = [];
|
|
87
|
+
let t = 0;
|
|
88
|
+
for (const n of sequence) {
|
|
89
|
+
out.push({ pitch: n.pitch, velocity: n.velocity, durationMs: n.durationMs, startMs: t, channel: 0 });
|
|
90
|
+
t += n.durationMs;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
})();
|
|
94
|
+
const endMs = events.length ? Math.max(...events.map((e) => e.startMs + e.durationMs)) : 0;
|
|
95
|
+
const totalSamples = Math.max(1, Math.ceil((endMs / 1000) * SAMPLE_RATE));
|
|
96
|
+
const mix = new Float32Array(totalSamples);
|
|
97
|
+
for (const note of events) {
|
|
98
|
+
const isDrum = note.channel === 9;
|
|
99
|
+
const freq = isDrum ? 0 : pitchToFrequency(note.pitch);
|
|
100
|
+
const durationS = Math.max(0.02, note.durationMs / 1000);
|
|
101
|
+
const count = Math.max(1, Math.floor(SAMPLE_RATE * durationS));
|
|
102
|
+
const amp = (Math.max(1, Math.min(127, note.velocity)) / 127) * (isDrum ? 0.5 : 0.35);
|
|
103
|
+
const startSample = Math.max(0, Math.floor((note.startMs / 1000) * SAMPLE_RATE));
|
|
104
|
+
for (let i = 0; i < count; i++) {
|
|
105
|
+
const idx = startSample + i;
|
|
106
|
+
if (idx >= mix.length)
|
|
107
|
+
break;
|
|
108
|
+
if (isDrum) {
|
|
109
|
+
mix[idx] += drumSample(note.pitch, i, count) * amp;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const t = i / SAMPLE_RATE;
|
|
113
|
+
const phase = 2 * Math.PI * freq * t;
|
|
114
|
+
const envDecay = 2 + (1 - fx.reverb) * 4;
|
|
115
|
+
const env = Math.exp((-envDecay * i) / count);
|
|
116
|
+
const osc = waveSample(fx.waveform, phase);
|
|
117
|
+
mix[idx] += osc * amp * env;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// normalize before post fx
|
|
122
|
+
let peak = 0;
|
|
123
|
+
for (let i = 0; i < mix.length; i++)
|
|
124
|
+
peak = Math.max(peak, Math.abs(mix[i]));
|
|
125
|
+
if (peak > 1) {
|
|
126
|
+
const inv = 1 / peak;
|
|
127
|
+
for (let i = 0; i < mix.length; i++)
|
|
128
|
+
mix[i] *= inv;
|
|
129
|
+
}
|
|
130
|
+
const floatSamples = mix;
|
|
131
|
+
const processed = applyPostFx(floatSamples, fx);
|
|
132
|
+
const pcm = new Int16Array(processed.length);
|
|
133
|
+
for (let i = 0; i < processed.length; i++) {
|
|
134
|
+
pcm[i] = i16(processed[i] * 32767);
|
|
135
|
+
}
|
|
136
|
+
const dataSize = pcm.length * 2;
|
|
137
|
+
const b = Buffer.alloc(44 + dataSize);
|
|
138
|
+
b.write('RIFF', 0);
|
|
139
|
+
b.writeUInt32LE(36 + dataSize, 4);
|
|
140
|
+
b.write('WAVE', 8);
|
|
141
|
+
b.write('fmt ', 12);
|
|
142
|
+
b.writeUInt32LE(16, 16);
|
|
143
|
+
b.writeUInt16LE(1, 20);
|
|
144
|
+
b.writeUInt16LE(1, 22);
|
|
145
|
+
b.writeUInt32LE(SAMPLE_RATE, 24);
|
|
146
|
+
b.writeUInt32LE(SAMPLE_RATE * 2, 28);
|
|
147
|
+
b.writeUInt16LE(2, 32);
|
|
148
|
+
b.writeUInt16LE(16, 34);
|
|
149
|
+
b.write('data', 36);
|
|
150
|
+
b.writeUInt32LE(dataSize, 40);
|
|
151
|
+
for (let i = 0; i < pcm.length; i++) {
|
|
152
|
+
b.writeInt16LE(pcm[i], 44 + i * 2);
|
|
153
|
+
}
|
|
154
|
+
return b;
|
|
155
|
+
}
|
|
156
|
+
async function getFfmpegBinary() {
|
|
157
|
+
try {
|
|
158
|
+
const mod = await import('ffmpeg-static');
|
|
159
|
+
const path = mod.default;
|
|
160
|
+
if (path && typeof path === 'string')
|
|
161
|
+
return path;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Fall back to system ffmpeg.
|
|
165
|
+
}
|
|
166
|
+
return 'ffmpeg';
|
|
167
|
+
}
|
|
168
|
+
async function runFfmpeg(args) {
|
|
169
|
+
const bin = await getFfmpegBinary();
|
|
170
|
+
return new Promise((resolvePromise, reject) => {
|
|
171
|
+
const p = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
172
|
+
let stderr = '';
|
|
173
|
+
if (p.stderr) {
|
|
174
|
+
p.stderr.on('data', (chunk) => {
|
|
175
|
+
stderr += chunk.toString();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
p.on('error', reject);
|
|
179
|
+
p.on('close', (code) => {
|
|
180
|
+
if (code === 0)
|
|
181
|
+
resolvePromise();
|
|
182
|
+
else {
|
|
183
|
+
const tail = stderr.trim().split('\n').slice(-8).join('\n');
|
|
184
|
+
reject(new Error(`ffmpeg exited with code ${code ?? -1}${tail ? `\n${tail}` : ''}`));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export function isFfmpegMissing(err) {
|
|
190
|
+
if (!err || typeof err !== 'object')
|
|
191
|
+
return false;
|
|
192
|
+
const maybe = err;
|
|
193
|
+
return maybe.code === 'ENOENT';
|
|
194
|
+
}
|
|
195
|
+
export async function exportSequenceToWav(sequence, outPath, fx) {
|
|
196
|
+
const fullPath = resolve(outPath);
|
|
197
|
+
await ensureParent(fullPath);
|
|
198
|
+
await writeFile(fullPath, buildWavPcm(sequence, fx));
|
|
199
|
+
return fullPath;
|
|
200
|
+
}
|
|
201
|
+
export async function exportWavToMp3(wavPath, outPath) {
|
|
202
|
+
const fullPath = resolve(outPath);
|
|
203
|
+
await ensureParent(fullPath);
|
|
204
|
+
await runFfmpeg(['-y', '-i', resolve(wavPath), '-codec:a', 'libmp3lame', '-q:a', '2', fullPath]);
|
|
205
|
+
return fullPath;
|
|
206
|
+
}
|
|
207
|
+
export async function exportWavToMp4(wavPath, outPath, coverImagePath) {
|
|
208
|
+
const fullPath = resolve(outPath);
|
|
209
|
+
await ensureParent(fullPath);
|
|
210
|
+
const audio = resolve(wavPath);
|
|
211
|
+
const image = coverImagePath?.trim();
|
|
212
|
+
let hasImage = false;
|
|
213
|
+
if (image) {
|
|
214
|
+
try {
|
|
215
|
+
await access(resolve(image));
|
|
216
|
+
hasImage = true;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
hasImage = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const args = image
|
|
223
|
+
? [
|
|
224
|
+
'-y',
|
|
225
|
+
'-loop',
|
|
226
|
+
'1',
|
|
227
|
+
'-i',
|
|
228
|
+
resolve(image),
|
|
229
|
+
'-i',
|
|
230
|
+
audio,
|
|
231
|
+
'-c:v',
|
|
232
|
+
'libx264',
|
|
233
|
+
'-tune',
|
|
234
|
+
'stillimage',
|
|
235
|
+
'-c:a',
|
|
236
|
+
'aac',
|
|
237
|
+
'-b:a',
|
|
238
|
+
'192k',
|
|
239
|
+
'-pix_fmt',
|
|
240
|
+
'yuv420p',
|
|
241
|
+
'-shortest',
|
|
242
|
+
fullPath,
|
|
243
|
+
]
|
|
244
|
+
: [
|
|
245
|
+
'-y',
|
|
246
|
+
'-f',
|
|
247
|
+
'lavfi',
|
|
248
|
+
'-i',
|
|
249
|
+
'color=c=#120a1a:s=1280x720:r=30',
|
|
250
|
+
'-i',
|
|
251
|
+
audio,
|
|
252
|
+
'-c:v',
|
|
253
|
+
'libx264',
|
|
254
|
+
'-tune',
|
|
255
|
+
'stillimage',
|
|
256
|
+
'-c:a',
|
|
257
|
+
'aac',
|
|
258
|
+
'-b:a',
|
|
259
|
+
'192k',
|
|
260
|
+
'-pix_fmt',
|
|
261
|
+
'yuv420p',
|
|
262
|
+
'-shortest',
|
|
263
|
+
fullPath,
|
|
264
|
+
];
|
|
265
|
+
if (!hasImage) {
|
|
266
|
+
await runFfmpeg([
|
|
267
|
+
'-y',
|
|
268
|
+
'-f',
|
|
269
|
+
'lavfi',
|
|
270
|
+
'-i',
|
|
271
|
+
'color=c=#120a1a:s=1280x720:r=30',
|
|
272
|
+
'-i',
|
|
273
|
+
audio,
|
|
274
|
+
'-c:v',
|
|
275
|
+
'libx264',
|
|
276
|
+
'-tune',
|
|
277
|
+
'stillimage',
|
|
278
|
+
'-c:a',
|
|
279
|
+
'aac',
|
|
280
|
+
'-b:a',
|
|
281
|
+
'192k',
|
|
282
|
+
'-pix_fmt',
|
|
283
|
+
'yuv420p',
|
|
284
|
+
'-shortest',
|
|
285
|
+
fullPath,
|
|
286
|
+
]);
|
|
287
|
+
return fullPath;
|
|
288
|
+
}
|
|
289
|
+
await runFfmpeg(args);
|
|
290
|
+
return fullPath;
|
|
291
|
+
}
|
|
292
|
+
function detectRecordInput(device) {
|
|
293
|
+
const d = (device ?? '').trim();
|
|
294
|
+
if (process.platform === 'darwin') {
|
|
295
|
+
// avfoundation uses "<video>:<audio>", so audio-only default is ":0".
|
|
296
|
+
const input = d ? (d.includes(':') ? d : `:${d}`) : ':0';
|
|
297
|
+
return ['-f', 'avfoundation', '-i', input];
|
|
298
|
+
}
|
|
299
|
+
if (process.platform === 'linux') {
|
|
300
|
+
const input = d || 'default';
|
|
301
|
+
return ['-f', 'pulse', '-i', input];
|
|
302
|
+
}
|
|
303
|
+
if (process.platform === 'win32') {
|
|
304
|
+
const input = d || 'audio=default';
|
|
305
|
+
return ['-f', 'dshow', '-i', input];
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`Recording is not supported on platform: ${process.platform}`);
|
|
308
|
+
}
|
|
309
|
+
export async function recordInputToWav(outPath, seconds, device) {
|
|
310
|
+
const fullPath = resolve(outPath);
|
|
311
|
+
await ensureParent(fullPath);
|
|
312
|
+
const duration = Math.max(1, Math.floor(seconds));
|
|
313
|
+
const inputArgs = detectRecordInput(device);
|
|
314
|
+
await runFfmpeg([
|
|
315
|
+
'-y',
|
|
316
|
+
...inputArgs,
|
|
317
|
+
'-t',
|
|
318
|
+
String(duration),
|
|
319
|
+
'-ac',
|
|
320
|
+
'1',
|
|
321
|
+
'-ar',
|
|
322
|
+
String(SAMPLE_RATE),
|
|
323
|
+
fullPath,
|
|
324
|
+
]);
|
|
325
|
+
return fullPath;
|
|
326
|
+
}
|
|
327
|
+
export async function processRecordedWav(inPath, outPath, options) {
|
|
328
|
+
const fullPath = resolve(outPath);
|
|
329
|
+
await ensureParent(fullPath);
|
|
330
|
+
const eqMode = options?.eqMode ?? 'balanced';
|
|
331
|
+
const profile = options?.profile ?? 'default';
|
|
332
|
+
const family = options?.family ?? 'character';
|
|
333
|
+
const bugMode = options?.bugMode ?? 'off';
|
|
334
|
+
const intensity = Math.max(0, Math.min(100, options?.intensity ?? 45)) / 100;
|
|
335
|
+
const chaos = Math.max(0, Math.min(100, options?.chaos ?? 25)) / 100;
|
|
336
|
+
const mix = Math.max(0, Math.min(100, options?.mix ?? 40)) / 100;
|
|
337
|
+
const fxAmt = Math.max(0, Math.min(1, intensity * (0.25 + mix * 0.75)));
|
|
338
|
+
const scratch = options?.scratch ?? 'off';
|
|
339
|
+
const wavyAmount = Math.max(0, Math.min(100, options?.wavy ?? 0));
|
|
340
|
+
const filters = [];
|
|
341
|
+
if (profile === 'vinyl') {
|
|
342
|
+
filters.push('highpass=f=110');
|
|
343
|
+
filters.push('lowpass=f=7200');
|
|
344
|
+
filters.push('acompressor=threshold=-20dB:ratio=2.2:attack=10:release=120');
|
|
345
|
+
filters.push('aecho=0.6:0.3:38:0.12');
|
|
346
|
+
}
|
|
347
|
+
else if (profile === 'dust') {
|
|
348
|
+
filters.push('highpass=f=280');
|
|
349
|
+
filters.push('lowpass=f=3600');
|
|
350
|
+
filters.push('acompressor=threshold=-18dB:ratio=3.6:attack=12:release=150');
|
|
351
|
+
filters.push('acrusher=bits=7:mode=lin:mix=0.22');
|
|
352
|
+
}
|
|
353
|
+
if (eqMode === 'balanced') {
|
|
354
|
+
filters.push('highpass=f=35');
|
|
355
|
+
filters.push('lowpass=f=13000');
|
|
356
|
+
filters.push('equalizer=f=140:width_type=h:width=90:g=1.6');
|
|
357
|
+
filters.push('equalizer=f=3300:width_type=h:width=1800:g=1.1');
|
|
358
|
+
}
|
|
359
|
+
else if (eqMode === 'warm') {
|
|
360
|
+
filters.push('lowpass=f=8400');
|
|
361
|
+
filters.push('equalizer=f=170:width_type=h:width=120:g=2.4');
|
|
362
|
+
filters.push('equalizer=f=2600:width_type=h:width=1300:g=-1.4');
|
|
363
|
+
}
|
|
364
|
+
else if (eqMode === 'bright') {
|
|
365
|
+
filters.push('highpass=f=45');
|
|
366
|
+
filters.push('equalizer=f=4600:width_type=h:width=2200:g=2.2');
|
|
367
|
+
filters.push('equalizer=f=160:width_type=h:width=100:g=-1');
|
|
368
|
+
}
|
|
369
|
+
else if (eqMode === 'bass') {
|
|
370
|
+
filters.push('equalizer=f=95:width_type=h:width=75:g=4');
|
|
371
|
+
filters.push('equalizer=f=3000:width_type=h:width=1800:g=-1.2');
|
|
372
|
+
}
|
|
373
|
+
else if (eqMode === 'phone') {
|
|
374
|
+
filters.push('highpass=f=350');
|
|
375
|
+
filters.push('lowpass=f=3200');
|
|
376
|
+
filters.push('equalizer=f=1500:width_type=h:width=900:g=2.2');
|
|
377
|
+
}
|
|
378
|
+
if (family === 'character') {
|
|
379
|
+
const crushBits = Math.max(7, Math.round(16 - fxAmt * 7));
|
|
380
|
+
const crushMix = (0.06 + fxAmt * 0.18).toFixed(2);
|
|
381
|
+
filters.push(`acrusher=bits=${crushBits}:mode=lin:mix=${crushMix}`);
|
|
382
|
+
filters.push('acompressor=threshold=-18dB:ratio=2.2:attack=6:release=80');
|
|
383
|
+
}
|
|
384
|
+
else if (family === 'motion') {
|
|
385
|
+
const tremDepth = (0.08 + fxAmt * 0.42).toFixed(2);
|
|
386
|
+
const vibDepth = Math.min(0.9, 0.05 + fxAmt * 0.6).toFixed(2);
|
|
387
|
+
const vibFreq = (2.5 + chaos * 5).toFixed(2);
|
|
388
|
+
filters.push(`tremolo=f=6:d=${tremDepth}`);
|
|
389
|
+
filters.push(`vibrato=f=${vibFreq}:d=${vibDepth}`);
|
|
390
|
+
}
|
|
391
|
+
else if (family === 'space') {
|
|
392
|
+
const d1 = Math.round(40 + fxAmt * 120);
|
|
393
|
+
const d2 = Math.round(80 + fxAmt * 220);
|
|
394
|
+
const dec1 = (0.12 + fxAmt * 0.35).toFixed(2);
|
|
395
|
+
const dec2 = (0.08 + fxAmt * 0.24).toFixed(2);
|
|
396
|
+
filters.push(`aecho=0.7:0.45:${d1}|${d2}:${dec1}|${dec2}`);
|
|
397
|
+
filters.push('lowpass=f=8200');
|
|
398
|
+
}
|
|
399
|
+
else if (family === 'bug' && bugMode !== 'off') {
|
|
400
|
+
if (bugMode === 'pll-drift') {
|
|
401
|
+
const vibFreq = (0.45 + chaos * 1.8).toFixed(2);
|
|
402
|
+
const vibDepth = Math.min(0.8, 0.08 + fxAmt * 0.5).toFixed(2);
|
|
403
|
+
filters.push(`vibrato=f=${vibFreq}:d=${vibDepth}`);
|
|
404
|
+
filters.push(`tremolo=f=${(3 + chaos * 6).toFixed(2)}:d=${(0.1 + fxAmt * 0.16).toFixed(2)}`);
|
|
405
|
+
}
|
|
406
|
+
else if (bugMode === 'buffer-tear') {
|
|
407
|
+
const tempo = (1 + (chaos - 0.5) * 0.08).toFixed(3);
|
|
408
|
+
filters.push(`atempo=${tempo}`);
|
|
409
|
+
filters.push(`tremolo=f=${(8 + chaos * 14).toFixed(2)}:d=${(0.2 + fxAmt * 0.28).toFixed(2)}`);
|
|
410
|
+
}
|
|
411
|
+
else if (bugMode === 'clock-bleed') {
|
|
412
|
+
const bits = Math.max(4, Math.round(12 - fxAmt * 6));
|
|
413
|
+
const crushMix = (0.22 + fxAmt * 0.5).toFixed(2);
|
|
414
|
+
filters.push(`acrusher=bits=${bits}:mode=lin:mix=${crushMix}`);
|
|
415
|
+
filters.push('highpass=f=260');
|
|
416
|
+
}
|
|
417
|
+
else if (bugMode === 'memory-rot') {
|
|
418
|
+
const lp = Math.round(5200 - fxAmt * 2600);
|
|
419
|
+
const bits = Math.max(5, Math.round(10 - fxAmt * 4));
|
|
420
|
+
filters.push(`lowpass=f=${lp}`);
|
|
421
|
+
filters.push(`acrusher=bits=${bits}:mode=lin:mix=${(0.18 + fxAmt * 0.35).toFixed(2)}`);
|
|
422
|
+
}
|
|
423
|
+
else if (bugMode === 'crc-glitch') {
|
|
424
|
+
filters.push(`tremolo=f=${(14 + chaos * 20).toFixed(2)}:d=${(0.28 + fxAmt * 0.24).toFixed(2)}`);
|
|
425
|
+
filters.push('highpass=f=900');
|
|
426
|
+
filters.push('lowpass=f=2400');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (scratch === 'texture') {
|
|
430
|
+
filters.push('tremolo=f=9:d=0.35');
|
|
431
|
+
filters.push('highpass=f=420');
|
|
432
|
+
filters.push('lowpass=f=3200');
|
|
433
|
+
}
|
|
434
|
+
else if (scratch === 'dj') {
|
|
435
|
+
filters.push('tremolo=f=13:d=0.78');
|
|
436
|
+
filters.push('asetrate=44100*1.04');
|
|
437
|
+
filters.push('aresample=44100');
|
|
438
|
+
filters.push('aecho=0.7:0.45:35|70:0.35|0.15');
|
|
439
|
+
filters.push('highpass=f=520');
|
|
440
|
+
filters.push('lowpass=f=2800');
|
|
441
|
+
}
|
|
442
|
+
if (wavyAmount > 0) {
|
|
443
|
+
const tremDepth = (0.08 + (wavyAmount / 100) * 0.42).toFixed(2);
|
|
444
|
+
// ffmpeg vibrato depth "d" must be in [0, 1].
|
|
445
|
+
const vibDepth = Math.min(0.95, 0.08 + (wavyAmount / 100) * 0.87).toFixed(2);
|
|
446
|
+
filters.push(`tremolo=f=4:d=${tremDepth}`);
|
|
447
|
+
filters.push(`vibrato=f=5:d=${vibDepth}`);
|
|
448
|
+
}
|
|
449
|
+
// Keep output audible when aggressive FX stacks are selected.
|
|
450
|
+
filters.push('acompressor=threshold=-16dB:ratio=2:attack=5:release=90:makeup=4');
|
|
451
|
+
filters.push('alimiter=limit=0.96');
|
|
452
|
+
const args = ['-y', '-i', resolve(inPath)];
|
|
453
|
+
if (filters.length > 0) {
|
|
454
|
+
args.push('-af', filters.join(','));
|
|
455
|
+
}
|
|
456
|
+
args.push(fullPath);
|
|
457
|
+
await runFfmpeg(args);
|
|
458
|
+
return fullPath;
|
|
459
|
+
}
|