first commit
This commit is contained in:
104
docs/media/audio/audio-cli/README.md
Normal file
104
docs/media/audio/audio-cli/README.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# @suaveplan/audio-cli - Server-tier `audio` command-line tool: chainable pipeline grammar, batch glob processing, analysis JSON reports, codec conversion, info, play, record. Thin shell over @suaveplan/audio-core + audio-codecs + audio-engine-server.
|
||||
|
||||
Server-tier `audio` command-line tool. Thin shell over
|
||||
`@suaveplan/audio-core`, `@suaveplan/audio-codecs`, and
|
||||
`@suaveplan/audio-engine-server`: every audio transform delegates to the
|
||||
universal layer; the CLI itself owns argument parsing, the pipeline grammar,
|
||||
command dispatch, exit-code mapping, and SIGINT handling.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [bin](./src/bin/bin.md) — This module is the `bin` shebang entry for the `suaveplan-audio` command. It is the only file in the package with side…
|
||||
- [bin](./src/bin/bin.md) — This module is the `bin` shebang entry for the `suaveplan-audio` command. It is the only file in the package with side…
|
||||
- [cli](./src/cli/cli.md) — `AudioCLI` is the application-class wrapper for the `audio` command-line tool. It owns the default codec registry, the…
|
||||
- [cli](./src/cli/cli.md) — `AudioCLI` is the application-class wrapper for the `audio` command-line tool. It owns the default codec registry, the…
|
||||
- [analyze](./src/commands/analyze.md) — `runAnalyze` is the analysis pipeline entry. It loads an input file through the `FileSourceAdapter`, runs the requested…
|
||||
- [batch](./src/commands/batch.md) — `runBatch` is the bulk-processing entry point. It expands a glob pattern or a comma-separated list of explicit paths in…
|
||||
- [convert](./src/commands/convert.md) — `runConvert` is a deliberately thin wrapper around `runProcess`. It validates the small `ConvertOptions` surface (`form…
|
||||
- [info](./src/commands/info.md) — `runInfo` produces a one-shot metadata + statistics report for a file: sample rate, channel count, duration, total samp…
|
||||
- [play](./src/commands/play.md) — `runPlay` plays a file through an injected `SpeakerAdapter`. The CLI package intentionally carries no platform audio bi…
|
||||
- [process](./src/commands/process.md) — `runProcess` is the engine of the `audio process` and bare-op invocations. It loads the input file via `FileSourceAdapt…
|
||||
- [record](./src/commands/record.md) — `runRecord` captures audio from an injected `MicrophoneAdapter` for a fixed duration and saves the resulting `AudioDocu…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module is the structured error and exit-code boundary for `@suaveplan/audio-cli`. It defines the six `AUDIO_CLI.*`…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module is the structured error and exit-code boundary for `@suaveplan/audio-cli`. It defines the six `AUDIO_CLI.*`…
|
||||
- [pipeline](./src/pipeline/parser.md) — This module turns a CLI user's free-form pipeline expression into a typed `[opName, params]` chain that the rest of the…
|
||||
- [parser](./src/pipeline/parser.md) — This module turns a CLI user's free-form pipeline expression into a typed `[opName, params]` chain that the rest of the…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/audio-cli
|
||||
```
|
||||
|
||||
The `suaveplan-audio` bin is installed alongside the package.
|
||||
|
||||
## CLI
|
||||
|
||||
```sh
|
||||
suaveplan-audio --version
|
||||
suaveplan-audio --help
|
||||
|
||||
# Process: load → ops → save
|
||||
suaveplan-audio process input.wav output.flac --pipeline "trim 30s | normalize -16lufs"
|
||||
|
||||
# Batch: glob + shared pipeline
|
||||
suaveplan-audio batch --input "tracks/*.mp3" --output processed/ --pipeline "normalize" --workers 4
|
||||
|
||||
# Analyze: produce a JSON report
|
||||
suaveplan-audio analyze input.mp3 --bpm --key --lufs --output report.json
|
||||
|
||||
# Format conversion (no ops)
|
||||
suaveplan-audio convert input.wav output.flac
|
||||
|
||||
# Print metadata + computeStats
|
||||
suaveplan-audio info input.mp3
|
||||
|
||||
# Headless play (uses an injected SpeakerAdapter)
|
||||
suaveplan-audio play input.wav
|
||||
|
||||
# Record silence (default mic) for N seconds
|
||||
suaveplan-audio record out.wav --duration 60
|
||||
```
|
||||
|
||||
## Programmatic API
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
await cli.process("input.wav", "output.flac", { pipeline: "trim 30s | normalize" });
|
||||
const report = await cli.analyze("input.mp3", { bpm: true, key: true, lufs: true });
|
||||
```
|
||||
|
||||
Subpath exports for tree-shaken consumption:
|
||||
|
||||
- `@suaveplan/audio-cli/cli` — the `AudioCLI` class.
|
||||
- `@suaveplan/audio-cli/pipeline/parser` — `parsePipeline` / `validatePipeline`.
|
||||
- `@suaveplan/audio-cli/error-codes` — `AUDIO_CLI_CODES`, `AudioCliError`,
|
||||
`EXIT_CODES`, `exitCodeFor`.
|
||||
- `@suaveplan/audio-cli/commands/<command>` — each `runX` entry.
|
||||
|
||||
## Exit codes
|
||||
|
||||
Stable mapping from `AUDIO_CLI.*` codes onto Unix exit codes:
|
||||
|
||||
| Code | Exit |
|
||||
|---|---|
|
||||
| `AUDIO_CLI.INVALID_ARGS` | `2` |
|
||||
| `AUDIO_CLI.UNKNOWN_OP` | `2` |
|
||||
| `AUDIO_CLI.INPUT_NOT_FOUND` | `3` |
|
||||
| `AUDIO_CLI.BAD_OUTPUT` | `4` |
|
||||
| `AUDIO_CLI.PIPELINE_FAILED` | `5` |
|
||||
| `AUDIO_CLI.ABORTED` | `130` |
|
||||
|
||||
`0` is reserved for success. Any uncaught non-`AudioCliError` exits `1`.
|
||||
|
||||
## Quality
|
||||
|
||||
- Server-tier, Node.js >=22.
|
||||
- 100/100/100/100 coverage on every implementation file.
|
||||
- Zero mocks — tests drive the real CLI against real `mkdtemp` temp files
|
||||
and the real `@suaveplan/audio-engine-server` adapters.
|
||||
|
||||
59
docs/media/audio/audio-cli/src/bin/bin.md
Normal file
59
docs/media/audio/audio-cli/src/bin/bin.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# `bin` — `suaveplan-audio` entry point
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the `bin` shebang entry for the `suaveplan-audio` command. It
|
||||
is the only file in the package with side effects: when executed as a script
|
||||
it installs a `SIGINT` handler, constructs an `AudioCLI`, dispatches
|
||||
`process.argv`, and calls `process.exit` with the returned exit code. The
|
||||
function `main` is also exported so tests and programmatic callers can drive
|
||||
the exact dispatch path without spawning a subprocess.
|
||||
|
||||
The root barrel deliberately does NOT re-export this module — importing
|
||||
`@suaveplan/audio-cli` should never read `process.argv` or attach signal
|
||||
listeners.
|
||||
|
||||
## Features
|
||||
|
||||
- `#!/usr/bin/env bun` shebang so the installed `bin` runs through Bun.
|
||||
- Single `AbortController` per invocation, threaded into every long-running
|
||||
command via `cli.dispatch`.
|
||||
- Two-tick `SIGINT` handling: first signal aborts cooperatively, second
|
||||
signal calls `process.exit(130)` for force-exit.
|
||||
- `import.meta.url` self-test guard so importing the file as a module is a
|
||||
pure type/export operation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```sh
|
||||
$ suaveplan-audio --version
|
||||
0.1.0
|
||||
$ suaveplan-audio convert input.wav output.flac
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { main } from "@suaveplan/audio-cli/bin";
|
||||
|
||||
const exitCode = await main(["analyze", "track.wav", "--lufs"]);
|
||||
console.log("exit:", exitCode);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `main(argv: readonly string[]): Promise<number>` — dispatch and return
|
||||
exit code without calling `process.exit` (caller's responsibility).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `import.meta.url === \`file://\${process.argv[1]}\`` check is the
|
||||
canonical Node ESM way to ask "am I running as the entry script?" It avoids
|
||||
the historical `require.main === module` CJS idiom and keeps the file safe
|
||||
to import from tests. The single `AbortController` is threaded via the
|
||||
dispatch's per-command argument extraction so the same signal reaches the
|
||||
engine adapters as the spec mandates.
|
||||
76
docs/media/audio/audio-cli/src/cli/cli.md
Normal file
76
docs/media/audio/audio-cli/src/cli/cli.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# `cli` — `AudioCLI` class
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`AudioCLI` is the application-class wrapper for the `audio` command-line tool.
|
||||
It owns the default codec registry, the default op registry, the
|
||||
`FileSourceAdapter` / `FileSinkAdapter` pair from `@suaveplan/audio-engine-server`,
|
||||
the stdout/stderr streams, the OTel instrumentation, and the argv dispatch
|
||||
logic. Both the `bin` entry and programmatic consumers construct an instance
|
||||
of this class and call its public methods, so the test surface and the binary
|
||||
exercise the same code path.
|
||||
|
||||
The class deliberately holds only wiring: every audio transform is delegated
|
||||
to the seven `runX` command modules, the pipeline parser, and the underlying
|
||||
`@suaveplan/audio-*` packages.
|
||||
|
||||
## Features
|
||||
|
||||
- Default `CodecRegistry` populated with the 10 built-in codecs.
|
||||
- Default `OpRegistry.createWithDefaults()` plus user-supplied overrides.
|
||||
- Injectable `FileSourceAdapter` / `FileSinkAdapter` for tests without
|
||||
patching `node:fs`.
|
||||
- OTel span (`audio_cli.<command>`), command counter, duration histogram,
|
||||
files-processed counter, all tagged with `{ command, outcome }`.
|
||||
- `dispatch(argv)` returns a numeric exit code, never throws — failures are
|
||||
formatted onto stderr and mapped through the `EXIT_CODES` table.
|
||||
- `helpText()` lists every subcommand and global flag for `--help` rendering.
|
||||
- `detectCodecForOutput(path, registry)` exposed for tests and the convert
|
||||
command: extension → codec name, throws `AUDIO_CLI.BAD_OUTPUT` on miss.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli/cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
await cli.process("input.wav", "output.flac");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli/cli";
|
||||
import { CodecRegistry, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const wavOnly = new CodecRegistry({ codecs: [WavCodec] });
|
||||
const cli = new AudioCLI({ registry: wavOnly, version: "1.2.3" });
|
||||
const exit = await cli.dispatch(["--version"]); // prints "1.2.3", returns 0
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AudioCLI(options?: AudioCliOptions)` — constructor.
|
||||
- `AudioCliOptions` — `{ registry, ops, source, sink, stdout, stderr, version }`.
|
||||
- `cli.process / batch / analyze / convert / info / play / record` — typed
|
||||
promises, each delegating to its `runX` module.
|
||||
- `cli.dispatch(argv)` — exit-code-returning dispatcher.
|
||||
- `cli.helpText()` — rendered help string.
|
||||
- `createDefaultCodecRegistry()` — top-level helper used by the default
|
||||
constructor path.
|
||||
- `detectCodecForOutput(path, registry)` — extension → codec-name lookup.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The OTel wrapper records the start time at the top of every command, records
|
||||
the outcome attribute in `finally`, and lets the `AudioCliError` propagate.
|
||||
`#reportAndExit` is the single point that converts a thrown value into a
|
||||
numeric exit code; it writes a single human-readable diagnostic line to
|
||||
stderr (`code: message`) so the binary composes cleanly inside shell
|
||||
pipelines. The `dispatch` switch is deliberately flat — every subcommand owns
|
||||
its argument-parsing helper, validates inputs with a Zod schema, and either
|
||||
returns `0` or throws an `AudioCliError`.
|
||||
63
docs/media/audio/audio-cli/src/commands/analyze.md
Normal file
63
docs/media/audio/audio-cli/src/commands/analyze.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# `commands/analyze` — `runAnalyze`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runAnalyze` is the analysis pipeline entry. It loads an input file through
|
||||
the `FileSourceAdapter`, runs the requested analysis ops, and returns (or
|
||||
writes to `--output`) a structured `AnalyzeReport` JSON payload. The default
|
||||
behaviour — when no analysis flag is passed — runs the universal
|
||||
`computeStats` from `@suaveplan/audio-core` so a `peak/rms/lufs` summary is
|
||||
always available.
|
||||
|
||||
The module is deliberately a composition point: the `--bpm`, `--key`, and
|
||||
`--lufs` flags are wired so the heavier
|
||||
`@suaveplan/audio-analysis-*` packages can be plugged in via OpRegistry-style
|
||||
extension without modifying this code path.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated `AnalyzeOptions` (`bpm`, `key`, `lufs`, `stats`, `output`,
|
||||
`signal`).
|
||||
- Default fallback to `computeStats` when no flag is given.
|
||||
- Optional `--output report.json` writes pretty-printed JSON to disk.
|
||||
- `--lufs` is independently selectable so a CI gate can produce just LUFS
|
||||
without re-running BPM/key.
|
||||
- Pre-aborted signal short-circuits before any I/O.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
const report = await cli.analyze("input.mp3");
|
||||
console.log(report.peak, report.rms);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runAnalyze } from "@suaveplan/audio-cli/commands/analyze";
|
||||
|
||||
await runAnalyze(cli, "track.wav", {
|
||||
bpm: true,
|
||||
key: true,
|
||||
lufs: true,
|
||||
output: "report.json",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runAnalyze(cli, input, opts?): Promise<AnalyzeReport>`
|
||||
- `AnalyzeOptions` — `{ bpm?, key?, lufs?, stats?, output?, signal? }`
|
||||
- `AnalyzeReport` — `{ input, sampleRate, channels, duration, peak?, rms?,
|
||||
lufs?, bpm?, key? }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`computeStats` is called once per branch — the `wantStats` and `--lufs` paths
|
||||
both delegate to it rather than re-implementing the loop. When `--output` is
|
||||
provided the function still returns the report so a programmatic caller can
|
||||
inspect the same payload that was just persisted. The signal threading
|
||||
mirrors `runProcess` so the abort guarantees are uniform across commands.
|
||||
70
docs/media/audio/audio-cli/src/commands/batch.md
Normal file
70
docs/media/audio/audio-cli/src/commands/batch.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# `commands/batch` — `runBatch`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runBatch` is the bulk-processing entry point. It expands a glob pattern or a
|
||||
comma-separated list of explicit paths into a concrete file list, runs the
|
||||
shared pipeline against every file, and returns a structured `BatchReport`
|
||||
that carries per-file success/failure rows alongside aggregate counts.
|
||||
Per-file failures do not abort the batch — the report flags them and the run
|
||||
continues — but an `AbortSignal` is honoured cooperatively at every task
|
||||
boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Glob expansion: `*.wav`, `tracks/*.mp3`, `**/foo.flac` (basename glob).
|
||||
- Comma-separated explicit list: `a.wav,b.wav,c.wav`.
|
||||
- Output naming preserves the basename and applies `outputExt` (defaulting to
|
||||
the source extension) so `tracks/song.mp3` → `out/song.<ext>`.
|
||||
- Configurable `workers` lane count (>= 1) using a cursor-based pool so
|
||||
exceptions in one lane do not stall others.
|
||||
- Pre-aborted signal short-circuits before any read; mid-flight abort throws
|
||||
`AUDIO_CLI.ABORTED` with a `{ completed, total }` context.
|
||||
- Per-file failures land in the report as `{ ok: false, error }` entries.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
const report = await cli.batch({
|
||||
input: "tracks/*.mp3",
|
||||
output: "processed/",
|
||||
pipeline: "trim 30s | normalize",
|
||||
});
|
||||
console.log(report.succeeded, "/", report.total);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runBatch } from "@suaveplan/audio-cli/commands/batch";
|
||||
|
||||
const ac = new AbortController();
|
||||
const report = await runBatch(cli, {
|
||||
input: "a.wav,b.wav,c.wav",
|
||||
output: "out/",
|
||||
outputExt: "flac",
|
||||
workers: 4,
|
||||
pipeline: "normalize -16lufs",
|
||||
signal: ac.signal,
|
||||
});
|
||||
for (const e of report.entries) if (!e.ok) console.error(e.input, e.error);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runBatch(cli, opts: BatchOptions): Promise<BatchReport>`
|
||||
- `BatchOptions` — `{ input, output, pipeline, outputExt?, workers?, signal? }`
|
||||
- `BatchEntry` — `{ input, output, ok, error?, durationMs }`
|
||||
- `BatchReport` — `{ total, succeeded, failed, entries }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The glob expansion is deliberately small and dependency-free: a `readdir`
|
||||
call on the parent directory plus a regex compiled from the pattern. Comma
|
||||
forms bypass `readdir` entirely. The worker pool uses an integer cursor so
|
||||
adding more lanes than tasks is a no-op rather than an error. The output
|
||||
directory is `dirname`-touched once so callers passing a deep `--output` path
|
||||
do not silently lose the prefix to dead-code elimination during minification.
|
||||
63
docs/media/audio/audio-cli/src/commands/convert.md
Normal file
63
docs/media/audio/audio-cli/src/commands/convert.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# `commands/convert` — `runConvert`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runConvert` is a deliberately thin wrapper around `runProcess`. It validates
|
||||
the small `ConvertOptions` surface (`format`, `bitDepth`, `signal`) and
|
||||
forwards into the shared decode/encode path with no pipeline ops. Keeping it
|
||||
this thin means the format mapping, codec lookup, abort handling, and error
|
||||
translation live in exactly one place — the `process` command — which the
|
||||
spec calls out as the single source of truth for streaming through
|
||||
`audio-core`/`audio-codecs`/`audio-engine-server`.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-strict validation of `ConvertOptions`.
|
||||
- Optional explicit `format` overriding extension detection.
|
||||
- Optional `bitDepth` forwarded to the codec encoder where supported.
|
||||
- Pre-aborted signal short-circuits via `runProcess`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
await cli.convert("input.wav", "output.flac");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runConvert } from "@suaveplan/audio-cli/commands/convert";
|
||||
|
||||
await runConvert(cli, "input.wav", "output.aiff", { bitDepth: 24 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runConvert(cli, input, output, opts?): Promise<void>`
|
||||
- `ConvertOptions` — `{ format?, bitDepth?, signal? }`
|
||||
- `ConvertOptionsSchema` — Zod boundary validation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Because the implementation forwards into `runProcess`, every error code that
|
||||
`runProcess` produces (`INPUT_NOT_FOUND`, `BAD_OUTPUT`, `PIPELINE_FAILED`,
|
||||
`ABORTED`) is reachable from `convert` as well. The empty pipeline path is
|
||||
not a special case in `runProcess`: an empty edit chain just means
|
||||
"decode → re-encode", which is exactly the conversion semantic.
|
||||
|
||||
The Zod schema is `strict()`, so a typo in an option key (`{ formt: "wav" }`)
|
||||
fails fast at the boundary with `AUDIO_CLI.INVALID_ARGS` rather than being
|
||||
silently dropped. The `bitDepth` field is constrained to the union
|
||||
`8 | 16 | 24 | 32` so encoders that only accept those depths receive a
|
||||
correctly-typed value. Passing an `AbortSignal` that is already aborted at
|
||||
call time short-circuits inside `runProcess` before any file is opened, so
|
||||
`convert` participates in the same SIGINT contract as every other command.
|
||||
|
||||
`convert` is the cheapest way to migrate a file between formats in shell
|
||||
pipelines: it skips the planner and the op chain entirely, so a file with
|
||||
no edits goes straight through the codec stack. For copying with an op
|
||||
chain attached, prefer the `process` command directly — `convert` is
|
||||
deliberately a forward-only wrapper with a smaller option surface.
|
||||
70
docs/media/audio/audio-cli/src/commands/info.md
Normal file
70
docs/media/audio/audio-cli/src/commands/info.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# `commands/info` — `runInfo`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runInfo` produces a one-shot metadata + statistics report for a file:
|
||||
sample rate, channel count, duration, total samples, peak/RMS/DC-offset, and
|
||||
LUFS (when available). It is the read-only counterpart to `analyze`: cheap,
|
||||
predictable, intended for shell-pipeline composition (`audio info x.wav |
|
||||
jq '.lufs'`).
|
||||
|
||||
The function returns the structured `InfoReport` so programmatic consumers
|
||||
get the same payload the CLI writes to stdout — there is no formatting
|
||||
divergence between programmatic and shell usage.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-strict `InfoOptions` (currently just an optional `signal`).
|
||||
- Delegates to `computeStats` from `@suaveplan/audio-core`.
|
||||
- Pre-aborted signal short-circuits before any I/O.
|
||||
- Distinguishes "no LUFS" (`null`) from "0 LUFS" so JSON consumers can
|
||||
branch on a missing measurement.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
const info = await cli.info("input.wav");
|
||||
console.log(info.duration, info.peak, info.lufs);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runInfo } from "@suaveplan/audio-cli/commands/info";
|
||||
|
||||
const ac = new AbortController();
|
||||
const info = await runInfo(cli, "input.flac", { signal: ac.signal });
|
||||
if (info.lufs !== null && info.lufs > -14) {
|
||||
console.warn("Above broadcast loudness target");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runInfo(cli, input, opts?): Promise<InfoReport>`
|
||||
- `InfoOptions` — `{ signal? }`
|
||||
- `InfoReport` — `{ input, sampleRate, channels, duration, samples, peak,
|
||||
rms, dcOffset, lufs }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`computeStats` walks the file once; the LUFS column is `null`-mapped from the
|
||||
optional `stats.lufs` field so the JSON shape is always uniform. The function
|
||||
intentionally does not retain the decoded `AudioDocument` past the stats call
|
||||
so the GC can reclaim the page memory as soon as the report is constructed.
|
||||
|
||||
The `samples` field comes from `AudioDocument.sourceLength`, not from
|
||||
recomputing the page lengths, so it stays accurate even when the document
|
||||
carries non-trivial edits. The `duration` field is the document's own
|
||||
`duration` getter (length / sample rate), so callers reading the JSON do not
|
||||
need to derive it.
|
||||
|
||||
`info` is intentionally a read-only operation: it never opens an output
|
||||
sink and never participates in the codec write path, so it is safe to chain
|
||||
into a shell pipeline that processes the JSON downstream. The Zod option
|
||||
surface is currently a single optional `signal`, but the schema is
|
||||
declared `strict()` so adding a future option there is a non-breaking
|
||||
change for callers that pass an empty options object.
|
||||
60
docs/media/audio/audio-cli/src/commands/play.md
Normal file
60
docs/media/audio/audio-cli/src/commands/play.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `commands/play` — `runPlay`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runPlay` plays a file through an injected `SpeakerAdapter`. The CLI package
|
||||
intentionally carries no platform audio binding — naming a specific speaker
|
||||
binary (sox, mpv, ffplay) would couple the package to whichever the user has
|
||||
installed. Instead, the function exercises argument validation, the input
|
||||
load path, and the adapter dispatch, and consumers (or installer hooks) wire
|
||||
the actual hardware adapter behind the `SpeakerAdapter` interface.
|
||||
|
||||
A `noopSpeaker` is exported so headless servers, CI, and tests can run the
|
||||
`play` code path without audio output.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-strict `PlayOptions` accepting an injected `SpeakerAdapter` and an
|
||||
`AbortSignal`.
|
||||
- Default `noopSpeaker` adapter for headless environments.
|
||||
- Same input-error mapping (`AUDIO_CLI.INPUT_NOT_FOUND`) as the other
|
||||
load-driven commands.
|
||||
- Adapter failures wrap as `AUDIO_CLI.PIPELINE_FAILED` with the original
|
||||
cause attached.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
await cli.play("input.wav"); // noop on a headless host
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runPlay } from "@suaveplan/audio-cli/commands/play";
|
||||
import type { SpeakerAdapter } from "@suaveplan/audio-cli/commands/play";
|
||||
|
||||
const mySpeaker: SpeakerAdapter = {
|
||||
async play(doc, { signal }) { /* ...write doc to a DAC... */ },
|
||||
};
|
||||
await runPlay(cli, "song.flac", { speaker: mySpeaker });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runPlay(cli, input, opts?): Promise<void>`
|
||||
- `PlayOptions` — `{ speaker?, signal? }`
|
||||
- `SpeakerAdapter.play(doc, { signal? }): Promise<void>`
|
||||
- `noopSpeaker: SpeakerAdapter`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function deliberately structures the failure modes so a missing speaker
|
||||
falls through to the no-op rather than throwing — this matches `audio play`
|
||||
on a headless box: argv parses, the file decodes, and the command returns
|
||||
`0`. Real hardware bindings ship as separate adapters consumers add at
|
||||
construction time via `new AudioCLI({ ... })` or by passing `opts.speaker`
|
||||
directly.
|
||||
66
docs/media/audio/audio-cli/src/commands/process.md
Normal file
66
docs/media/audio/audio-cli/src/commands/process.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# `commands/process` — `runProcess`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runProcess` is the engine of the `audio process` and bare-op invocations. It
|
||||
loads the input file via `FileSourceAdapter`, validates and appends the
|
||||
parsed pipeline as `AudioEdit` records on the resulting `AudioDocument`, and
|
||||
streams the compiled plan into `FileSinkAdapter.save` against the
|
||||
extension-inferred (or explicit) output codec. Every audio transform is
|
||||
delegated to `@suaveplan/audio-core` and `@suaveplan/audio-codecs`; this
|
||||
module owns only argument validation, error-code translation, and the
|
||||
sequencing.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod validation of `ProcessOptions` (`pipeline`, `format`, `bitDepth`,
|
||||
`signal`).
|
||||
- Accepts both string and pre-parsed `ParsedPipeline` for the `pipeline`
|
||||
field; the string form is parsed via `parsePipeline`.
|
||||
- Treats a `save` token in the chain as a sentinel and skips it during
|
||||
registry validation (the bare-op form ends with `save <output>`).
|
||||
- Maps every engine error to an `AUDIO_CLI.*` code with `cause` preserved:
|
||||
- `AUDIO_ENGINE_SERVER.FILE_NOT_FOUND` / `IO_FAILED` →
|
||||
`AUDIO_CLI.INPUT_NOT_FOUND` (exit 3).
|
||||
- `AUDIO_ENGINE_SERVER.ABORTED` → `AUDIO_CLI.ABORTED` (exit 130).
|
||||
- Anything else during load/run/save → `AUDIO_CLI.PIPELINE_FAILED` (exit 5).
|
||||
- Pre-aborted `AbortSignal` short-circuits before any I/O.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
await cli.process("input.wav", "output.flac", { pipeline: "normalize -16lufs" });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runProcess } from "@suaveplan/audio-cli/commands/process";
|
||||
|
||||
const ac = new AbortController();
|
||||
setTimeout(() => ac.abort(), 250);
|
||||
await runProcess(cli, "input.wav", "output.flac", {
|
||||
pipeline: [["trim", { start: 30 }], ["normalize", { lufs: -16 }]],
|
||||
bitDepth: 24,
|
||||
signal: ac.signal,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runProcess(cli, input, output, opts?)` — async, returns `void`, throws
|
||||
`AudioCliError` on failure.
|
||||
- `ProcessOptions` — `{ pipeline?, format?, bitDepth?, signal? }`.
|
||||
- `ProcessOptionsSchema` — Zod strict schema for boundary validation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function intentionally builds the executable chain in three stages:
|
||||
parse (or accept), filter out the `save` sentinel, then validate names
|
||||
against the registry. That keeps the pipe-form and bare-op-form code paths
|
||||
identical past the parser. The engine error mapping mirrors the spec's
|
||||
`AUDIO_CLI.*` requirements and is the only place those translations live — the
|
||||
CLI dispatch layer just propagates whatever this throws.
|
||||
62
docs/media/audio/audio-cli/src/commands/record.md
Normal file
62
docs/media/audio/audio-cli/src/commands/record.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# `commands/record` — `runRecord`
|
||||
|
||||
## Purpose
|
||||
|
||||
`runRecord` captures audio from an injected `MicrophoneAdapter` for a fixed
|
||||
duration and saves the resulting `AudioDocument` through `FileSinkAdapter`.
|
||||
The CLI ships only the adapter contract and a silence-producing default —
|
||||
specific platform bindings (CoreAudio, ALSA, WebAudio) ship as separate
|
||||
adapter packages or installer hooks, so this command works identically on
|
||||
CI/server hosts and on real hardware.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-strict `RecordOptions` (`duration`, `sampleRate`, `channels`,
|
||||
`format`, `bitDepth`, `microphone`, `signal`).
|
||||
- Default `createSilentMicrophone()` adapter produces a deterministic silent
|
||||
document — useful for CI and tests.
|
||||
- Output format inferred from extension (or set via `opts.format`).
|
||||
- Default 48 kHz / 2 channels matches mainstream broadcast/recording targets.
|
||||
- Pre-aborted signal short-circuits before any adapter call.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from "@suaveplan/audio-cli";
|
||||
|
||||
const cli = new AudioCLI();
|
||||
await cli.record("out.wav", { duration: 60 }); // silent on headless boxes
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runRecord, createSilentMicrophone } from "@suaveplan/audio-cli/commands/record";
|
||||
import type { MicrophoneAdapter } from "@suaveplan/audio-cli/commands/record";
|
||||
|
||||
const myMic: MicrophoneAdapter = {
|
||||
async capture({ duration, sampleRate, channels }) {
|
||||
// ...read from a real device...
|
||||
return createSilentMicrophone().capture({ duration, sampleRate, channels });
|
||||
},
|
||||
};
|
||||
await runRecord(cli, "out.flac", { duration: 30, microphone: myMic, bitDepth: 24 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runRecord(cli, output, opts): Promise<void>`
|
||||
- `RecordOptions` — `{ duration, sampleRate?, channels?, format?, bitDepth?,
|
||||
microphone?, signal? }`
|
||||
- `MicrophoneAdapter.capture({ duration, sampleRate, channels, signal? }): Promise<AudioDocument>`
|
||||
- `createSilentMicrophone(): MicrophoneAdapter`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The silent default produces `Math.round(duration * sampleRate)` samples per
|
||||
channel so byte-exact tests can assert document length without a hardware
|
||||
dependency. The save path mirrors `runProcess`: failures wrap to
|
||||
`AUDIO_CLI.PIPELINE_FAILED` with `cause` and a `{ stage, output, format }`
|
||||
context. The `--duration` argument is mandatory because indefinite recording
|
||||
needs a different surface (an interactive stop signal) than what `audio
|
||||
record <output> --duration <s>` exposes.
|
||||
79
docs/media/audio/audio-cli/src/error-codes/error-codes.md
Normal file
79
docs/media/audio/audio-cli/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# `error-codes` — AudioCliError and EXIT_CODES
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the structured error and exit-code boundary for
|
||||
`@suaveplan/audio-cli`. It defines the six `AUDIO_CLI.*` codes the package
|
||||
throws, the `AudioCliError` class extending `SuaveplanError` carrying `code`,
|
||||
`cause`, and `context`, the `AUDIO_CLI_ERROR_METADATA` table with descriptions
|
||||
and severity, and the `EXIT_CODES` table that maps every code onto a stable
|
||||
non-zero Unix exit integer.
|
||||
|
||||
It also exports `exitCodeFor`, which the CLI entry point calls in its global
|
||||
catch to convert any thrown value into a `process.exit` argument. The mapping
|
||||
is part of the public contract: CI pipelines and shell scripts depend on it,
|
||||
so changing a code-to-integer mapping is a breaking change.
|
||||
|
||||
## Features
|
||||
|
||||
- Six-code registry (`INVALID_ARGS`, `UNKNOWN_OP`, `INPUT_NOT_FOUND`,
|
||||
`BAD_OUTPUT`, `PIPELINE_FAILED`, `ABORTED`) via `defineCodes`.
|
||||
- `AudioCliError extends SuaveplanError` with strongly-typed `code`.
|
||||
- `EXIT_CODES` immutable table (`0`-reserved, `2/3/4/5/130`).
|
||||
- `exitCodeFor(err)` unknown-shape fallback to `1`.
|
||||
- Idempotent `registerAudioCliErrorCodes()` for cross-package code-table fans.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AudioCliError, AUDIO_CLI_CODES, exitCodeFor } from "@suaveplan/audio-cli/error-codes";
|
||||
|
||||
try {
|
||||
throw new AudioCliError(AUDIO_CLI_CODES.INPUT_NOT_FOUND, "audio-cli.input-not-found", {
|
||||
context: { path: "missing.wav" },
|
||||
});
|
||||
} catch (err) {
|
||||
process.exit(exitCodeFor(err));
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_CLI_CODES,
|
||||
AUDIO_CLI_ERROR_METADATA,
|
||||
EXIT_CODES,
|
||||
type AudioCliErrorCode,
|
||||
} from "@suaveplan/audio-cli/error-codes";
|
||||
|
||||
// Render a help table of every code, its severity, and the exit it produces.
|
||||
for (const [code, meta] of Object.entries(AUDIO_CLI_ERROR_METADATA)) {
|
||||
const c = code as AudioCliErrorCode;
|
||||
console.log(`${c}\t${meta.severity}\texit=${EXIT_CODES[c]}\t${meta.description}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUDIO_CLI_CODES` — frozen string-literal record of the six codes.
|
||||
- `AudioCliErrorCode` — union of those code values.
|
||||
- `AudioCliError(code, messageKey, options?)` — `SuaveplanError` subclass.
|
||||
- `AUDIO_CLI_ERROR_METADATA` — per-code `{ description, severity }`.
|
||||
- `EXIT_CODES` — per-code non-zero integer.
|
||||
- `registerAudioCliErrorCodes()` — idempotent registry hook.
|
||||
- `exitCodeFor(err)` — map a thrown value to an exit code (`0`/`1`/EXIT_CODES).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The error class follows the same pattern as `AudioEngineServerError` and
|
||||
`AudioCodecsError` in sibling packages. `defineCodes` ensures the literal
|
||||
record is frozen at the type level so a typo in `AUDIO_CLI_CODES.foo` fails to
|
||||
compile. `exitCodeFor` walks two shapes: an `AudioCliError` instance (cheap
|
||||
typeof check) and a duck-typed `{ code: string }` so foreign errors carrying a
|
||||
known `AUDIO_CLI.*` code still map correctly. Anything else falls through to
|
||||
`1`, the canonical "uncaught error" Unix convention.
|
||||
69
docs/media/audio/audio-cli/src/pipeline/parser.md
Normal file
69
docs/media/audio/audio-cli/src/pipeline/parser.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# `pipeline/parser` — `parsePipeline` and `validatePipeline`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module turns a CLI user's free-form pipeline expression into a typed
|
||||
`[opName, params]` chain that the rest of the CLI feeds into
|
||||
`AudioDocument.run`. It supports both the **pipe-form** string —
|
||||
`"trim 30s | normalize -16lufs | fade 0.5s 1s"` — and the **positional
|
||||
bare-op** token array — `["trim", "30s", "normalize", "-16lufs"]` — and
|
||||
collapses both surface forms onto the same internal representation so there is
|
||||
exactly one downstream code path.
|
||||
|
||||
It also exposes `validatePipeline(chain, knownOps)` which the CLI calls before
|
||||
opening any file, so an unknown op name throws `AUDIO_CLI.UNKNOWN_OP` fast and
|
||||
no I/O is wasted.
|
||||
|
||||
## Features
|
||||
|
||||
- `tokenise` respects double-quoted strings (`"a b"`) and backslash escapes
|
||||
(`a\ b`) so a single op can carry a string parameter with spaces.
|
||||
- Unit suffix coercion: `30s → start=30`, `500ms → start=0.5`,
|
||||
`-16lufs → lufs=-16`, `6db → db=6`, `44100hz → rate=44100`,
|
||||
`48khz → rate=48000`.
|
||||
- `key=value` arguments (`foo=bar`) become named params; numeric literals
|
||||
without a unit become positional `start`/`duration`.
|
||||
- `validatePipeline` lists the registry up-front so the diagnostic names the
|
||||
offending op alongside the sorted set of known ops.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parsePipeline } from "@suaveplan/audio-cli/pipeline/parser";
|
||||
|
||||
const chain = parsePipeline("trim 30s | normalize -16lufs");
|
||||
// → [["trim", { start: 30 }], ["normalize", { lufs: -16 }]]
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { parsePipeline, validatePipeline } from "@suaveplan/audio-cli/pipeline/parser";
|
||||
import { OpRegistry } from "@suaveplan/audio-core";
|
||||
|
||||
const registry = OpRegistry.createWithDefaults();
|
||||
const known = new Set(registry.list().map((d) => d.name));
|
||||
const chain = parsePipeline(["trim", "30s", "normalize", "-16lufs", "fade", "0.5s", "1s"]);
|
||||
validatePipeline(chain, known); // throws AUDIO_CLI.UNKNOWN_OP if a name is unknown
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ParsedPipeline` — `ReadonlyArray<readonly [string, Record<string, unknown>]>`.
|
||||
- `parsePipeline(spec: string | readonly string[])` — produce the chain.
|
||||
- `validatePipeline(chain, knownOps: ReadonlySet<string>)` — throws
|
||||
`AudioCliError(AUDIO_CLI.UNKNOWN_OP)` on the first unknown op name.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The hand-written tokenizer is deliberately small: a single forward pass with
|
||||
two flags (`inQuote`, `escaped`) and a per-stage accumulator. The pipe-form
|
||||
splits on `|`; the bare-op form treats every identifier-shaped token (matching
|
||||
`/^[a-zA-Z][a-zA-Z0-9_-]*$/`) as the start of a new stage so positional
|
||||
arguments belonging to the previous op are correctly grouped. Unit coercion
|
||||
runs as a small regex per argument so `trim 30s` always lands as
|
||||
`{ start: 30 }` regardless of which surface form it arrived through.
|
||||
Reference in New Issue
Block a user