n8n-nodes-audio-tools
Version:
Community audio processing nodes for n8n: concatWithGap & mergeTracks
169 lines (168 loc) • 7.87 kB
JavaScript
;
/* -------------------------------------------------------------------------
* nodes/AudioConcatWithGap.node.ts · v3
* – Concatène deux pistes audio binaires (file1 → silence → file2)
* – Pas de fichier final sur disque ; un petit fichier temp pour la 2ᵉ piste
* ------------------------------------------------------------------------- */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioConcatWithGap = void 0;
const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = __importDefault(require("path"));
const ffmpeg_utils_1 = require("../helpers/ffmpeg-utils");
class AudioConcatWithGap {
constructor() {
/* ────── Node metadata ────── */
this.description = {
name: "audioConcatWithGap",
displayName: "Audio · Concat With Gap",
group: ["transform"],
version: 3,
icon: "file:audio-tools.png",
description: "Binary in → binary out. First file · silence · second file, optional fades.",
defaults: { name: "Concat With Gap" },
inputs: ["main"],
outputs: ["main"],
properties: [
{
displayName: "File 1 Binary Property",
name: "file1Prop",
type: "string",
default: "file1",
},
{
displayName: "File 2 Binary Property",
name: "file2Prop",
type: "string",
default: "file2",
},
{
displayName: "Gap Duration (s)",
name: "gapDuration",
type: "number",
typeOptions: { minValue: 0 },
default: 0,
},
{
displayName: "Fade-out of File 1 (ms)",
name: "fadeOutMs",
type: "number",
typeOptions: { minValue: 0 },
default: 0,
},
{
displayName: "Fade-in of File 2 (ms)",
name: "fadeInMs",
type: "number",
typeOptions: { minValue: 0 },
default: 0,
},
{
displayName: "Output Format",
name: "outputFormat",
type: "options",
options: [
{ name: "Same as File 1", value: "auto" },
{ name: "WAV", value: "wav" },
{ name: "MP3", value: "mp3" },
{ name: "FLAC", value: "flac" },
],
default: "auto",
},
{
displayName: "Output Binary Property Name",
name: "binaryPropertyName",
type: "string",
default: "data",
},
],
};
}
/* ───────────────────────────────────────────────────────────────────── */
async execute() {
var _a, _b;
const items = this.getInputData();
const returnData = [];
for (let i = 0; i < items.length; i++) {
/* ────── params ────── */
const p1 = this.getNodeParameter("file1Prop", i);
const p2 = this.getNodeParameter("file2Prop", i);
const gapDur = this.getNodeParameter("gapDuration", i, 0);
const fadeOut = this.getNodeParameter("fadeOutMs", i, 0);
const fadeIn = this.getNodeParameter("fadeInMs", i, 0);
const outFmt = this.getNodeParameter("outputFormat", i, "auto");
const outProp = this.getNodeParameter("binaryPropertyName", i, "data");
/* ────── binaries ────── */
const bin1 = (_a = items[i].binary) === null || _a === void 0 ? void 0 : _a[p1];
const bin2 = (_b = items[i].binary) === null || _b === void 0 ? void 0 : _b[p2];
(0, ffmpeg_utils_1.ensureBinaryExists)(bin1, p1);
(0, ffmpeg_utils_1.ensureBinaryExists)(bin2, p2);
const buf1 = Buffer.from(bin1.data, "base64");
const buf2 = Buffer.from(bin2.data, "base64");
const ext1 = (0, ffmpeg_utils_1.getExtensionFromName)(bin1.fileName);
const ext2 = (0, ffmpeg_utils_1.getExtensionFromName)(bin2.fileName);
const fmt = (outFmt === "auto" ? ext1 : outFmt);
/* ────── write buf2 → fichier temp (limite 1 stream de fluent-ffmpeg) ────── */
const tmpFile2 = path_1.default.join((0, os_1.tmpdir)(), `n8n-audio-${Date.now()}-${Math.random()
.toString(36)
.slice(2)}.${ext2}`);
await fs_1.promises.writeFile(tmpFile2, buf2);
try {
/* ────── FFmpeg cmd ────── */
let cmd = (0, fluent_ffmpeg_1.default)()
.input((0, ffmpeg_utils_1.bufferToStream)(buf1))
.inputFormat(ext1) // 1er flux
.input(`anullsrc=r=44100:cl=stereo:d=${gapDur}`) // silence
.inputOptions(["-f", "lavfi"])
.input(tmpFile2); // 2ᵉ piste (fichier)
/* ────── filtergraph ────── */
const filters = [];
// fade-out piste 1
if (fadeOut > 0) {
const d1 = await (0, ffmpeg_utils_1.probeDurationBuffer)(buf1, ext1);
filters.push(`[0:a]afade=t=out:st=${d1 - fadeOut / 1000}:d=${fadeOut / 1000}[a0]`);
}
else
filters.push("[0:a]anull[a0]");
// fade-in piste 2
if (fadeIn > 0)
filters.push(`[2:a]afade=t=in:st=0:d=${fadeIn / 1000}[a2]`);
else
filters.push("[2:a]anull[a2]");
// concat + normalise
filters.push("[a0][1:a][a2]concat=n=3:v=0:a=1, dynaudnorm[out]");
cmd = cmd.complexFilter(filters, "out");
/* ────── codecs ────── */
switch (fmt) {
case "mp3":
cmd.audioCodec("libmp3lame").outputOptions([
"-q:a",
"0",
]);
break;
case "wav":
cmd.audioCodec("pcm_s16le");
break;
case "flac":
cmd.audioCodec("flac");
break;
}
/* ────── run ────── */
const outBuf = await (0, ffmpeg_utils_1.runFfmpegToBuffer)(cmd, fmt);
const binary = await this.helpers.prepareBinaryData(outBuf, `concat.${fmt}`);
returnData.push({ json: {}, binary: { [outProp]: binary } });
}
finally {
/* nettoyage fichier temp */
await fs_1.promises.unlink(tmpFile2).catch(() => { });
}
}
return [returnData];
}
}
exports.AudioConcatWithGap = AudioConcatWithGap;
exports.default = AudioConcatWithGap;