@buffered-audio/nodes 0.18.2 → 0.19.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
@@ -438,99 +438,87 @@ Write audio to a file
438
438
 
439
439
  ## Creating Nodes
440
440
 
441
- 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
+ Each node has two parts: a **Node** (inert descriptor) and a **Stream** (stateful runtime instance). The node is pure static declaration `nodeName`, `description`, `schema`, `Stream`, plus `packageName`/`packageVersion` and carries no per-render state. The executor constructs one stream per node per render.
442
442
 
443
- 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
+ Transforms extend one of two stream bases from `@buffered-audio/core`, picked by whether the node needs the whole signal (or fixed-size blocks) before it can produce output:
444
+
445
+ - **`UnbufferedTransformStream`** — per-block streaming (gain, pan, dither). One input block in, zero or more output blocks out.
446
+ - **`BufferedTransformStream`** — measure-then-apply and windowed DSP (normalize, loudness). Accumulates into a `BlockBuffer` sized by `blockSize` (default `WHOLE_FILE`), then serves.
444
447
 
445
448
  ### Stream Hooks
446
449
 
447
- - **`_buffer(chunk, buffer)`**called for each incoming chunk. Override to inspect or modify data as it's buffered. Default appends to the buffer.
448
- - **`_process(buffer)`** — called once the buffer reaches `bufferSize`. Use this for analysis or in-place modification of the full buffer.
449
- - **`_unbuffer(chunk)`** — called for each chunk emitted from the buffer. Transform or replace the chunk here. Return `undefined` to drop it.
450
- - **`_teardown()`** cleanup after render completes. Close file handles, free native resources, release ONNX sessions. Called automatically on all streams.
450
+ All output hooks are generators `yield` a block to emit it, yield nothing to drop it. Production is paced by downstream demand (one `yield` served per pull).
451
+
452
+ - **`_transform`** — the core hook. Unbuffered: `*_transform(block)` runs once per input block. Buffered: `async *_transform(buffered: BlockBuffer)` runs once per assembled block (and once at end of stream with the trailing partial); walk the buffer with `buffered.iterate(frames)` and `yield` output.
453
+ - **`_prepare(block)`** (buffered only) a length-preserving transform on each incoming block before it is buffered. Use it to fold a streaming measurement (peak, LUFS) on the way in.
454
+ - **`_flush()`** — a generator emitting trailing output at graceful end of stream, after the final `_transform`.
455
+ - **`_setup(context)`** — context-dependent initialization (subprocess, ONNX session, FFT workspace). Runs before piping.
456
+ - **`_pipe(input)`** — maps the input readable to the output; override to compose inner streams (e.g. wrap the transform in resamplers).
457
+ - **`_destroy()`** — cleanup on every termination path (graceful end, error, cancel), invoked at most once. Close file handles, free native resources, release ONNX sessions.
458
+
459
+ Report progress with `this.emitProgress(phase, framesDone, framesTotal?)` (pace it with `createProgressGate`) and structured logs with `this.log(message, data?, level?)`.
451
460
 
452
- ### Buffer Size Modes
461
+ ### blockSize Modes (buffered)
453
462
 
454
- - `0` — pass-through. Each chunk flows through `_unbuffer` immediately.
455
- - `N` — block mode. Chunks accumulate until `N` frames are collected, then `_process` runs and `_unbuffer` emits the result.
456
- - `WHOLE_FILE` (`Infinity`) — full-file. All audio is buffered before `_process` and `_unbuffer` run.
463
+ - `WHOLE_FILE` (`Infinity`, the default) one firing at end of stream with the whole signal.
464
+ - `N` — block mode. `_transform` fires each time the buffer fills to `N` frames (short only at end of stream).
457
465
 
458
466
  ### Example: Normalize
459
467
 
460
468
  ```ts
461
469
  import { z } from "zod";
462
- import { BufferedTransformStream, TransformNode, WHOLE_FILE, type AudioChunk, type ChunkBuffer, type TransformNodeProperties } from "@buffered-audio/core";
470
+ import { BufferedTransformStream, TransformNode, WHOLE_FILE, type Block, type BlockBuffer, type TransformNodeProperties } from "@buffered-audio/core";
463
471
 
464
- const schema = z.object({
472
+ export const schema = z.object({
465
473
  ceiling: z.number().min(0).max(1).multipleOf(0.01).default(1.0).describe("Ceiling"),
466
474
  });
467
475
 
468
- interface NormalizeProperties extends z.infer<typeof schema>, TransformNodeProperties {}
469
-
470
- class NormalizeStream extends BufferedTransformStream<NormalizeProperties> {
471
- private peak = 0;
472
- private scale = 1;
473
-
474
- override async _buffer(chunk: AudioChunk, buffer: ChunkBuffer): Promise<void> {
475
- await super._buffer(chunk, buffer);
476
+ export interface NormalizeProperties extends z.infer<typeof schema>, TransformNodeProperties {}
476
477
 
477
- for (let ch = 0; ch < chunk.samples.length; ch++) {
478
- const channel = chunk.samples[ch] ?? new Float32Array(0);
478
+ export class NormalizeStream extends BufferedTransformStream<NormalizeNode> {
479
+ override blockSize = WHOLE_FILE;
479
480
 
480
- for (let si = 0; si < channel.length; si++) {
481
- const absolute = Math.abs(channel[si] ?? 0);
481
+ private peak = 0;
482
482
 
483
- if (Number.isFinite(absolute) && absolute > this.peak) this.peak = absolute;
484
- }
483
+ override _prepare(block: Block): Block {
484
+ for (const channel of block.samples) {
485
+ for (const sample of channel) this.peak = Math.max(this.peak, Math.abs(sample));
485
486
  }
486
- }
487
-
488
- override _process(_buffer: ChunkBuffer): void {
489
- const raw = this.peak === 0 ? 1 : this.properties.ceiling / this.peak;
490
487
 
491
- this.scale = Number.isFinite(raw) ? raw : 1;
488
+ return block;
492
489
  }
493
490
 
494
- override _unbuffer(chunk: AudioChunk): AudioChunk {
495
- if (this.scale === 1) return chunk;
491
+ override async *_transform(buffered: BlockBuffer): AsyncGenerator<Block> {
492
+ const scale = this.peak > 0 ? this.properties.ceiling / this.peak : 1;
496
493
 
497
- const scaledSamples = chunk.samples.map((channel) => {
498
- const scaled = new Float32Array(channel.length);
494
+ for await (const block of buffered.iterate(44100)) {
495
+ if (scale === 1) {
496
+ yield block;
499
497
 
500
- for (let index = 0; index < channel.length; index++) {
501
- scaled[index] = (channel[index] ?? 0) * this.scale;
498
+ continue;
502
499
  }
503
500
 
504
- return scaled;
505
- });
506
-
507
- return { samples: scaledSamples, offset: chunk.offset, sampleRate: chunk.sampleRate, bitDepth: chunk.bitDepth };
501
+ yield {
502
+ samples: block.samples.map((channel) => channel.map((sample) => sample * scale)),
503
+ offset: block.offset,
504
+ sampleRate: block.sampleRate,
505
+ bitDepth: block.bitDepth,
506
+ };
507
+ }
508
508
  }
509
509
  }
510
510
 
511
- class NormalizeNode extends TransformNode<NormalizeProperties> {
511
+ export class NormalizeNode extends TransformNode<NormalizeProperties> {
512
512
  static override readonly nodeName = "Normalize";
513
- static override readonly packageName = "buffered-audio-nodes";
514
- static override readonly nodeDescription = "Adjust peak or loudness level to a target ceiling";
513
+ static override readonly packageName = "@buffered-audio/nodes";
514
+ static override readonly packageVersion = "0.8.0";
515
+ static override readonly description = "Adjust peak or loudness level to a target ceiling";
515
516
  static override readonly schema = schema;
516
-
517
- override readonly type = ["buffered-audio-node", "transform", "normalize"] as const;
518
-
519
- constructor(properties: NormalizeProperties) {
520
- super({ bufferSize: WHOLE_FILE, latency: WHOLE_FILE, ...properties });
521
- }
522
-
523
- override createStream(): NormalizeStream {
524
- return new NormalizeStream({ ...this.properties, bufferSize: this.bufferSize, overlap: this.properties.overlap ?? 0 });
525
- }
526
-
527
- override clone(overrides?: Partial<NormalizeProperties>): NormalizeNode {
528
- return new NormalizeNode({ ...this.properties, previousProperties: this.properties, ...overrides });
529
- }
517
+ static override readonly Stream = NormalizeStream;
530
518
  }
531
519
 
532
520
  export function normalize(options?: { ceiling?: number; id?: string }): NormalizeNode {
533
- return new NormalizeNode({ ceiling: options?.ceiling ?? 1.0, id: options?.id });
521
+ return new NormalizeNode(options ?? {});
534
522
  }
535
523
  ```
536
524