UNPKG

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
'use strict'; 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;