92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
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;
|
|
}
|