@e9g/buffered-audio-nodes 0.2.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 ADDED
@@ -0,0 +1,532 @@
1
+ # buffered-audio-nodes
2
+
3
+ A streaming audio processing protocol. Chainable modules that read, transform, and write audio — an open, scriptable, extensible alternative to GUI-bound audio engineering tools.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @e9g/buffered-audio-nodes
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Three module types — sources produce audio, transforms process it, targets consume it. The `chain()` function wires them into a pipeline.
14
+
15
+ ```ts
16
+ import { chain, read, normalize, write } from "@e9g/buffered-audio-nodes";
17
+
18
+ const source = chain(
19
+ read("input.wav"),
20
+ normalize({ ceiling: 0.95 }),
21
+ write("output.wav", { bitDepth: "24" })
22
+ );
23
+
24
+ await source.render();
25
+ ```
26
+
27
+ `render()` streams audio through the chain. Backpressure, buffering, and lifecycle are handled by the framework.
28
+
29
+ ### Fan
30
+
31
+ Split a stream into parallel branches with `fan()`:
32
+
33
+ ```ts
34
+ import { chain, read, fan, normalize, trim, write } from "@e9g/buffered-audio-nodes";
35
+
36
+ const source = chain(
37
+ read("input.wav"),
38
+ fan(
39
+ chain(normalize(), write("normalized.wav")),
40
+ chain(trim(), write("trimmed.wav"))
41
+ )
42
+ );
43
+
44
+ await source.render();
45
+ ```
46
+
47
+ ## CLI
48
+
49
+ Run pipelines from TypeScript files. The file's default export must be a `SourceModule`.
50
+
51
+ ```bash
52
+ npx @e9g/buffered-audio-nodes process --pipeline pipeline.ts
53
+ ```
54
+
55
+ ```ts
56
+ // pipeline.ts
57
+ import { chain, read, normalize, trim, write } from "@e9g/buffered-audio-nodes";
58
+
59
+ export default chain(
60
+ read("input.wav"),
61
+ normalize(),
62
+ trim({ threshold: -60 }),
63
+ write("output.wav")
64
+ );
65
+ ```
66
+
67
+ | Flag | Description |
68
+ |------|-------------|
69
+ | `--chunk-size <samples>` | Chunk size in samples |
70
+ | `--high-water-mark <count>` | Stream backpressure high water mark |
71
+
72
+ ## Modules
73
+
74
+ ### Sources
75
+
76
+ #### `read(path, options?)`
77
+
78
+ Read audio from a file. WAV files are read natively. Other formats are transcoded via FFmpeg.
79
+
80
+ | Parameter | Type | Default | Description |
81
+ |-----------|------|---------|-------------|
82
+ | `path` | `string` | | File path |
83
+ | `channels` | `number[]` | all | Channel indices to extract |
84
+ | `ffmpegPath` | `string` | | Path to [ffmpeg](https://ffmpeg.org/download.html). Required for non-WAV files. |
85
+ | `ffprobePath` | `string` | | Path to [ffprobe](https://ffmpeg.org/download.html). Required for non-WAV files. |
86
+
87
+ ### Targets
88
+
89
+ #### `write(path, options?)`
90
+
91
+ Write audio to a file. Writes WAV natively. Other formats are encoded via FFmpeg.
92
+
93
+ | Parameter | Type | Default | Description |
94
+ |-----------|------|---------|-------------|
95
+ | `path` | `string` | | Output file path |
96
+ | `bitDepth` | `"16" \| "24" \| "32" \| "32f"` | `"16"` | WAV bit depth |
97
+ | `encoding` | `EncodingOptions` | | Non-WAV encoding (format, bitrate, vbr) |
98
+ | `ffmpegPath` | `string` | | Path to [ffmpeg](https://ffmpeg.org/download.html). Required for non-WAV formats. |
99
+
100
+ ### Composites
101
+
102
+ #### `chain(...modules)`
103
+
104
+ Wire modules into a linear pipeline. Returns the source module.
105
+
106
+ #### `fan(...branches)`
107
+
108
+ Split a stream into parallel transform branches.
109
+
110
+ ### Transforms — Basic
111
+
112
+ #### `normalize(options?)`
113
+
114
+ Adjust peak level to a target ceiling.
115
+
116
+ | Parameter | Type | Default | Description |
117
+ |-----------|------|---------|-------------|
118
+ | `ceiling` | `number` | `1.0` | Target peak level (0–1) |
119
+
120
+ #### `trim(options?)`
121
+
122
+ Remove silence from start and end.
123
+
124
+ | Parameter | Type | Default | Description |
125
+ |-----------|------|---------|-------------|
126
+ | `threshold` | `number` | `0.001` | Silence threshold (0–1) |
127
+ | `margin` | `number` | `0.01` | Margin to keep around content (0–1) |
128
+ | `start` | `boolean` | `true` | Trim start |
129
+ | `end` | `boolean` | `true` | Trim end |
130
+
131
+ #### `cut(options?)`
132
+
133
+ Remove regions from audio.
134
+
135
+ | Parameter | Type | Default | Description |
136
+ |-----------|------|---------|-------------|
137
+ | `regions` | `CutRegion[]` | | Regions to remove (`{ start, end }` in samples) |
138
+
139
+ #### `pad(options?)`
140
+
141
+ Add silence to start or end.
142
+
143
+ | Parameter | Type | Default | Description |
144
+ |-----------|------|---------|-------------|
145
+ | `before` | `number` | `0` | Samples of silence before audio |
146
+ | `after` | `number` | `0` | Samples of silence after audio |
147
+
148
+ #### `dither(options?)`
149
+
150
+ Add dither noise before bit-depth reduction.
151
+
152
+ | Parameter | Type | Default | Description |
153
+ |-----------|------|---------|-------------|
154
+ | `bitDepth` | `"16" \| "24"` | `"16"` | Target bit depth |
155
+ | `noiseShaping` | `boolean` | `false` | Apply noise shaping |
156
+
157
+ #### `phase(options?)` / `invert()`
158
+
159
+ Invert polarity or shift phase.
160
+
161
+ | Parameter | Type | Default | Description |
162
+ |-----------|------|---------|-------------|
163
+ | `invert` | `boolean` | `true` | Invert polarity |
164
+ | `angle` | `number` | `0` | Phase angle (-180 to 180) |
165
+
166
+ #### `reverse()`
167
+
168
+ Reverse audio. No parameters.
169
+
170
+ #### `splice(options?)`
171
+
172
+ Insert audio at a position.
173
+
174
+ | Parameter | Type | Default | Description |
175
+ |-----------|------|---------|-------------|
176
+ | `insertPath` | `string` | | WAV file to insert |
177
+ | `insertAt` | `number` | `0` | Insert position in samples |
178
+
179
+ #### `waveform(options?)`
180
+
181
+ Extract waveform data to a file.
182
+
183
+ | Parameter | Type | Default | Description |
184
+ |-----------|------|---------|-------------|
185
+ | `outputPath` | `string` | | Output file path |
186
+ | `resolution` | `number` | `1000` | Number of waveform points (100–10000) |
187
+
188
+ ### Transforms — FFmpeg
189
+
190
+ These transforms require an [ffmpeg](https://ffmpeg.org/download.html) binary.
191
+
192
+ #### `ffmpeg(options)`
193
+
194
+ Run arbitrary FFmpeg filters.
195
+
196
+ | Parameter | Type | Default | Description |
197
+ |-----------|------|---------|-------------|
198
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
199
+ | `args` | `string[] \| (ctx) => string[]` | `[]` | FFmpeg filter arguments |
200
+
201
+ #### `resample(ffmpegPath, sampleRate, options?)`
202
+
203
+ Change sample rate.
204
+
205
+ | Parameter | Type | Default | Description |
206
+ |-----------|------|---------|-------------|
207
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
208
+ | `sampleRate` | `number` | `44100` | Target sample rate (8000–192000) |
209
+ | `dither` | `"triangular" \| "lipshitz" \| "none"` | `"triangular"` | Dither method |
210
+
211
+ #### `loudness(ffmpegPath, options?)`
212
+
213
+ Adjust loudness to EBU R128 target.
214
+
215
+ | Parameter | Type | Default | Description |
216
+ |-----------|------|---------|-------------|
217
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
218
+ | `target` | `number` | `-14` | Target LUFS (-50 to 0) |
219
+ | `truePeak` | `number` | `-1` | True peak limit dBTP (-10 to 0) |
220
+ | `lra` | `number` | `0` | Loudness range target (0–20) |
221
+
222
+ #### `loudnessStats(options?)`
223
+
224
+ Measure loudness statistics. Access results via `.stats` after render. No parameters.
225
+
226
+ #### `spectrogram(outputPath, options?)`
227
+
228
+ Generate spectrogram data file.
229
+
230
+ | Parameter | Type | Default | Description |
231
+ |-----------|------|---------|-------------|
232
+ | `outputPath` | `string` | | Output file path |
233
+ | `fftSize` | `number` | `2048` | FFT size (256–8192) |
234
+ | `hopSize` | `number` | `512` | Hop size (64–4096) |
235
+ | `frequencyScale` | `"linear" \| "log"` | `"log"` | Frequency axis scale |
236
+ | `numBands` | `number` | `512` | Number of frequency bands (log scale) |
237
+ | `minFrequency` | `number` | `20` | Minimum frequency in Hz |
238
+ | `maxFrequency` | `number` | `sr/2` | Maximum frequency in Hz |
239
+
240
+ ### Transforms — Audio Engineering
241
+
242
+ #### `breathControl(options?)`
243
+
244
+ Reduce or remove breath sounds.
245
+
246
+ | Parameter | Type | Default | Description |
247
+ |-----------|------|---------|-------------|
248
+ | `sensitivity` | `number` | `0.5` | Detection sensitivity (0–1) |
249
+ | `reduction` | `number` | `-12` | Reduction in dB (-60 to 0) |
250
+ | `mode` | `"remove" \| "attenuate"` | `"attenuate"` | Processing mode |
251
+
252
+ #### `deBleed(referencePath, options?)`
253
+
254
+ Remove microphone bleed using a reference signal.
255
+
256
+ | Parameter | Type | Default | Description |
257
+ |-----------|------|---------|-------------|
258
+ | `referencePath` | `string` | | Path to reference WAV |
259
+ | `filterLength` | `number` | `1024` | Adaptive filter length (64–8192) |
260
+ | `stepSize` | `number` | `0.1` | Adaptation step size (0.001–1) |
261
+
262
+ #### `deClick(options?)`
263
+
264
+ Remove clicks and pops.
265
+
266
+ | Parameter | Type | Default | Description |
267
+ |-----------|------|---------|-------------|
268
+ | `sensitivity` | `number` | `0.5` | Detection sensitivity (0–1) |
269
+ | `maxClickDuration` | `number` | `200` | Max click duration in samples (1–1000) |
270
+
271
+ #### `deCrackle(options?)`
272
+
273
+ Remove crackle noise.
274
+
275
+ | Parameter | Type | Default | Description |
276
+ |-----------|------|---------|-------------|
277
+ | `sensitivity` | `number` | `0.5` | Detection sensitivity (0–1) |
278
+
279
+ #### `mouthDeClick(options?)`
280
+
281
+ Remove mouth clicks.
282
+
283
+ | Parameter | Type | Default | Description |
284
+ |-----------|------|---------|-------------|
285
+ | `sensitivity` | `number` | `0.7` | Detection sensitivity (0–1) |
286
+
287
+ #### `deClip(options?)`
288
+
289
+ Repair clipped audio.
290
+
291
+ | Parameter | Type | Default | Description |
292
+ |-----------|------|---------|-------------|
293
+ | `threshold` | `number` | `0.99` | Clipping detection threshold (0–1) |
294
+ | `method` | `"ar" \| "sparse"` | `"ar"` | Repair method |
295
+
296
+ #### `dePlosive(options?)`
297
+
298
+ Reduce plosive sounds.
299
+
300
+ | Parameter | Type | Default | Description |
301
+ |-----------|------|---------|-------------|
302
+ | `sensitivity` | `number` | `0.5` | Detection sensitivity (0–1) |
303
+ | `frequency` | `number` | `200` | Plosive frequency cutoff in Hz (50–500) |
304
+
305
+ #### `deReverb(options?)`
306
+
307
+ Reduce reverb and room tone using WPE dereverberation.
308
+
309
+ | Parameter | Type | Default | Description |
310
+ |-----------|------|---------|-------------|
311
+ | `sensitivity` | `number` | `0.5` | Controls prediction delay, filter length, and iterations (0–1) |
312
+ | `predictionDelay` | `number` | | Prediction delay in frames (1–10) |
313
+ | `filterLength` | `number` | | Filter length in frames (5–30) |
314
+ | `iterations` | `number` | | WPE iterations (1–10) |
315
+ | `vkfftAddonPath` | `string` | | Path to [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) (GPU FFT) |
316
+ | `fftwAddonPath` | `string` | | Path to [fftw-addon](https://github.com/visionsofparadise/fftw-addon) (CPU FFT) |
317
+
318
+ #### `dialogueIsolate(options)`
319
+
320
+ Isolate dialogue using MDX-Net vocal separation.
321
+
322
+ | Parameter | Type | Default | Description |
323
+ |-----------|------|---------|-------------|
324
+ | `modelPath` | `string` | | Path to [Kim_Vocal_2.onnx](https://huggingface.co/seanghay/uvr_models) |
325
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
326
+ | `onnxAddonPath` | `string` | | Path to [onnx-runtime-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
327
+ | `highPass` | `number` | `80` | High pass filter in Hz (20–500) |
328
+ | `lowPass` | `number` | `20000` | Low pass filter in Hz (1000–22050) |
329
+
330
+ #### `eqMatch(referencePath, options?)`
331
+
332
+ Match EQ profile to a reference recording.
333
+
334
+ | Parameter | Type | Default | Description |
335
+ |-----------|------|---------|-------------|
336
+ | `referencePath` | `string` | | Path to reference WAV |
337
+ | `smoothing` | `number` | `0.33` | Spectral smoothing (0–1) |
338
+ | `vkfftAddonPath` | `string` | | Path to [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) |
339
+ | `fftwAddonPath` | `string` | | Path to [fftw-addon](https://github.com/visionsofparadise/fftw-addon) |
340
+
341
+ #### `leveler(options?)`
342
+
343
+ Automatic level control.
344
+
345
+ | Parameter | Type | Default | Description |
346
+ |-----------|------|---------|-------------|
347
+ | `target` | `number` | `-20` | Target level in dB (-60 to 0) |
348
+ | `window` | `number` | `0.5` | Analysis window in seconds (0.01–5) |
349
+ | `speed` | `number` | `0.1` | Adjustment speed (0.01–1) |
350
+ | `maxGain` | `number` | `12` | Maximum gain in dB (0–40) |
351
+ | `maxCut` | `number` | `12` | Maximum cut in dB (0–40) |
352
+
353
+ #### `musicRebalance(modelPath, stems, options?)`
354
+
355
+ Adjust stem levels using HTDemucs source separation.
356
+
357
+ | Parameter | Type | Default | Description |
358
+ |-----------|------|---------|-------------|
359
+ | `modelPath` | `string` | | Path to [htdemucs.onnx](https://github.com/facebookresearch/demucs) (requires .onnx.data file alongside) |
360
+ | `stems` | `Partial<StemGains>` | | Gain per stem: `{ vocals, drums, bass, other }` |
361
+ | `onnxAddonPath` | `string` | | Path to [onnx-runtime-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
362
+
363
+ #### `pitchShift(ffmpegPath, semitones, options?)`
364
+
365
+ Shift pitch without changing duration.
366
+
367
+ | Parameter | Type | Default | Description |
368
+ |-----------|------|---------|-------------|
369
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
370
+ | `semitones` | `number` | | Semitones to shift (-24 to 24) |
371
+ | `cents` | `number` | `0` | Cents to shift (-100 to 100) |
372
+
373
+ #### `spectralRepair(regions, options?)`
374
+
375
+ Repair spectral regions.
376
+
377
+ | Parameter | Type | Default | Description |
378
+ |-----------|------|---------|-------------|
379
+ | `regions` | `SpectralRegion[]` | | Regions to repair (`{ startTime, endTime, startFreq, endFreq }`) |
380
+ | `method` | `"ar" \| "nmf"` | `"ar"` | Repair method |
381
+ | `vkfftAddonPath` | `string` | | Path to [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) |
382
+ | `fftwAddonPath` | `string` | | Path to [fftw-addon](https://github.com/visionsofparadise/fftw-addon) |
383
+
384
+ #### `timeStretch(ffmpegPath, rate, options?)`
385
+
386
+ Change duration without affecting pitch.
387
+
388
+ | Parameter | Type | Default | Description |
389
+ |-----------|------|---------|-------------|
390
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
391
+ | `rate` | `number` | | Time stretch rate (0.25–4) |
392
+
393
+ #### `voiceDenoise(options)`
394
+
395
+ Remove background noise from speech using DTLN.
396
+
397
+ | Parameter | Type | Default | Description |
398
+ |-----------|------|---------|-------------|
399
+ | `modelPath1` | `string` | | Path to [DTLN model_1.onnx](https://github.com/breizhn/DTLN) (magnitude mask estimation) |
400
+ | `modelPath2` | `string` | | Path to [DTLN model_2.onnx](https://github.com/breizhn/DTLN) (signal reconstruction) |
401
+ | `ffmpegPath` | `string` | | Path to ffmpeg |
402
+ | `onnxAddonPath` | `string` | | Path to [onnx-runtime-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
403
+ | `vkfftAddonPath` | `string` | | Path to [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) |
404
+ | `fftwAddonPath` | `string` | | Path to [fftw-addon](https://github.com/visionsofparadise/fftw-addon) |
405
+
406
+ ## Creating Modules
407
+
408
+ Extend `SourceModule`, `TransformModule`, or `TargetModule`.
409
+
410
+ ### Transform
411
+
412
+ The most common module type. A transform's `bufferSize` controls how audio is collected before processing:
413
+
414
+ - `0` — streaming. Each chunk passes through `_unbuffer` immediately.
415
+ - `n` — block-based. Chunks accumulate in a `ChunkBuffer` until `n` frames are collected, then `_process` runs on the buffer and `_unbuffer` emits the result.
416
+ - `Infinity` — full-file. All audio is buffered before `_process` and `_unbuffer` run.
417
+
418
+ The three hooks:
419
+
420
+ - **`_buffer(chunk, buffer)`** — called for each incoming chunk. Override to inspect or modify data as it's buffered. Default appends to the buffer.
421
+ - **`_process(buffer)`** — called once the buffer reaches `bufferSize`. Use this for analysis or in-place modification of the full buffer.
422
+ - **`_unbuffer(chunk)`** — called for each chunk emitted from the buffer. Transform or replace the chunk here. Return `undefined` to drop it.
423
+
424
+ ```ts
425
+ import {
426
+ TransformModule,
427
+ type TransformModuleProperties,
428
+ type AudioChunk,
429
+ type ChunkBuffer,
430
+ } from "@e9g/buffered-audio-nodes";
431
+ import { z } from "zod";
432
+
433
+ const schema = z.object({
434
+ ceiling: z.number().min(0).max(1).default(1.0),
435
+ });
436
+
437
+ interface NormalizeProperties extends z.infer<typeof schema>, TransformModuleProperties {}
438
+
439
+ class NormalizeModule extends TransformModule<NormalizeProperties> {
440
+ static override readonly moduleName = "Normalize";
441
+ static override readonly moduleDescription = "Adjust peak level to a target ceiling";
442
+ static override readonly schema = schema;
443
+
444
+ override readonly type = ["buffered-audio-node", "transform", "normalize"] as const;
445
+
446
+ // Buffer all audio before processing — we need to find the peak first
447
+ override readonly bufferSize = Infinity;
448
+ override readonly latency = Infinity;
449
+
450
+ private peak = 0;
451
+ private scale = 1;
452
+
453
+ // _buffer: called for each incoming chunk. Track the peak while buffering.
454
+ override async _buffer(chunk: AudioChunk, buffer: ChunkBuffer): Promise<void> {
455
+ await super._buffer(chunk, buffer);
456
+
457
+ for (const channel of chunk.samples) {
458
+ for (const sample of channel) {
459
+ const absolute = Math.abs(sample);
460
+ if (absolute > this.peak) this.peak = absolute;
461
+ }
462
+ }
463
+ }
464
+
465
+ // _process: called once all audio is buffered. Compute the gain scale.
466
+ override _process(_buffer: ChunkBuffer): void {
467
+ this.scale = this.peak === 0 ? 1 : this.properties.ceiling / this.peak;
468
+ }
469
+
470
+ // _unbuffer: called for each chunk emitted from the buffer. Apply the gain.
471
+ override _unbuffer(chunk: AudioChunk): AudioChunk {
472
+ if (this.scale === 1) return chunk;
473
+
474
+ const scaled = chunk.samples.map((channel) => {
475
+ const out = new Float32Array(channel.length);
476
+ for (let i = 0; i < channel.length; i++) {
477
+ out[i] = (channel[i] ?? 0) * this.scale;
478
+ }
479
+ return out;
480
+ });
481
+
482
+ return { samples: scaled, offset: chunk.offset, duration: chunk.duration };
483
+ }
484
+
485
+ clone(overrides?: Partial<NormalizeProperties>): NormalizeModule {
486
+ return new NormalizeModule({ ...this.properties, previousProperties: this.properties, ...overrides });
487
+ }
488
+ }
489
+
490
+ export function normalize(options?: { ceiling?: number }): NormalizeModule {
491
+ return new NormalizeModule({ ceiling: options?.ceiling ?? 1.0 });
492
+ }
493
+ ```
494
+
495
+ ### Source
496
+
497
+ Implement `_init` to return stream metadata (`sampleRate`, `channels`, `duration`), `_read` to produce chunks via the controller, and `_flush` for cleanup.
498
+
499
+ ### Target
500
+
501
+ Implement `_write` to consume each chunk and `_close` to finalize (close file handles, flush buffers).
502
+
503
+ ### FFT Backends
504
+
505
+ Transforms that use spectral processing (STFT/iSTFT) can use native FFT backends for performance. The framework selects a backend based on the stream's `executionProviders` preference:
506
+
507
+ | Backend | Provider | Addon | Description |
508
+ |---------|----------|-------|-------------|
509
+ | VkFFT | `gpu` | [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) | GPU-accelerated FFT via Vulkan |
510
+ | FFTW | `cpu-native` | [fftw-addon](https://github.com/visionsofparadise/fftw-addon) | Native CPU FFT |
511
+ | JavaScript | `cpu` | Built-in | Pure JS fallback, no addon needed |
512
+
513
+ Pass addon paths via module properties (`vkfftAddonPath`, `fftwAddonPath`). Falls back to the built-in JavaScript implementation when no native addon is available.
514
+
515
+ ### ONNX Models
516
+
517
+ ML-based transforms use ONNX Runtime for inference via a native addon. Modules that use ONNX accept:
518
+
519
+ - `onnxAddonPath` — path to the [onnx-runtime-addon](https://github.com/visionsofparadise/onnx-runtime-addon) native binary
520
+ - `modelPath` — path to the `.onnx` model file
521
+
522
+ Models are not bundled with the package. Each module's parameter table links to the expected model source.
523
+
524
+ | Module | Model | Source |
525
+ |--------|-------|--------|
526
+ | DialogueIsolate | Kim_Vocal_2.onnx | [uvr_models](https://huggingface.co/seanghay/uvr_models) |
527
+ | MusicRebalance | htdemucs.onnx + .onnx.data | [demucs](https://github.com/facebookresearch/demucs) |
528
+ | VoiceDenoise | model_1.onnx, model_2.onnx | [DTLN](https://github.com/breizhn/DTLN) |
529
+
530
+ ## License
531
+
532
+ ISC
package/dist/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { SourceNode, validateGraphDefinition, renderGraph } from 'buffered-audio-nodes-core';
6
+
7
+ var program = new Command();
8
+ program.name("buffered-audio-nodes").description("Process audio through buffered audio node pipelines");
9
+ program.command("process").description("Run an async audio processing pipeline").requiredOption("--pipeline <file>", "TypeScript file with default SourceAsyncModule export").option("--chunk-size <samples>", "Chunk size in samples").option("--high-water-mark <count>", "Stream backpressure high water mark").action(async (options) => {
10
+ const pipelinePath = resolve(options.pipeline);
11
+ if (!existsSync(pipelinePath)) {
12
+ process.stderr.write(`Error: pipeline file not found: ${pipelinePath}
13
+ `);
14
+ process.exit(1);
15
+ }
16
+ const { register } = await import('tsx/esm/api');
17
+ const unregister = register();
18
+ try {
19
+ const mod = await import(pipelinePath);
20
+ const source = mod.default;
21
+ if (!(source instanceof SourceNode)) {
22
+ process.stderr.write("Error: default export must be a SourceAsyncModule\n");
23
+ process.exit(1);
24
+ }
25
+ const chunkSize = options.chunkSize ? parseInt(options.chunkSize, 10) : void 0;
26
+ const highWaterMark = options.highWaterMark ? parseInt(options.highWaterMark, 10) : void 0;
27
+ if (chunkSize !== void 0 && (!Number.isFinite(chunkSize) || chunkSize <= 0)) {
28
+ process.stderr.write(`Error: --chunk-size must be a positive integer, got "${options.chunkSize}"
29
+ `);
30
+ process.exit(1);
31
+ }
32
+ if (highWaterMark !== void 0 && (!Number.isFinite(highWaterMark) || highWaterMark <= 0)) {
33
+ process.stderr.write(`Error: --high-water-mark must be a positive integer, got "${options.highWaterMark}"
34
+ `);
35
+ process.exit(1);
36
+ }
37
+ const renderOptions = {
38
+ chunkSize,
39
+ highWaterMark
40
+ };
41
+ process.stdout.write(`Processing pipeline: ${pipelinePath}
42
+ `);
43
+ await source.render(renderOptions);
44
+ process.stdout.write("Done.\n");
45
+ } finally {
46
+ await unregister();
47
+ }
48
+ });
49
+ program.command("render").description("Render a .bag graph definition file").argument("<file>", "Path to .bag file (JSON)").option("--chunk-size <samples>", "Chunk size in samples").option("--high-water-mark <count>", "Stream backpressure high water mark").action(async (file, options) => {
50
+ const bagPath = resolve(file);
51
+ if (!existsSync(bagPath)) {
52
+ process.stderr.write(`Error: file not found: ${bagPath}
53
+ `);
54
+ process.exit(1);
55
+ }
56
+ const json = JSON.parse(readFileSync(bagPath, "utf-8"));
57
+ const definition = validateGraphDefinition(json);
58
+ const { register } = await import('tsx/esm/api');
59
+ const unregister = register();
60
+ try {
61
+ const registry = /* @__PURE__ */ new Map();
62
+ for (const nodeDef of definition.nodes) {
63
+ if (!registry.has(nodeDef.package)) {
64
+ const mod = await import(nodeDef.package);
65
+ const packageMap = /* @__PURE__ */ new Map();
66
+ for (const [key, value] of Object.entries(mod)) {
67
+ if (typeof value === "function") {
68
+ packageMap.set(key, value);
69
+ }
70
+ }
71
+ registry.set(nodeDef.package, packageMap);
72
+ }
73
+ }
74
+ const chunkSize = options.chunkSize ? parseInt(options.chunkSize, 10) : void 0;
75
+ const highWaterMark = options.highWaterMark ? parseInt(options.highWaterMark, 10) : void 0;
76
+ process.stdout.write(`Rendering graph: ${definition.name}
77
+ `);
78
+ await renderGraph(definition, registry, { chunkSize, highWaterMark });
79
+ process.stdout.write("Done.\n");
80
+ } finally {
81
+ await unregister();
82
+ }
83
+ });
84
+ program.parse();