Search code examples
node.jsffmpegecmascript-6es6-promisefluent-ffmpeg

FFmpeg Fade Out Audio Filter (afade) not being Applied to Track Clipped Iteratively


I am trying to split a track into multiple fixed-sized (30-second) clips, each with a (5-second) fade-in / fade-out at the beginning and end, respectively. I'm using node-fluent-ffmpeg to interface with ffmpeg and saving each of the ffmpeg commands in an array of ES6 Promises that I later execute using Promise.all().

I am able to clip the tracks and add the fade-in filter successfully, but for some reason the fade-out filter is only applied to the first clip of the track. I have looked around for answers both in the ffmpeg and node-fluent-ffmpeg documentation (here and here), but there is no mention of issues arising from applying fade-out filters to a track that is being clipped multiple times.

My code is very similar to the snippet below, with the audio filters being applied in sequence using the audioFilters method. Note that I have tried leaving only the fade-out filter, but the problem persists. Any pointers would be greatly appreciated.

var promises = [];
const duration = track.duration;
const interval = 30;
const fade = 5;             
const bitrate = 128;        

for (var i = 0; i <= Math.floor(duration) - interval; ++i) {
    const start = i;            // Start second.      
    const end = start + interval;
    const mp3 = `${new ObjectId().toHexString()}.mp3`;

    var command = new Promise((resolve, reject) => {
        ffmpeg(path).setStartTime(start)
                    .audioBitrate(bitrate)
                    .audioFilters([
                        {
                            filter: 'afade',
                            options: `t=in:ss=${start}:d=${fade}`
                        },
                        {
                            filter: 'afade',
                            options: `t=out:st=${end - fade}:d=${fade}`
                        }
                    ])
                    .duration(interval)
                    .on('error', (err) => {
                        reject("An error occurred while clipping.");
                    })
                    .on('end', () => {
                        resolve(`Finished processing ${output}.`);
                    })
                    .save(mp3);
    });
    promises.push(command);     
}

And here is my ffmpeg version information:

ffmpeg version 2.8.6 Copyright (c) 2000-2016 the FFmpeg developers
built with Apple LLVM version 7.0.2 (clang-700.1.81)
configuration: --prefix=/usr/local/Cellar/ffmpeg/2.8.6 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-opencl --enable-libx264 --enable-libmp3lame --enable-libvo-aacenc --enable-libxvid --enable-vda
libavutil      54. 31.100 / 54. 31.100
libavcodec     56. 60.100 / 56. 60.100
libavformat    56. 40.101 / 56. 40.101
libavdevice    56.  4.100 / 56.  4.100
libavfilter     5. 40.101 /  5. 40.101
libavresample   2.  1.  0 /  2.  1.  0
libswscale      3.  1.101 /  3.  1.101
libswresample   1.  2.101 /  1.  2.101
libpostproc    53.  3.100 / 53.  3.100

Solution

  • After a while of tinkering around, clipping different tracks with the code above, I figured out the issue.

    The Problem

    The problem was with how I kept track of the start and end times for the fades. It turns out that the times for the fade were being measured with respect to the 30-second clips, not the original track itself. I, on the other hand, kept increasing the start and end times using the loop below, thinking the fade was applied at the time step in relation to the original track.

    for (var i = 0; i <= Math.floor(duration) - interval; ++i) {
        const start = i;            // Start second.      
        const end = start + interval;
        ...
    

    So by updating the start and end values with the loop, I was going beyond the end of the 30-second clip when I applied the audio filters.

    .audioFilters([
        {
            filter: 'afade',
            options: `t=in:ss=${start}:d=${fade}`
        },
        {
            filter: 'afade',
            options: `t=out:st=${end - fade}:d=${fade}`
        }
    ])
    

    That is why I could only hear the fade out for the first clips. The fade-in was not working either, but it was harder to notice, because the first few clips would have a fade in that sounded natural (even though it was also moving up the track and eventually not being applied).

    The Solution

    Turns out it was super simple. I just needed to replace the values in the options key so that the start for the fade in was always 0 and the start for the fade out was always the interval length (30) minus the fade duration (3).

    .audioFilters([
        {
            filter: 'afade',
            options: `t=in:ss=0:d=${fade}`
        },
        {
            filter: 'afade',
            options: `t=out:st=${interval - fade}:d=${fade}`
        }
    ])
    

    It was a simple issue in the end. Nevertheless, it's good to point out that if you're clipping and applying a fade at the same time, you need to make sure your times are with regards to the clipped track, not the original one.