@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 +566 -0
- package/dist/cli.js +85 -0
- package/dist/index.d.ts +1557 -0
- package/dist/index.js +37589 -0
- package/package.json +52 -3
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();
|