File size: 3,760 Bytes
2cae2a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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)
  }
}