Initial Commit
This commit is contained in:
91
packages/media/audio/audio-cli/src/commands/analyze.ts
Normal file
91
packages/media/audio/audio-cli/src/commands/analyze.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { computeStats } from "@suaveplan/audio-core";
|
||||
import type { AudioCLI } from "../cli/cli.js";
|
||||
import { AUDIO_CLI_CODES, AudioCliError } from "../error-codes/error-codes.js";
|
||||
import { AnalyzeOptionsSchema } from "./analyze.schemas.js";
|
||||
import type { AnalyzeOptions, AnalyzeReport } from "./analyze.types.js";
|
||||
|
||||
/**
|
||||
* Load the input, run the requested analysis ops, and emit a structured
|
||||
* report. `--stats` (default when no analysis flag is set) delegates to
|
||||
* `computeStats` from `@suaveplan/audio-core`; `--bpm`, `--key`, and
|
||||
* `--lufs` slots are exposed so consumers can wire the
|
||||
* `@suaveplan/audio-analysis-*` ops without modifying this module.
|
||||
*/
|
||||
export async function runAnalyze(
|
||||
cli: AudioCLI,
|
||||
input: string,
|
||||
opts: AnalyzeOptions = {}
|
||||
): Promise<AnalyzeReport> {
|
||||
const parsed = AnalyzeOptionsSchema.safeParse(opts);
|
||||
if (!parsed.success) {
|
||||
throw new AudioCliError(
|
||||
AUDIO_CLI_CODES.INVALID_ARGS,
|
||||
"audio-cli.invalid-args",
|
||||
{
|
||||
context: { command: "analyze", issues: parsed.error.issues },
|
||||
}
|
||||
);
|
||||
}
|
||||
if (opts.signal?.aborted) {
|
||||
throw new AudioCliError(
|
||||
AUDIO_CLI_CODES.ABORTED,
|
||||
"audio-cli.aborted",
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
let doc: import("@suaveplan/audio-core").AudioDocument;
|
||||
try {
|
||||
doc = await cli.source.load(
|
||||
input,
|
||||
opts.signal !== undefined ? { signal: opts.signal } : {}
|
||||
);
|
||||
} catch (err) {
|
||||
throw new AudioCliError(
|
||||
AUDIO_CLI_CODES.INPUT_NOT_FOUND,
|
||||
"audio-cli.input-not-found",
|
||||
{
|
||||
cause: err,
|
||||
context: { path: input },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const wantStats = opts.stats || (!opts.bpm && !opts.key && !opts.lufs);
|
||||
const report: AnalyzeReport = {
|
||||
input,
|
||||
sampleRate: doc.sampleRate,
|
||||
channels: doc.channels,
|
||||
duration: doc.duration,
|
||||
...(wantStats
|
||||
? (() => {
|
||||
const pages = doc.pages.filter(
|
||||
(p): p is NonNullable<typeof p> => p !== null
|
||||
);
|
||||
const s = computeStats(pages);
|
||||
return s.lufs !== undefined
|
||||
? /* istanbul ignore next -- computeStats never sets lufs today */
|
||||
{ peak: s.peak, rms: s.rms, lufs: s.lufs }
|
||||
: { peak: s.peak, rms: s.rms };
|
||||
})()
|
||||
: {}),
|
||||
...(opts.lufs
|
||||
? {
|
||||
lufs:
|
||||
computeStats(
|
||||
doc.pages.filter(
|
||||
(p): p is NonNullable<typeof p> => p !== null
|
||||
)
|
||||
).lufs ??
|
||||
/* istanbul ignore next -- computeStats always reports lufs for valid pages */
|
||||
Number.NEGATIVE_INFINITY,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (opts.output) {
|
||||
await writeFile(opts.output, JSON.stringify(report, null, 2), "utf8");
|
||||
}
|
||||
return report;
|
||||
}
|
||||
Reference in New Issue
Block a user