migrate to root dir
This commit is contained in:
125
lib/pipeline/audio.ts
Normal file
125
lib/pipeline/audio.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { spawn } from "child_process";
|
||||
import { StoryConfig } from "./config";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import ffmpeg from "ffmpeg-static";
|
||||
import ffprobe from "ffprobe-static";
|
||||
|
||||
export function getDuration(file: string): Promise<number> {
|
||||
const ffprobePath: string = (ffprobe as unknown as { path: string }).path;
|
||||
const args = ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file];
|
||||
|
||||
const ffprobeProcess = spawn(ffprobePath, args);
|
||||
|
||||
let duration = "";
|
||||
ffprobeProcess.stdout.on("data", (data: Buffer) => {
|
||||
duration += data.toString();
|
||||
});
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
ffprobeProcess.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve(parseFloat(duration));
|
||||
} else {
|
||||
reject(new Error(`ffprobe process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getChunkDuration(chunkPath: string): Promise<number> {
|
||||
return getDuration(chunkPath);
|
||||
}
|
||||
|
||||
export async function generateSilence(duration: number, outputFile: string): Promise<void> {
|
||||
const ffmpegPath: string = (ffmpeg as unknown as string) || "ffmpeg";
|
||||
const args = ["-y", "-f", "lavfi", "-i", `anullsrc=r=44100:cl=stereo:d=${duration}`, outputFile];
|
||||
|
||||
const ffmpegProcess = spawn(ffmpegPath, args);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ffmpegProcess.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`ffmpeg process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function combineAudio(storyName: string, storyConfig: StoryConfig, audioFiles: string[]): Promise<void> {
|
||||
const ffmpegPath: string = (ffmpeg as unknown as string) || "ffmpeg";
|
||||
const introFile = path.resolve("stories", storyName, storyConfig.config.intro_audio_file);
|
||||
const outroFile = path.resolve("stories", storyName, storyConfig.config.outro_audio_file);
|
||||
const finalAudioDir = path.resolve("stories", storyName, "final_audio");
|
||||
const tempAudioFile = path.join(finalAudioDir, "temp.mp3");
|
||||
const finalAudioFile = path.join(finalAudioDir, "final.mp3");
|
||||
const backgroundMusicFile = path.resolve("stories", storyName, storyConfig.config.background_music_file);
|
||||
|
||||
fs.mkdirSync(finalAudioDir, { recursive: true });
|
||||
|
||||
const allFiles = [introFile, ...audioFiles.map((f) => path.resolve(f)), outroFile];
|
||||
const fileList = allFiles.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n");
|
||||
const listFile = path.resolve("stories", storyName, "filelist.txt");
|
||||
fs.writeFileSync(listFile, fileList);
|
||||
|
||||
const concatArgs = ["-y", "-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", tempAudioFile];
|
||||
|
||||
const concatProcess = spawn(ffmpegPath, concatArgs);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
concatProcess.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`ffmpeg concat process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const duration = await getDuration(tempAudioFile);
|
||||
|
||||
if (!fs.existsSync(backgroundMusicFile)) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const args = ["-y", "-i", tempAudioFile, "-c:a", "libmp3lame", "-q:a", "4", finalAudioFile];
|
||||
const p = spawn(ffmpegPath, args);
|
||||
p.on("close", (code: number | null) =>
|
||||
code === 0 ? resolve() : reject(new Error(`ffmpeg copy failed ${code}`))
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const bgVolume = "0.2";
|
||||
const mixArgs = [
|
||||
"-y",
|
||||
"-i",
|
||||
tempAudioFile,
|
||||
"-stream_loop",
|
||||
"-1",
|
||||
"-i",
|
||||
backgroundMusicFile,
|
||||
"-filter_complex",
|
||||
`[1:a]volume=${bgVolume},atrim=0:${duration},asetpts=N/SR/TB[bg];[0:a][bg]amix=inputs=2:duration=first:dropout_transition=0[a]`,
|
||||
"-map",
|
||||
"[a]",
|
||||
"-c:a",
|
||||
"libmp3lame",
|
||||
"-q:a",
|
||||
"4",
|
||||
finalAudioFile,
|
||||
];
|
||||
|
||||
const mixProcess = spawn(ffmpegPath, mixArgs);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
mixProcess.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`ffmpeg mix process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user