ffmpeg-audio-mixer
Version:
A node audio processing library using FFmpeg. It can convert audio files and mix them.
169 lines (163 loc) • 4.95 kB
JavaScript
;
var child_process = require('child_process');
function execCommand(command, args, outputStream) {
const process = child_process.spawn(command, args);
process.stderr.setEncoding("utf-8");
const execPromise = new Promise((resolve, reject) => {
let processHasError = false;
process.on("error", () => {
processHasError = true;
reject(new Error("FFmpeg is not available."));
});
process.on("close", (code, signal) => {
if (!processHasError && signal !== null) {
reject(new Error(`FFmpeg was killed with signal ${signal}`));
} else if (!processHasError && code !== 0) {
reject(new Error(`FFmpeg exited with code ${code}`));
} else if (!processHasError) {
resolve();
}
});
});
if (outputStream) {
execPromise.catch((e) => {
throw e;
});
return process.stdout;
} else {
return execPromise;
}
}
function getFfmpegArgs(inputs, options, output) {
const args = [];
args.push("-hide_banner", "-loglevel", "error");
if (output.kind === "file") {
args.push("-y");
}
for (const input of inputs) {
args.push("-i", input.inputFile);
}
const complexFilters = getMixedComplexFilters(inputs, options.normalize);
if (options.volume) {
complexFilters.push(`volume=${options.volume}`);
}
if (complexFilters.length > 0) {
args.push("-filter_complex", complexFilters.join(","));
}
if (options.codec !== void 0) {
args.push("-codec:a", options.codec);
}
if (options.frames !== void 0) {
args.push("-frames:a", `${options.frames}`);
}
if (options.sampleFormat !== void 0) {
args.push("-sample_fmt:a", options.sampleFormat);
}
if (output.kind === "stream") {
args.push("-f", output.fileFormat);
}
args.push(output.kind === "file" ? output.fileName : "pipe:1");
return args;
}
function getMixedComplexFilters(inputs, normalize = false) {
const complexFilters = [];
const inputsNames = Object.keys(inputs);
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
const hasDelay = input.delay !== void 0 && input.delay !== 0;
if (input.trim !== void 0 && (input.trim.start || input.trim.end)) {
complexFilters.push(
`[${i}]atrim=${[
input.trim.start ? `start=${input.trim.start}` : "",
input.trim.end && (!input.trim.start || input.trim.end > input.trim.start) ? `end=${input.trim.end}` : ""
].filter((attr) => Boolean(attr)).join(":")}${inputs.length > 1 || hasDelay ? `[${i}bis]` : ""}`
);
inputsNames[i] = `${i}bis`;
}
if (hasDelay) {
complexFilters.push(
`[${inputsNames[i]}]adelay=${input.delay}:1${inputs.length > 1 ? `[${i}ter]` : ""}`
);
inputsNames[i] = `${i}ter`;
}
}
if (inputs.length > 1) {
complexFilters.push(
`[${inputsNames.join("][")}]amix=inputs=${inputs.length}:duration=longest:weights=${inputs.map((i) => i.weight ?? 1).join(" ")}:normalize=${normalize ? "1" : "0"}`
);
}
return complexFilters;
}
const getInput = (input) => typeof input === "string" ? {
inputFile: input
} : input;
class AudioMixer {
constructor(inputs, options) {
this.inputs = Array.isArray(inputs) ? inputs.map(getInput) : [getInput(inputs)];
this.options = options || {};
}
// --- Inputs ---
addInput(input) {
this.inputs.push(getInput(input));
return this;
}
// --- Options ---
setOptions(options) {
this.options = options;
return this;
}
setFrames(frames) {
this.options.frames = frames;
return this;
}
setCodec(codec) {
this.options.codec = codec;
return this;
}
setSampleFormat(sampleFormat) {
this.options.sampleFormat = sampleFormat;
return this;
}
setVolume(volume) {
this.options.volume = volume;
return this;
}
// --- Processing ---
async toFile(outputFileName) {
if (!outputFileName) {
throw new Error("Missing output file path");
}
const args = getFfmpegArgs(this.inputs, this.options, {
kind: "file",
fileName: outputFileName
});
await execCommand("ffmpeg", args);
}
toStream(fileFormat) {
if (!fileFormat) {
throw new Error("Missing output file format");
}
const args = getFfmpegArgs(this.inputs, this.options, {
kind: "stream",
fileFormat
});
return execCommand("ffmpeg", args, true);
}
toBuffer(fileFormat) {
const outputStream = this.toStream(fileFormat);
return new Promise((resolve, reject) => {
const _buf = Array();
outputStream.on("data", (chunk) => _buf.push(chunk));
outputStream.on("end", () => resolve(Buffer.concat(_buf)));
outputStream.on(
"error",
(err) => reject(`Error converting stream audio to buffer: ${err}`)
);
});
}
}
function mixAudio(inputs, options) {
return new AudioMixer(inputs, options);
}
exports.AudioMixer = AudioMixer;
exports.mixAudio = mixAudio;