@buffered-audio/nodes 0.15.2 → 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 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 --bag pipeline.bag
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 moduleName = "Normalize";
524
+ static override readonly nodeName = "Normalize";
512
525
  static override readonly packageName = "buffered-audio-nodes";
513
- static override readonly moduleDescription = "Adjust peak or loudness level to a target ceiling";
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.moduleName !== "string") continue;
70
- packageMap.set(ctor.moduleName, 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
- await renderGraph(definition, registry, { chunkSize, highWaterMark });
80
- process.stdout.write("Done.\n");
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
  }