@buffered-audio/nodes 0.16.0 → 0.17.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 -10
- package/dist/cli.js +100 -6
- package/dist/index.d.ts +90 -398
- package/dist/index.js +1218 -1176
- package/package.json +2 -2
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
|
|
|
@@ -404,7 +418,6 @@ Host a chain of VST3 effect plugins via Pedalboard (whole-file offline mode)
|
|
|
404
418
|
| `stages[].pluginPath` | string | — | VST3 plugin file or bundle |
|
|
405
419
|
| `stages[].pluginName` | string, optional | — | Sub-plugin name when pluginPath is a multi-plugin shell (e.g. WaveShell) |
|
|
406
420
|
| `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
421
|
| `bypass` | boolean | `false` | Pass audio through unchanged (no subprocess spawn) |
|
|
409
422
|
|
|
410
423
|
### Waveform
|
|
@@ -508,9 +521,9 @@ class NormalizeStream extends BufferedTransformStream<NormalizeProperties> {
|
|
|
508
521
|
}
|
|
509
522
|
|
|
510
523
|
class NormalizeNode extends TransformNode<NormalizeProperties> {
|
|
511
|
-
static override readonly
|
|
524
|
+
static override readonly nodeName = "Normalize";
|
|
512
525
|
static override readonly packageName = "buffered-audio-nodes";
|
|
513
|
-
static override readonly
|
|
526
|
+
static override readonly nodeDescription = "Adjust peak or loudness level to a target ceiling";
|
|
514
527
|
static override readonly schema = schema;
|
|
515
528
|
|
|
516
529
|
override readonly type = ["buffered-audio-node", "transform", "normalize"] as const;
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,88 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
4
4
|
import { resolve } from 'path';
|
|
5
5
|
import { SourceNode, validateGraphDefinition, renderGraph } from '@buffered-audio/core';
|
|
6
6
|
|
|
7
|
+
var labelOf = (node) => node.id ? `${node.nodeName}#${node.id}` : node.nodeName;
|
|
8
|
+
var collect = (value, previous) => [...previous, value];
|
|
9
|
+
function parseParams(entries) {
|
|
10
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const separatorIndex = entry.indexOf("=");
|
|
13
|
+
if (separatorIndex === -1) {
|
|
14
|
+
process.stderr.write(`Error: --param must be in name=value form, got "${entry}"
|
|
15
|
+
`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const name = entry.slice(0, separatorIndex);
|
|
19
|
+
const value = entry.slice(separatorIndex + 1);
|
|
20
|
+
if (name === "") {
|
|
21
|
+
process.stderr.write(`Error: --param name must not be empty, got "${entry}"
|
|
22
|
+
`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (parameters.has(name)) {
|
|
26
|
+
process.stderr.write(`Error: --param ${name} given more than once
|
|
27
|
+
`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
parameters.set(name, value);
|
|
31
|
+
}
|
|
32
|
+
return Object.fromEntries(parameters);
|
|
33
|
+
}
|
|
34
|
+
function createEventSink() {
|
|
35
|
+
const totals = /* @__PURE__ */ new Map();
|
|
36
|
+
const onEvent = (node, event) => {
|
|
37
|
+
const label = labelOf(node);
|
|
38
|
+
switch (event.kind) {
|
|
39
|
+
case "started":
|
|
40
|
+
process.stdout.write(`[${label}] started
|
|
41
|
+
`);
|
|
42
|
+
break;
|
|
43
|
+
case "progress":
|
|
44
|
+
if (event.framesTotal !== void 0) {
|
|
45
|
+
const percent = Math.round(event.framesDone / event.framesTotal * 100);
|
|
46
|
+
process.stdout.write(`[${label}] ${event.phase} ${percent}%
|
|
47
|
+
`);
|
|
48
|
+
} else {
|
|
49
|
+
process.stdout.write(`[${label}] ${event.phase} frames=${event.framesDone}
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case "log": {
|
|
54
|
+
const data = event.data ? Object.entries(event.data).map(([key, value]) => `${key}=${String(value)}`) : [];
|
|
55
|
+
const parts = [event.message, ...data].join(" ");
|
|
56
|
+
const prefix = event.level === "warn" ? "warn: " : "";
|
|
57
|
+
process.stdout.write(`${prefix}[${label}] ${parts}
|
|
58
|
+
`);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "finished":
|
|
62
|
+
totals.set(node, { framesDone: event.framesDone, processingMs: event.processingMs });
|
|
63
|
+
process.stdout.write(`[${label}] finished
|
|
64
|
+
`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const printSummary = (sources) => {
|
|
69
|
+
for (const [node, { framesDone, processingMs }] of totals) {
|
|
70
|
+
const label = labelOf(node);
|
|
71
|
+
if (processingMs !== void 0) {
|
|
72
|
+
process.stdout.write(`[${label}] processed ${framesDone} frames in ${Math.round(processingMs)}ms
|
|
73
|
+
`);
|
|
74
|
+
} else {
|
|
75
|
+
process.stdout.write(`[${label}] processed ${framesDone} frames
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const source of sources) {
|
|
80
|
+
const timing = source.renderTiming;
|
|
81
|
+
if (!timing) continue;
|
|
82
|
+
const label = labelOf({ nodeName: source.constructor.nodeName, id: source.id });
|
|
83
|
+
process.stdout.write(`[${label}] total ${(timing.totalMs / 1e3).toFixed(1)}s, ${timing.realTimeMultiplier.toFixed(1)}x RT
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return { onEvent, printSummary };
|
|
88
|
+
}
|
|
7
89
|
var program = new Command();
|
|
8
90
|
program.name("buffered-audio-nodes").description("Process audio through buffered audio node pipelines");
|
|
9
91
|
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 +116,22 @@ program.command("process").description("Run an async audio processing pipeline")
|
|
|
34
116
|
`);
|
|
35
117
|
process.exit(1);
|
|
36
118
|
}
|
|
119
|
+
const sink = createEventSink();
|
|
37
120
|
const renderOptions = {
|
|
38
121
|
chunkSize,
|
|
39
|
-
highWaterMark
|
|
122
|
+
highWaterMark,
|
|
123
|
+
onEvent: sink.onEvent
|
|
40
124
|
};
|
|
41
125
|
process.stdout.write(`Processing pipeline: ${pipelinePath}
|
|
42
126
|
`);
|
|
43
127
|
await source.render(renderOptions);
|
|
128
|
+
sink.printSummary([source]);
|
|
44
129
|
process.stdout.write("Done.\n");
|
|
45
130
|
} finally {
|
|
46
131
|
await unregister();
|
|
47
132
|
}
|
|
48
133
|
});
|
|
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) => {
|
|
134
|
+
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
135
|
const bagPath = resolve(file);
|
|
51
136
|
if (!existsSync(bagPath)) {
|
|
52
137
|
process.stderr.write(`Error: file not found: ${bagPath}
|
|
@@ -66,18 +151,27 @@ program.command("render").description("Render a .bag graph definition file").arg
|
|
|
66
151
|
for (const value of Object.values(mod)) {
|
|
67
152
|
if (typeof value !== "function") continue;
|
|
68
153
|
const ctor = value;
|
|
69
|
-
if (typeof ctor.
|
|
70
|
-
packageMap.set(ctor.
|
|
154
|
+
if (typeof ctor.nodeName !== "string") continue;
|
|
155
|
+
packageMap.set(ctor.nodeName, ctor);
|
|
71
156
|
}
|
|
72
157
|
registry.set(nodeDef.packageName, packageMap);
|
|
73
158
|
}
|
|
74
159
|
}
|
|
75
160
|
const chunkSize = options.chunkSize ? parseInt(options.chunkSize, 10) : void 0;
|
|
76
161
|
const highWaterMark = options.highWaterMark ? parseInt(options.highWaterMark, 10) : void 0;
|
|
162
|
+
const parameters = parseParams(options.param);
|
|
163
|
+
const sink = createEventSink();
|
|
77
164
|
process.stdout.write(`Rendering graph: ${definition.name}
|
|
78
165
|
`);
|
|
79
|
-
|
|
80
|
-
|
|
166
|
+
try {
|
|
167
|
+
const sources = await renderGraph(definition, registry, { chunkSize, highWaterMark, parameters, onEvent: sink.onEvent });
|
|
168
|
+
sink.printSummary(sources);
|
|
169
|
+
process.stdout.write("Done.\n");
|
|
170
|
+
} catch (error) {
|
|
171
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
172
|
+
`);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
}
|
|
81
175
|
} finally {
|
|
82
176
|
await unregister();
|
|
83
177
|
}
|