Setting the file. One moment.
Subchapter 18.88
audio/scripts/lib/recording.mjs
JavaScript53 lines2 KB
// recording.mjs — the person's own voice recording as a voice line: converted to the engine's wav,
// time-stretched by `speed` with the pitch kept, with word timings scaled to match.
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
// ffmpeg's atempo takes 0.5..2 per stage; chain stages for the rest of the 0.1..3 speed range.
export function atempoChain(speed) {
const stages = [];
let left = speed;
while (left > 2) {
stages.push(2);
left /= 2;
}
while (left < 0.5) {
stages.push(0.5);
left /= 0.5;
}
if (Math.abs(left - 1) > 1e-9 || stages.length === 0) stages.push(left);
return stages.map((s) => `atempo=${Number(s.toFixed(6))}`).join(",");
}
/** src -> 44.1k mono wav at wavAbs, played `speed` times faster. Returns { ok, error? }. */
export function stretchToWav(src, wavAbs, speed = 1) {
if (!existsSync(src)) return { ok: false, error: `recording not found: ${src}` };
mkdirSync(dirname(wavAbs), { recursive: true });
const filter = speed === 1 ? [] : ["-af", atempoChain(speed)];
const r = spawnSync(
"ffmpeg",
["-y", "-loglevel", "error", "-i", src, ...filter, "-ar", "44100", "-ac", "1", wavAbs],
{ encoding: "utf8" },
);
if (r.error || r.status !== 0 || !existsSync(wavAbs)) {
return { ok: false, error: `ffmpeg could not convert ${src}: ${r.error?.message ?? r.stderr}` };
}
return { ok: true };
}
/** Caller-supplied word timings ({text|word, start, end}) as [{text,start,end}], or null when absent. */
export function parseWords(words) {
if (!Array.isArray(words) || words.length === 0) return null;
return words.map((w, i) => {
const text = w?.text ?? w?.word;
if (typeof text !== "string" || !Number.isFinite(w.start) || !Number.isFinite(w.end)) {
throw new Error(`words[${i}] needs text (or word), start and end in seconds`);
}
return { text, start: w.start, end: w.end };
});
}
/** Words of a recording played `speed` times faster land at start/speed. */
export const scaleWords = (words, speed) =>
words.map((w) => ({ ...w, start: w.start / speed, end: w.end / speed }));