@buffered-audio/nodes 0.0.0 → 0.15.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,566 @@
1
+ # buffered-audio-nodes
2
+
3
+ A streaming audio processing framework. Chainable nodes 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 @buffered-audio/nodes
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Three node 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 "@buffered-audio/nodes";
17
+
18
+ const pipeline = chain(read("input.wav"), normalize({ ceiling: 0.95 }), write("output.wav", { bitDepth: "24" }));
19
+
20
+ await pipeline.render();
21
+ ```
22
+
23
+ `render()` streams audio through the chain. Backpressure, buffering, and lifecycle are handled by the framework.
24
+
25
+ ### Fan-out
26
+
27
+ Split a stream into parallel branches by calling `.to()` multiple times from the same node:
28
+
29
+ ```ts
30
+ import { chain, read, normalize, trim, write } from "@buffered-audio/nodes";
31
+
32
+ const source = read("input.wav");
33
+ const normalizeNode = normalize();
34
+ const trimNode = trim();
35
+
36
+ source.to(normalizeNode);
37
+ source.to(trimNode);
38
+ normalizeNode.to(write("normalized.wav"));
39
+ trimNode.to(write("trimmed.wav"));
40
+
41
+ await source.render();
42
+ ```
43
+
44
+ ## CLI
45
+
46
+ ### `process`
47
+
48
+ Run pipelines from TypeScript files. The file's default export must be a `SourceNode`.
49
+
50
+ ```bash
51
+ npx @buffered-audio/nodes process --pipeline pipeline.ts
52
+ ```
53
+
54
+ ```ts
55
+ // pipeline.ts
56
+ import { chain, read, normalize, trim, write } from "@buffered-audio/nodes";
57
+
58
+ export default chain(read("input.wav"), normalize(), trim({ threshold: -60 }), write("output.wav"));
59
+ ```
60
+
61
+ ### `render`
62
+
63
+ Render a `.bag` (Buffered Audio Graph) file. BAG files are JSON-serialized graph definitions.
64
+
65
+ ```bash
66
+ npx @buffered-audio/nodes render --bag pipeline.bag
67
+ ```
68
+
69
+ | Flag | Description |
70
+ | --------------------------- | ----------------------------------- |
71
+ | `--chunk-size <samples>` | Chunk size in samples |
72
+ | `--high-water-mark <count>` | Stream backpressure high water mark |
73
+
74
+ ## Nodes
75
+
76
+ ### Crest Reduce
77
+
78
+ Content-adaptive, magnitude-preserving, phase-only crest-factor reducer — a pre-limiter headroom stage that rearranges signal phase to flatten true-peak excursions without changing the magnitude spectrum, never increasing crest factor
79
+
80
+ [Source](./src/transforms/crest-reduce/index.ts)
81
+
82
+ | Parameter | Type | Default | Description |
83
+ | --- | --- | --- | --- |
84
+ | `smoothing` | number (min 0) | `100` | Bidirectional (zero-phase) smoothing time constant in ms applied to the per-frame decorrelation envelope before it drives the lattice (default 100 ms). The envelope is 0 in segments with no active-band peak and the per-binding-peak optimal value at active-band peaks; smoothing eases it toward 0 across gaps so the bidirectional pass is predictable. Applied to the CONTROL trajectory only — never the audio path |
85
+ | `frameSize` | number | `2048` | Analysis frame length in samples (default 2048 @ 48 kHz ≈ 43 ms; 75% overlap, Hann analysis window). Whole-file processing — output is produced after the full input is accumulated |
86
+ | `vkfftAddonPath` | string | `""` | VkFFT native addon — GPU FFT acceleration Download: [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) |
87
+ | `fftwAddonPath` | string | `""` | FFTW native addon — CPU FFT acceleration Download: [fftw-addon](https://github.com/visionsofparadise/fftw-addon) |
88
+
89
+ ### Cut
90
+
91
+ Remove a region of audio
92
+
93
+ [Source](./src/transforms/cut/index.ts)
94
+
95
+ | Parameter | Type | Default | Description |
96
+ | --- | --- | --- | --- |
97
+ | `regions` | Object[] | `[]` | Regions |
98
+ | `regions[].start` | number (min 0) | — | Start (seconds) |
99
+ | `regions[].end` | number (min 0) | — | End (seconds) |
100
+
101
+ ### De-Bleed Adaptive
102
+
103
+ Adaptive (MEF FDAF Kalman + MWF + MSAD) reference-based microphone bleed reduction. Stages 1+2 are MEF Meyer-Elshamy-Fingscheidt 2020; Stage 3 is Lukin-Todd 2D NLM+DFTT post-filter.
104
+
105
+ [Source](./src/transforms/de-bleed/index.ts)
106
+
107
+ | Parameter | Type | Default | Description |
108
+ | --- | --- | --- | --- |
109
+ | `references` | string[] | `[]` | References |
110
+ | `reductionStrength` | number (0 to 10, step 0.1) | `5` | Reduction Strength |
111
+ | `artifactSmoothing` | number (0 to 10, step 0.1) | `5` | Artifact Smoothing |
112
+ | `adaptationSpeed` | number (0 to 10, step 0.1) | `3` | Adaptation Speed |
113
+ | `fftSize` | number (512 to 16384, step 256) | `4096` | FFT Size |
114
+ | `hopSize` | number (128 to 4096, step 64) | `1024` | Hop Size |
115
+ | `vkfftAddonPath` | string | `""` | VkFFT native addon — GPU FFT acceleration Download: [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) |
116
+ | `fftwAddonPath` | string | `""` | FFTW native addon — CPU FFT acceleration Download: [fftw-addon](https://github.com/visionsofparadise/fftw-addon) |
117
+ | `dfttBackend` | "" \| "js" \| "fftw" \| "vkfft" | `""` | DFTT Backend Override |
118
+
119
+ ### DeepFilterNet3 (Denoiser)
120
+
121
+ Remove background noise from speech using DeepFilterNet3 (48 kHz full-band CRN)
122
+
123
+ [Source](./src/transforms/deep-filter-net-3/index.ts)
124
+
125
+ | Parameter | Type | Default | Description |
126
+ | --- | --- | --- | --- |
127
+ | `modelPath` | string | `""` | DeepFilterNet3 48 kHz denoiser model (.onnx) Download: [dfn3](https://github.com/yuyun2000/SpeechDenoiser) |
128
+ | `ffmpegPath` | string | `""` | FFmpeg — only used when sampleRate ≠ 48000 to chain up/down resamplers around the inference stream; can be left blank when sampleRate === 48000. Download: [ffmpeg](https://ffmpeg.org/download.html) |
129
+ | `onnxAddonPath` | string | `""` | ONNX Runtime native addon Download: [onnx-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
130
+ | `sampleRate` | number (min 0) | — | Source audio sample rate in Hz. Required. When ≠ 48000, ffmpeg resampling is chained around the inference stream via _setup composition. |
131
+ | `attenuation` | number (0 to 100) | `30` | Attenuation cap in dB. Maps to the ONNX `atten_lim_db` input; 0 = no cap |
132
+
133
+ ### Dither
134
+
135
+ Add shaped noise to reduce quantization distortion
136
+
137
+ [Source](./src/transforms/dither/index.ts)
138
+
139
+ | Parameter | Type | Default | Description |
140
+ | --- | --- | --- | --- |
141
+ | `bitDepth` | 16 \| 24 | `16` | Bit Depth |
142
+ | `noiseShaping` | boolean | `false` | Noise Shaping |
143
+
144
+ ### Downmix Mono
145
+
146
+ Mix all input channels to a single mono channel by averaging
147
+
148
+ [Source](./src/transforms/downmix-mono/index.ts)
149
+
150
+ ### DTLN (Denoiser)
151
+
152
+ Remove background noise from speech using DTLN neural network
153
+
154
+ [Source](./src/transforms/dtln/index.ts)
155
+
156
+ | Parameter | Type | Default | Description |
157
+ | --- | --- | --- | --- |
158
+ | `modelPath1` | string | `""` | DTLN magnitude mask model (.onnx) Download: [dtln-model_1](https://github.com/breizhn/DTLN) |
159
+ | `modelPath2` | string | `""` | DTLN time-domain model (.onnx) Download: [dtln-model_2](https://github.com/breizhn/DTLN) |
160
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
161
+ | `onnxAddonPath` | string | `""` | ONNX Runtime native addon Download: [onnx-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
162
+ | `vkfftAddonPath` | string | `""` | VkFFT native addon — GPU FFT acceleration Download: [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) |
163
+ | `fftwAddonPath` | string | `""` | FFTW native addon — CPU FFT acceleration Download: [fftw-addon](https://github.com/visionsofparadise/fftw-addon) |
164
+
165
+ ### Duplicate Channels
166
+
167
+ Duplicate a mono signal into multiple identical output channels
168
+
169
+ [Source](./src/transforms/duplicate-channels/index.ts)
170
+
171
+ | Parameter | Type | Default | Description |
172
+ | --- | --- | --- | --- |
173
+ | `channels` | number (2 to 8) | `2` | Output channel count |
174
+
175
+ ### FFmpeg
176
+
177
+ Process audio through FFmpeg filters
178
+
179
+ [Source](./src/transforms/ffmpeg/index.ts)
180
+
181
+ | Parameter | Type | Default | Description |
182
+ | --- | --- | --- | --- |
183
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
184
+ | `args` | string[] | `[]` | |
185
+ | `outputSampleRate` | number (min 0), optional | — | Sample rate of emitted chunks. Required when args change the rate (e.g. -af aresample=24000). |
186
+
187
+ ### Gain
188
+
189
+ Adjust signal level by a fixed amount in dB
190
+
191
+ [Source](./src/transforms/gain/index.ts)
192
+
193
+ | Parameter | Type | Default | Description |
194
+ | --- | --- | --- | --- |
195
+ | `gain` | number (-60 to 24, step 0.1) | `0` | Gain (dB) |
196
+
197
+ ### HTDemucs (Stem Separator)
198
+
199
+ Rebalance stem volumes using HTDemucs source separation
200
+
201
+ [Source](./src/transforms/htdemucs/index.ts)
202
+
203
+ | Parameter | Type | Default | Description |
204
+ | --- | --- | --- | --- |
205
+ | `modelPath` | string | `""` | HTDemucs source separation model (.onnx) — requires .onnx.data file alongside Download: [htdemucs](https://github.com/facebookresearch/demucs) |
206
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
207
+ | `onnxAddonPath` | string | `""` | ONNX Runtime native addon Download: [onnx-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
208
+ | `highPass` | number (0 to 500, step 10) | `0` | High Pass |
209
+ | `lowPass` | number (0 to 22050, step 100) | `0` | Low Pass |
210
+
211
+ ### Kim Vocal 2 (Stem Separator)
212
+
213
+ Isolate dialogue from background using MDX-Net vocal separation
214
+
215
+ [Source](./src/transforms/kim-vocal-2/index.ts)
216
+
217
+ | Parameter | Type | Default | Description |
218
+ | --- | --- | --- | --- |
219
+ | `modelPath` | string | `""` | MDX-Net vocal isolation model (.onnx) Download: [Kim_Vocal_2](https://huggingface.co/seanghay/uvr_models) |
220
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
221
+ | `onnxAddonPath` | string | `""` | ONNX Runtime native addon Download: [onnx-addon](https://github.com/visionsofparadise/onnx-runtime-addon) |
222
+ | `highPass` | number (20 to 500, step 10) | `80` | High Pass |
223
+ | `lowPass` | number (1000 to 22050, step 100) | `20000` | Low Pass |
224
+
225
+ ### Loudness Normalize
226
+
227
+ Measure integrated loudness (BS.1770) and apply a single linear gain to hit a target LUFS — no limiting, no dynamics
228
+
229
+ [Source](./src/transforms/loudness-normalize/index.ts)
230
+
231
+ | Parameter | Type | Default | Description |
232
+ | --- | --- | --- | --- |
233
+ | `target` | number (-50 to 0, step 0.1) | `-16` | Target integrated loudness (LUFS) |
234
+
235
+ ### Loudness Stats
236
+
237
+ Measure integrated loudness, true peak, and loudness range per EBU R128, plus an amplitude-distribution histogram
238
+
239
+ [Source](./src/targets/loudness-stats/index.ts)
240
+
241
+ | Parameter | Type | Default | Description |
242
+ | --- | --- | --- | --- |
243
+ | `bucketCount` | number (min 0) | `1024` | Amplitude histogram bucket count |
244
+ | `outputPath` | string | `""` | Output Path (JSON sidecar). Empty string disables file output. |
245
+
246
+ ### Loudness Target
247
+
248
+ Peak-aware content-adaptive curve fitting (LUFS, true-peak, LRA) via a single combined gain envelope with a peak-respecting two-stage smoother. The upper-arm peak anchor jointly iterates with the body gain to land both LUFS and true-peak targets in one envelope.
249
+
250
+ [Source](./src/transforms/loudness-target/index.ts)
251
+
252
+ | Parameter | Type | Default | Description |
253
+ | --- | --- | --- | --- |
254
+ | `targetLufs` | number (-50 to 0, step 0.1) | `-16` | Target integrated loudness (LUFS) |
255
+ | `pivot` | number (max 0), optional | — | Body anchor (dB). Default: median(considered LRA blocks) from BS.1770 LRA gating in pass 1. |
256
+ | `floor` | number (max 0), optional | — | Silence threshold (dB). Default: min(considered LRA blocks); no floor when no blocks survive gating. |
257
+ | `limitPercentile` | number (0.5 to 1) | `0.995` | Top-1−p fraction of detection samples to brick-wall. Default 0.995 brick-walls the top 0.5%. |
258
+ | `limitDb` | number (max 0), optional | — | Limit-anchor override (dB). Default: auto-derived from quantile(detection histogram, limitPercentile). Set explicitly to fix the limit anchor. |
259
+ | `maxAttempts` | number (min 1) | `10` | Hard cap on iteration attempts. |
260
+ | `targetTp` | number (max 0), optional | — | True-peak target (dBTP). Default: source true peak (peaks unchanged). |
261
+ | `smoothing` | number (0.01 to 200) | `1` | Peak-respecting envelope time constant (ms). |
262
+ | `tolerance` | number (min 0) | `0.5` | Iteration exit threshold (LUFS dB). |
263
+ | `peakTolerance` | number (min 0) | `0.1` | One-sided iteration exit threshold for output true-peak overshoot (dBTP; ceiling — undershoot ignored). |
264
+
265
+ ### Normalize
266
+
267
+ Adjust peak or loudness level to a target ceiling
268
+
269
+ [Source](./src/transforms/normalize/index.ts)
270
+
271
+ | Parameter | Type | Default | Description |
272
+ | --- | --- | --- | --- |
273
+ | `ceiling` | number (0 to 1, step 0.01) | `1` | Ceiling |
274
+
275
+ ### Pad
276
+
277
+ Add silence to start or end of audio
278
+
279
+ [Source](./src/transforms/pad/index.ts)
280
+
281
+ | Parameter | Type | Default | Description |
282
+ | --- | --- | --- | --- |
283
+ | `before` | number (min 0, step 0.001) | `0` | Before |
284
+ | `after` | number (min 0, step 0.001) | `0` | After |
285
+
286
+ ### Pan
287
+
288
+ Position mono signal in stereo field or adjust stereo balance
289
+
290
+ [Source](./src/transforms/pan/index.ts)
291
+
292
+ | Parameter | Type | Default | Description |
293
+ | --- | --- | --- | --- |
294
+ | `pan` | number (-1 to 1, step 0.01) | `0` | Pan (-1 = full left, 0 = center, 1 = full right) |
295
+
296
+ ### Phase
297
+
298
+ Invert or rotate signal phase
299
+
300
+ [Source](./src/transforms/phase/index.ts)
301
+
302
+ | Parameter | Type | Default | Description |
303
+ | --- | --- | --- | --- |
304
+ | `invert` | boolean | `true` | Invert |
305
+ | `angle` | number (-180 to 180, step 1), optional | — | Angle |
306
+
307
+ ### Read
308
+
309
+ Read audio from a file
310
+
311
+ [Source](./src/sources/read/index.ts)
312
+
313
+ | Parameter | Type | Default | Description |
314
+ | --- | --- | --- | --- |
315
+ | `path` | string | `""` | |
316
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
317
+ | `ffprobePath` | string | `""` | FFprobe — media file analyzer (included with FFmpeg) Download: [ffprobe](https://ffmpeg.org/download.html) |
318
+
319
+ ### Read FFmpeg
320
+
321
+ Read audio from a file using FFmpeg
322
+
323
+ [Source](./src/sources/read/ffmpeg/index.ts)
324
+
325
+ | Parameter | Type | Default | Description |
326
+ | --- | --- | --- | --- |
327
+ | `path` | string | `""` | |
328
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
329
+ | `ffprobePath` | string | `""` | FFprobe — media file analyzer (included with FFmpeg) Download: [ffprobe](https://ffmpeg.org/download.html) |
330
+
331
+ ### Read WAV
332
+
333
+ Read audio from a WAV file
334
+
335
+ [Source](./src/sources/read/wav/index.ts)
336
+
337
+ | Parameter | Type | Default | Description |
338
+ | --- | --- | --- | --- |
339
+ | `path` | string | `""` | |
340
+
341
+ ### Reverse
342
+
343
+ Reverse audio playback direction
344
+
345
+ [Source](./src/transforms/reverse/index.ts)
346
+
347
+ ### Spectrogram
348
+
349
+ Generate spectrogram visualization data
350
+
351
+ [Source](./src/targets/spectrogram/index.ts)
352
+
353
+ | Parameter | Type | Default | Description |
354
+ | --- | --- | --- | --- |
355
+ | `outputPath` | string | `""` | Output Path |
356
+ | `fftSize` | number (256 to 8192, step 256) | `2048` | FFT Size |
357
+ | `hopSize` | number (64 to 8192, step 64) | `512` | Hop Size |
358
+ | `fftwAddonPath` | string | `""` | FFTW Addon |
359
+
360
+ ### Splice
361
+
362
+ Replace a region of audio with processed content
363
+
364
+ [Source](./src/transforms/splice/index.ts)
365
+
366
+ | Parameter | Type | Default | Description |
367
+ | --- | --- | --- | --- |
368
+ | `insertPath` | string | `""` | Insert File Path |
369
+ | `insertAt` | number (min 0) | `0` | Insert At (frames) |
370
+
371
+ ### Trim
372
+
373
+ Remove silence from start and end
374
+
375
+ [Source](./src/transforms/trim/index.ts)
376
+
377
+ | Parameter | Type | Default | Description |
378
+ | --- | --- | --- | --- |
379
+ | `threshold` | number (0 to 1, step 0.001) | `0.001` | Threshold |
380
+ | `margin` | number (0 to 1, step 0.001) | `0.01` | Margin |
381
+ | `start` | boolean | `true` | Start |
382
+ | `end` | boolean | `true` | End |
383
+
384
+ ### True Peak Normalize
385
+
386
+ Measure source true peak (4× upsampled, BS.1770-4 style) and apply a single linear gain to hit a target dBTP
387
+
388
+ [Source](./src/transforms/true-peak-normalize/index.ts)
389
+
390
+ | Parameter | Type | Default | Description |
391
+ | --- | --- | --- | --- |
392
+ | `target` | number (max 0) | `-1` | Target true peak (dBTP). Must be < 0. |
393
+
394
+ ### VST3
395
+
396
+ Host a chain of VST3 effect plugins via Pedalboard (whole-file offline mode)
397
+
398
+ [Source](./src/transforms/vst3/index.ts)
399
+
400
+ | Parameter | Type | Default | Description |
401
+ | --- | --- | --- | --- |
402
+ | `vstHostPath` | string | `""` | vst-host — Pedalboard-based VST3 host CLI Download: [vst-host](https://github.com/visionsofparadise/vst-host) |
403
+ | `stages` | Object[] | — | Ordered chain of plugin/preset stages — processed end-to-end inside one Pedalboard offline call |
404
+ | `stages[].pluginPath` | string | — | VST3 plugin file or bundle |
405
+ | `stages[].pluginName` | string, optional | — | Sub-plugin name when pluginPath is a multi-plugin shell (e.g. WaveShell) |
406
+ | `stages[].presetPath` | string, optional | — | Optional .vstpreset state file applied after the plugin loads |
407
+ | `stages[].parameters` | record, optional | — | Optional parameter overrides applied after presetPath. Keys map to Pedalboard parameter names exposed by the plugin. |
408
+ | `bypass` | boolean | `false` | Pass audio through unchanged (no subprocess spawn) |
409
+
410
+ ### Waveform
411
+
412
+ Generate waveform visualization data
413
+
414
+ [Source](./src/targets/waveform/index.ts)
415
+
416
+ | Parameter | Type | Default | Description |
417
+ | --- | --- | --- | --- |
418
+ | `outputPath` | string | `""` | Output Path |
419
+ | `resolution` | number (100 to 10000, step 100) | `1000` | Resolution |
420
+
421
+ ### Write
422
+
423
+ Write audio to a file
424
+
425
+ [Source](./src/targets/write/index.ts)
426
+
427
+ | Parameter | Type | Default | Description |
428
+ | --- | --- | --- | --- |
429
+ | `path` | string | `""` | |
430
+ | `ffmpegPath` | string | `""` | FFmpeg — audio/video processing tool Download: [ffmpeg](https://ffmpeg.org/download.html) |
431
+ | `bitDepth` | "16" \| "24" \| "32" \| "32f" | `"16"` | |
432
+ | `encoding` | Object, optional | — | Encode through ffmpeg to a non-WAV format. Requires `ffmpegPath`. |
433
+ | `encoding.format` | "wav" \| "flac" \| "mp3" \| "aac" | — | |
434
+ | `encoding.bitrate` | string, optional | — | |
435
+ | `encoding.vbr` | number, optional | — | |
436
+ | `encoding.sampleRate` | number (min 0), optional | — | Output sample rate (Hz). When set, ffmpeg resamples on encode. |
437
+
438
+ ## Creating Nodes
439
+
440
+ Each node has two parts: a **Node** (inert descriptor) and a **Stream** (stateful runtime instance). Nodes are defined once and describe the transform. Streams are created fresh per render and hold the mutable processing state.
441
+
442
+ Extend `TransformNode` from `@buffered-audio/core` and create a companion `BufferedTransformStream`. The node's `createStream()` method produces a new stream instance for each render.
443
+
444
+ ### Stream Hooks
445
+
446
+ - **`_buffer(chunk, buffer)`** — called for each incoming chunk. Override to inspect or modify data as it's buffered. Default appends to the buffer.
447
+ - **`_process(buffer)`** — called once the buffer reaches `bufferSize`. Use this for analysis or in-place modification of the full buffer.
448
+ - **`_unbuffer(chunk)`** — called for each chunk emitted from the buffer. Transform or replace the chunk here. Return `undefined` to drop it.
449
+ - **`_teardown()`** — cleanup after render completes. Close file handles, free native resources, release ONNX sessions. Called automatically on all streams.
450
+
451
+ ### Buffer Size Modes
452
+
453
+ - `0` — pass-through. Each chunk flows through `_unbuffer` immediately.
454
+ - `N` — block mode. Chunks accumulate until `N` frames are collected, then `_process` runs and `_unbuffer` emits the result.
455
+ - `WHOLE_FILE` (`Infinity`) — full-file. All audio is buffered before `_process` and `_unbuffer` run.
456
+
457
+ ### Example: Normalize
458
+
459
+ ```ts
460
+ import { z } from "zod";
461
+ import { BufferedTransformStream, TransformNode, WHOLE_FILE, type AudioChunk, type ChunkBuffer, type TransformNodeProperties } from "@buffered-audio/core";
462
+
463
+ const schema = z.object({
464
+ ceiling: z.number().min(0).max(1).multipleOf(0.01).default(1.0).describe("Ceiling"),
465
+ });
466
+
467
+ interface NormalizeProperties extends z.infer<typeof schema>, TransformNodeProperties {}
468
+
469
+ class NormalizeStream extends BufferedTransformStream<NormalizeProperties> {
470
+ private peak = 0;
471
+ private scale = 1;
472
+
473
+ override async _buffer(chunk: AudioChunk, buffer: ChunkBuffer): Promise<void> {
474
+ await super._buffer(chunk, buffer);
475
+
476
+ for (let ch = 0; ch < chunk.samples.length; ch++) {
477
+ const channel = chunk.samples[ch] ?? new Float32Array(0);
478
+
479
+ for (let si = 0; si < channel.length; si++) {
480
+ const absolute = Math.abs(channel[si] ?? 0);
481
+
482
+ if (Number.isFinite(absolute) && absolute > this.peak) this.peak = absolute;
483
+ }
484
+ }
485
+ }
486
+
487
+ override _process(_buffer: ChunkBuffer): void {
488
+ const raw = this.peak === 0 ? 1 : this.properties.ceiling / this.peak;
489
+
490
+ this.scale = Number.isFinite(raw) ? raw : 1;
491
+ }
492
+
493
+ override _unbuffer(chunk: AudioChunk): AudioChunk {
494
+ if (this.scale === 1) return chunk;
495
+
496
+ const scaledSamples = chunk.samples.map((channel) => {
497
+ const scaled = new Float32Array(channel.length);
498
+
499
+ for (let index = 0; index < channel.length; index++) {
500
+ scaled[index] = (channel[index] ?? 0) * this.scale;
501
+ }
502
+
503
+ return scaled;
504
+ });
505
+
506
+ return { samples: scaledSamples, offset: chunk.offset, sampleRate: chunk.sampleRate, bitDepth: chunk.bitDepth };
507
+ }
508
+ }
509
+
510
+ class NormalizeNode extends TransformNode<NormalizeProperties> {
511
+ static override readonly moduleName = "Normalize";
512
+ static override readonly packageName = "buffered-audio-nodes";
513
+ static override readonly moduleDescription = "Adjust peak or loudness level to a target ceiling";
514
+ static override readonly schema = schema;
515
+
516
+ override readonly type = ["buffered-audio-node", "transform", "normalize"] as const;
517
+
518
+ constructor(properties: NormalizeProperties) {
519
+ super({ bufferSize: WHOLE_FILE, latency: WHOLE_FILE, ...properties });
520
+ }
521
+
522
+ override createStream(): NormalizeStream {
523
+ return new NormalizeStream({ ...this.properties, bufferSize: this.bufferSize, overlap: this.properties.overlap ?? 0 });
524
+ }
525
+
526
+ override clone(overrides?: Partial<NormalizeProperties>): NormalizeNode {
527
+ return new NormalizeNode({ ...this.properties, previousProperties: this.properties, ...overrides });
528
+ }
529
+ }
530
+
531
+ export function normalize(options?: { ceiling?: number; id?: string }): NormalizeNode {
532
+ return new NormalizeNode({ ceiling: options?.ceiling ?? 1.0, id: options?.id });
533
+ }
534
+ ```
535
+
536
+ ## FFT Backends
537
+
538
+ 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:
539
+
540
+ | Backend | Provider | Addon | Description |
541
+ | ---------- | ------------ | --------------------------------------------------------------- | --------------------------------- |
542
+ | VkFFT | `gpu` | [vkfft-addon](https://github.com/visionsofparadise/vkfft-addon) | GPU-accelerated FFT via Vulkan |
543
+ | FFTW | `cpu-native` | [fftw-addon](https://github.com/visionsofparadise/fftw-addon) | Native CPU FFT |
544
+ | JavaScript | `cpu` | Built-in | Pure JS fallback, no addon needed |
545
+
546
+ Pass addon paths via node properties (`vkfftAddonPath`, `fftwAddonPath`). Falls back to the built-in JavaScript implementation when no native addon is available.
547
+
548
+ ## ONNX Models
549
+
550
+ ML-based transforms use ONNX Runtime for inference via a native addon. Nodes that use ONNX accept:
551
+
552
+ - `onnxAddonPath` — path to the [onnx-runtime-addon](https://github.com/visionsofparadise/onnx-runtime-addon) native binary
553
+ - `modelPath` — path to the `.onnx` model file
554
+
555
+ Models are not bundled with the package. Each node's parameter table links to the expected model source.
556
+
557
+ | Node | Model | Source |
558
+ | --------------- | -------------------------------- | --------------------------------------------------------------------------------- |
559
+ | dtln | model_1.onnx, model_2.onnx | [DTLN](https://github.com/breizhn/DTLN) |
560
+ | deepFilterNet3 | dfn3.onnx | [SpeechDenoiser](https://github.com/yuyun2000/SpeechDenoiser) |
561
+ | kimVocal2 | Kim_Vocal_2.onnx | [uvr_models](https://huggingface.co/seanghay/uvr_models) |
562
+ | htdemucs | htdemucs.onnx + htdemucs.onnx.data | [demucs](https://github.com/facebookresearch/demucs) |
563
+
564
+ ## License
565
+
566
+ ISC
package/dist/cli.js ADDED
@@ -0,0 +1,85 @@
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/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.packageName)) {
64
+ const mod = await import(nodeDef.packageName);
65
+ const packageMap = /* @__PURE__ */ new Map();
66
+ for (const value of Object.values(mod)) {
67
+ if (typeof value !== "function") continue;
68
+ const ctor = value;
69
+ if (typeof ctor.moduleName !== "string") continue;
70
+ packageMap.set(ctor.moduleName, ctor);
71
+ }
72
+ registry.set(nodeDef.packageName, packageMap);
73
+ }
74
+ }
75
+ const chunkSize = options.chunkSize ? parseInt(options.chunkSize, 10) : void 0;
76
+ const highWaterMark = options.highWaterMark ? parseInt(options.highWaterMark, 10) : void 0;
77
+ process.stdout.write(`Rendering graph: ${definition.name}
78
+ `);
79
+ await renderGraph(definition, registry, { chunkSize, highWaterMark });
80
+ process.stdout.write("Done.\n");
81
+ } finally {
82
+ await unregister();
83
+ }
84
+ });
85
+ program.parse();