Skip to content

Instantly share code, notes, and snippets.

@remorses
Last active August 26, 2025 11:53
Show Gist options
  • Save remorses/4d32dbce64147a1dda28d08e7dec8a8e to your computer and use it in GitHub Desktop.
Save remorses/4d32dbce64147a1dda28d08e7dec8a8e to your computer and use it in GitHub Desktop.
ffmpeg concatenating audio clips the good way. no frozen frames, no gaps
interface InputFile {
path: string
start?: number // Start time for trimming (in seconds)
end?: number // End time for trimming (in seconds)
fadeIn?: number // Fade in duration in seconds
fadeOut?: number // Fade out duration in seconds
}
interface ConcatenateOptions {
inputFiles: InputFile[]
outputFile: string
signal?: AbortSignal
}
export async function concatenateAudio(
options: ConcatenateOptions,
): Promise<void> {
const { inputFiles, outputFile, signal } = options
if (!inputFiles?.length || !outputFile) {
throw new Error('Missing required parameters')
}
// Generate stable ID for timing
const timerId = `concat-${inputFiles.length}-videos-${outputFile.split('/').pop()}`
console.time(timerId)
// Build input file arguments for FFmpeg
const inputArgs = inputFiles.map((file) => `-i "${file.path}"`).join(' ')
// Build the filter_complex string
const filterComplexParts: string[] = []
const streamParts: string[] = []
inputFiles.forEach((file, index) => {
let processedAudio = `[${index}:a]`
// Add trimming if start and/or end are defined
if (file.start !== undefined || file.end !== undefined) {
const start = file.start ?? 0
const end = file.end ? `end=${file.end}` : ''
filterComplexParts.push(
`[${index}:a]atrim=start=${start}:${end},asetpts=PTS-STARTPTS[trim${index}]`,
)
processedAudio = `[trim${index}]`
}
// Add fade in/out if defined
if (file.fadeIn || file.fadeOut) {
if (file.fadeIn) {
filterComplexParts.push(
`${processedAudio}afade=t=in:st=0:d=${file.fadeIn}[fadein${index}]`,
)
processedAudio = `[fadein${index}]`
}
if (file.fadeOut && file.end) {
const fadeStart = file.end - file.fadeOut
filterComplexParts.push(
`${processedAudio}afade=t=out:st=${fadeStart}:d=${file.fadeOut}[fadeout${index}]`,
)
processedAudio = `[fadeout${index}]`
}
}
streamParts.push(processedAudio)
})
// Concatenate audio streams
filterComplexParts.push(
`${streamParts.join('')}concat=n=${inputFiles.length}:v=0:a=1[a_out]`,
)
const filterComplex = filterComplexParts.join(';')
// Build the FFmpeg command
const command = `ffmpeg -y ${inputArgs} -filter_complex "${filterComplex}" -map "[a_out]" "${outputFile}"`
// Execute the command
console.log('Running FFmpeg command:', command)
try {
await new Promise((resolve, reject) => {
const childProcess = spawn(command, {
shell: true,
env: process.env,
// stdio: 'inherit',
})
childProcess.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`FFmpeg exited with code ${code}`))
} else {
resolve(null)
}
})
if (signal) {
signal.addEventListener('abort', () => {
childProcess.kill()
reject(new Error('Operation aborted'))
})
}
})
} finally {
console.timeEnd(timerId)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment