@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 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
 
@@ -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 moduleName = "Normalize";
512
+ static override readonly nodeName = "Normalize";
512
513
  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 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, renderGraph } from '@buffered-audio/core';
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 renderOptions = {
38
- chunkSize,
39
- highWaterMark
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 source.render(renderOptions);
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.moduleName !== "string") continue;
70
- packageMap.set(ctor.moduleName, 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
- await renderGraph(definition, registry, { chunkSize, highWaterMark });
80
- process.stdout.write("Done.\n");
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
  }