ai-tube-clap-exporter / src /core /ffmpeg /concatenateAudio.mts
jbilcke-hf's picture
jbilcke-hf HF staff
initial commit 🎬
2cae2a9
raw
history blame
3.76 kB
import { existsSync, promises as fs } from "node:fs"
import os from "node:os"
import path from "node:path"
import { v4 as uuidv4 } from "uuid";
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg";
import { writeBase64ToFile } from "../files/writeBase64ToFile.mts";
import { getMediaInfo } from "./getMediaInfo.mts";
import { removeTemporaryFiles } from "../files/removeTmpFiles.mts";
import { addBase64Header } from "../base64/addBase64.mts";
export type ConcatenateAudioOptions = {
// those are base64 audio strings!
audioTracks?: string[]; // base64
audioFilePaths?: string[]; // path
crossfadeDurationInSec?: number;
outputFormat?: string; // "wav" or "mp3"
output?: string;
}
export type ConcatenateAudioOutput = {
filepath: string;
durationInSec: number;
}
export async function concatenateAudio({
output,
audioTracks = [],
audioFilePaths = [],
crossfadeDurationInSec = 10,
outputFormat = "wav"
}: ConcatenateAudioOptions): Promise<ConcatenateAudioOutput> {
if (!Array.isArray(audioTracks)) {
throw new Error("Audios must be provided in an array");
}
const tempDir = path.join(os.tmpdir(), uuidv4());
await fs.mkdir(tempDir);
// console.log(" |- created tmp dir")
// trivial case: there is only one audio to concatenate!
if (audioTracks.length === 1 && audioTracks[0]) {
const audioTrack = audioTracks[0]
const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`);
await writeBase64ToFile(addBase64Header(audioTrack, "wav"), outputFilePath);
// console.log(" |- there is only one track! so.. returning that")
const { durationInSec } = await getMediaInfo(outputFilePath);
return { filepath: outputFilePath, durationInSec };
}
if (audioFilePaths.length === 1) {
throw new Error("concatenating a single audio file path is not implemented yet")
}
try {
let i = 0
for (const track of audioTracks) {
if (!track) { continue }
const audioFilePath = path.join(tempDir, `audio_${++i}.wav`);
await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath);
audioFilePaths.push(audioFilePath);
}
audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio))
const outputFilePath = output ?? path.join(tempDir, `${uuidv4()}.${outputFormat}`);
let filterComplex = "";
let prevLabel = "0";
for (let i = 0; i < audioFilePaths.length - 1; i++) {
const nextLabel = `a${i}`;
filterComplex += `[${prevLabel}][${i + 1}]acrossfade=d=${crossfadeDurationInSec}:c1=tri:c2=tri[${nextLabel}];`;
prevLabel = nextLabel;
}
console.log(" |- concatenateAudio(): DEBUG:", {
tempDir,
audioFilePaths,
outputFilePath,
filterComplex,
prevLabel
})
let cmd: FfmpegCommand = ffmpeg() // .outputOptions('-vn');
audioFilePaths.forEach((audio, i) => {
cmd = cmd.input(audio);
});
const promise = new Promise<ConcatenateAudioOutput>((resolve, reject) => {
cmd = cmd
.on('error', reject)
.on('end', async () => {
try {
const { durationInSec } = await getMediaInfo(outputFilePath);
// console.log("concatenation ended! see ->", outputFilePath)
resolve({ filepath: outputFilePath, durationInSec });
} catch (err) {
reject(err);
}
})
.complexFilter(filterComplex, prevLabel)
.save(outputFilePath);
});
const result = await promise
return result
} catch (error) {
console.error(`Failed to assemble audio!`)
console.error(error)
throw new Error(`Failed to assemble audio: ${(error as Error)?.message || error}`);
} finally {
await removeTemporaryFiles(audioFilePaths)
}
}