@buffered-audio/nodes 0.16.0 → 0.18.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 +23 -22
- package/dist/cli.js +109 -11
- package/dist/index.d.ts +198 -563
- package/dist/index.js +2815 -2955
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -63,13 +63,27 @@ export default chain(read("input.wav"), normalize(), trim({ threshold: -60 }), w
|
|
|
63
63
|
Render a `.bag` (Buffered Audio Graph) file. BAG files are JSON-serialized graph definitions.
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
npx @buffered-audio/nodes render
|
|
66
|
+
npx @buffered-audio/nodes render pipeline.bag
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
| Flag | Description
|
|
70
|
-
| --------------------------- |
|
|
71
|
-
| `--chunk-size <samples>` | Chunk size in samples
|
|
72
|
-
| `--high-water-mark <count>` | Stream backpressure high water mark
|
|
69
|
+
| Flag | Description |
|
|
70
|
+
| --------------------------- | ----------------------------------------------------------- |
|
|
71
|
+
| `--chunk-size <samples>` | Chunk size in samples |
|
|
72
|
+
| `--high-water-mark <count>` | Stream backpressure high water mark |
|
|
73
|
+
| `--param <name=value>` | Bind a `{{name}}` template placeholder (repeatable) |
|
|
74
|
+
|
|
75
|
+
#### Template parameters
|
|
76
|
+
|
|
77
|
+
String values in a bag's node `parameters` may contain `{{name}}` placeholders. Each `--param name=value` binds one for that render; the same bag renders different inputs and outputs without editing the file. Every placeholder must be bound, and every `--param` must match a placeholder, or the render fails before any audio work.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "id": "a", "packageName": "@buffered-audio/nodes", "packageVersion": "0.16.0", "nodeName": "Read", "parameters": { "path": "{{episode}}/raw.wav" } }
|
|
81
|
+
{ "id": "z", "packageName": "@buffered-audio/nodes", "packageVersion": "0.16.0", "nodeName": "Write", "parameters": { "path": "{{episode}}/master.wav" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx @buffered-audio/nodes render master.bag --param episode=./e260
|
|
86
|
+
```
|
|
73
87
|
|
|
74
88
|
## Nodes
|
|
75
89
|
|
|
@@ -164,7 +178,7 @@ Remove background noise from speech using DTLN neural network
|
|
|
164
178
|
|
|
165
179
|
### Duplicate Channels
|
|
166
180
|
|
|
167
|
-
Duplicate a mono signal into multiple identical output channels
|
|
181
|
+
Duplicate a mono signal into multiple identical output channels; requires exactly 1 input channel, throws otherwise
|
|
168
182
|
|
|
169
183
|
[Source](./src/transforms/duplicate-channels/index.ts)
|
|
170
184
|
|
|
@@ -285,7 +299,7 @@ Add silence to start or end of audio
|
|
|
285
299
|
|
|
286
300
|
### Pan
|
|
287
301
|
|
|
288
|
-
Position mono signal in stereo field or adjust stereo balance
|
|
302
|
+
Position mono signal in stereo field or adjust stereo balance; throws for inputs with more than 2 channels
|
|
289
303
|
|
|
290
304
|
[Source](./src/transforms/pan/index.ts)
|
|
291
305
|
|
|
@@ -304,18 +318,6 @@ Invert or rotate signal phase
|
|
|
304
318
|
| `invert` | boolean | `true` | Invert |
|
|
305
319
|
| `angle` | number (-180 to 180, step 1), optional | — | Angle |
|
|
306
320
|
|
|
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
321
|
### Read FFmpeg
|
|
320
322
|
|
|
321
323
|
Read audio from a file using FFmpeg
|
|
@@ -404,7 +406,6 @@ Host a chain of VST3 effect plugins via Pedalboard (whole-file offline mode)
|
|
|
404
406
|
| `stages[].pluginPath` | string | — | VST3 plugin file or bundle |
|
|
405
407
|
| `stages[].pluginName` | string, optional | — | Sub-plugin name when pluginPath is a multi-plugin shell (e.g. WaveShell) |
|
|
406
408
|
| `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
409
|
| `bypass` | boolean | `false` | Pass audio through unchanged (no subprocess spawn) |
|
|
409
410
|
|
|
410
411
|
### Waveform
|
|
@@ -508,9 +509,9 @@ class NormalizeStream extends BufferedTransformStream<NormalizeProperties> {
|
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
class NormalizeNode extends TransformNode<NormalizeProperties> {
|
|
511
|
-
static override readonly
|
|
512
|
+
static override readonly nodeName = "Normalize";
|
|
512
513
|
static override readonly packageName = "buffered-audio-nodes";
|
|
513
|
-
static override readonly
|
|
514
|
+
static override readonly nodeDescription = "Adjust peak or loudness level to a target ceiling";
|
|
514
515
|
static override readonly schema = schema;
|
|
515
516
|
|
|
516
517
|
override readonly type = ["buffered-audio-node", "transform", "normalize"] as const;
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,95 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { existsSync, readFileSync } from 'fs';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
|
-
import { SourceNode, validateGraphDefinition,
|
|
5
|
+
import { SourceNode, validateGraphDefinition, createRenderJobs, BufferedSourceStream } from '@buffered-audio/core';
|
|
6
6
|
|
|
7
|
+
var labelOf = (node) => node.id ? `${node.nodeName}#${node.id}` : node.nodeName;
|
|
8
|
+
function sourceLabel(job) {
|
|
9
|
+
for (const [node, streams] of job.streams) {
|
|
10
|
+
if (streams.some((stream) => stream instanceof BufferedSourceStream)) {
|
|
11
|
+
return labelOf({ nodeName: node.constructor.nodeName, id: node.id });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
var collect = (value, previous) => [...previous, value];
|
|
17
|
+
function parseParams(entries) {
|
|
18
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const separatorIndex = entry.indexOf("=");
|
|
21
|
+
if (separatorIndex === -1) {
|
|
22
|
+
process.stderr.write(`Error: --param must be in name=value form, got "${entry}"
|
|
23
|
+
`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const name = entry.slice(0, separatorIndex);
|
|
27
|
+
const value = entry.slice(separatorIndex + 1);
|
|
28
|
+
if (name === "") {
|
|
29
|
+
process.stderr.write(`Error: --param name must not be empty, got "${entry}"
|
|
30
|
+
`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (parameters.has(name)) {
|
|
34
|
+
process.stderr.write(`Error: --param ${name} given more than once
|
|
35
|
+
`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
parameters.set(name, value);
|
|
39
|
+
}
|
|
40
|
+
return Object.fromEntries(parameters);
|
|
41
|
+
}
|
|
42
|
+
function createEventSink() {
|
|
43
|
+
const totals = /* @__PURE__ */ new Map();
|
|
44
|
+
const subscribe = (events) => {
|
|
45
|
+
events.on("started", (node) => {
|
|
46
|
+
process.stdout.write(`[${labelOf(node)}] started
|
|
47
|
+
`);
|
|
48
|
+
});
|
|
49
|
+
events.on("progress", (node, payload) => {
|
|
50
|
+
const label = labelOf(node);
|
|
51
|
+
if (payload.framesTotal !== void 0) {
|
|
52
|
+
const percent = Math.round(payload.framesDone / payload.framesTotal * 100);
|
|
53
|
+
process.stdout.write(`[${label}] ${payload.phase} ${percent}%
|
|
54
|
+
`);
|
|
55
|
+
} else {
|
|
56
|
+
process.stdout.write(`[${label}] ${payload.phase} frames=${payload.framesDone}
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
events.on("log", (node, payload) => {
|
|
61
|
+
const data = payload.data ? Object.entries(payload.data).map(([key, value]) => `${key}=${String(value)}`) : [];
|
|
62
|
+
const parts = [payload.message, ...data].join(" ");
|
|
63
|
+
const prefix = payload.level === "warn" ? "warn: " : "";
|
|
64
|
+
process.stdout.write(`${prefix}[${labelOf(node)}] ${parts}
|
|
65
|
+
`);
|
|
66
|
+
});
|
|
67
|
+
events.on("finished", (node, payload) => {
|
|
68
|
+
totals.set(node, { framesDone: payload.framesDone, processingMs: payload.processingMs });
|
|
69
|
+
process.stdout.write(`[${labelOf(node)}] finished
|
|
70
|
+
`);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const printSummary = (jobs) => {
|
|
74
|
+
for (const [node, { framesDone, processingMs }] of totals) {
|
|
75
|
+
const label = labelOf(node);
|
|
76
|
+
if (processingMs !== void 0) {
|
|
77
|
+
process.stdout.write(`[${label}] processed ${framesDone} frames in ${Math.round(processingMs)}ms
|
|
78
|
+
`);
|
|
79
|
+
} else {
|
|
80
|
+
process.stdout.write(`[${label}] processed ${framesDone} frames
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const job of jobs) {
|
|
85
|
+
const timing = job.timing;
|
|
86
|
+
if (!timing) continue;
|
|
87
|
+
const label = sourceLabel(job) ?? "source";
|
|
88
|
+
process.stdout.write(`[${label}] total ${(timing.totalMs / 1e3).toFixed(1)}s, ${timing.realTimeMultiplier.toFixed(1)}x RT
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return { subscribe, printSummary };
|
|
93
|
+
}
|
|
7
94
|
var program = new Command();
|
|
8
95
|
program.name("buffered-audio-nodes").description("Process audio through buffered audio node pipelines");
|
|
9
96
|
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) => {
|
|
@@ -34,19 +121,19 @@ program.command("process").description("Run an async audio processing pipeline")
|
|
|
34
121
|
`);
|
|
35
122
|
process.exit(1);
|
|
36
123
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
124
|
+
const sink = createEventSink();
|
|
125
|
+
const job = source.createRenderJob({ chunkSize, highWaterMark });
|
|
126
|
+
sink.subscribe(job.events);
|
|
41
127
|
process.stdout.write(`Processing pipeline: ${pipelinePath}
|
|
42
128
|
`);
|
|
43
|
-
await
|
|
129
|
+
await job.render();
|
|
130
|
+
sink.printSummary([job]);
|
|
44
131
|
process.stdout.write("Done.\n");
|
|
45
132
|
} finally {
|
|
46
133
|
await unregister();
|
|
47
134
|
}
|
|
48
135
|
});
|
|
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) => {
|
|
136
|
+
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").option("--param <name=value>", "Bind a {{name}} template placeholder in the bag (repeatable)", collect, []).action(async (file, options) => {
|
|
50
137
|
const bagPath = resolve(file);
|
|
51
138
|
if (!existsSync(bagPath)) {
|
|
52
139
|
process.stderr.write(`Error: file not found: ${bagPath}
|
|
@@ -66,18 +153,29 @@ program.command("render").description("Render a .bag graph definition file").arg
|
|
|
66
153
|
for (const value of Object.values(mod)) {
|
|
67
154
|
if (typeof value !== "function") continue;
|
|
68
155
|
const ctor = value;
|
|
69
|
-
if (typeof ctor.
|
|
70
|
-
packageMap.set(ctor.
|
|
156
|
+
if (typeof ctor.nodeName !== "string") continue;
|
|
157
|
+
packageMap.set(ctor.nodeName, ctor);
|
|
71
158
|
}
|
|
72
159
|
registry.set(nodeDef.packageName, packageMap);
|
|
73
160
|
}
|
|
74
161
|
}
|
|
75
162
|
const chunkSize = options.chunkSize ? parseInt(options.chunkSize, 10) : void 0;
|
|
76
163
|
const highWaterMark = options.highWaterMark ? parseInt(options.highWaterMark, 10) : void 0;
|
|
164
|
+
const parameters = parseParams(options.param);
|
|
165
|
+
const sink = createEventSink();
|
|
77
166
|
process.stdout.write(`Rendering graph: ${definition.name}
|
|
78
167
|
`);
|
|
79
|
-
|
|
80
|
-
|
|
168
|
+
try {
|
|
169
|
+
const jobs = createRenderJobs(definition, registry, { chunkSize, highWaterMark, parameters });
|
|
170
|
+
for (const job of jobs) sink.subscribe(job.events);
|
|
171
|
+
await Promise.all(jobs.map((job) => job.render()));
|
|
172
|
+
sink.printSummary(jobs);
|
|
173
|
+
process.stdout.write("Done.\n");
|
|
174
|
+
} catch (error) {
|
|
175
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
176
|
+
`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
81
179
|
} finally {
|
|
82
180
|
await unregister();
|
|
83
181
|
}
|