2
votes

I'm using NAudio to convert & trim some audio files, and I'm trying to add a fade-out to the last few seconds of each file.

I have checked this question, this, and this, but all the answers are talking about playing the wav file with fade, while I need to actually write that fade to an output file.

So, is there any way to do this using NAudio? If not, I'm open to other suggestions.


Edit: This is what I've tried:

private void PerformFadeOut(string inputPath, string outputPath)
{
    WaveFileReader waveSource = new WaveFileReader(inputPath);

    ISampleProvider sampleSource = waveSource.ToSampleProvider();

    OffsetSampleProvider fadeOutSource = new OffsetSampleProvider(sampleSource);
    // Assume the length of the audio file is 122 seconds.
    fadeOutSource.SkipOver = TimeSpan.FromSeconds(120);   // Hard-coded values for brevity

    // Two seconds fade
    var fadeOut = new FadeInOutSampleProvider(fadeOutSource);
    fadeOut.BeginFadeOut(2000);

    Player = new WaveOut();

    Player.Init(fadeOut);
    Player.Play();    
}

When I play the audio after applying the fade using Player.Play() -as shown in the method above-, it works perfectly as expected, and I can hear the fade. Now, I would like to export this result to an output WAV file.

I tried doing that by adding the following line:

WaveFileWriter.CreateWaveFile(outputPath, waveSource);

However, the output file doesn't have any fade applied to it. So, what am I missing here?

2
Did you try with WaveFileWriter.CreateWaveFile(outputPath, fadeOut); instead?yms
@yms CreateWaveFile method is expecting a second argument of type IWaveProvider. Apparently, FadeInOutSampleProvider class doesn't implement this interface, hence can't be casted.41686d6564
Then WaveFileWriter.CreateWaveFile16(outputPath, fadeOut) or WaveFileWriter.CreateWaveFile( outputPath, new SampleToWaveProvider(fadeOut)). See here: github.com/naudio/NAudio/blob/master/NAudio/Wave/… and here: github.com/naudio/NAudio/blob/master/NAudio/Wave/WaveOutputs/…yms
@yms CreateWaveFile16 doesn't work also because it expects the same type of arguments. But the second suggestion might actually work. Let me try it..41686d6564
" it expects the same type of arguments" no it does not... at least not in the source code that pointed to: public static void CreateWaveFile16(string filename, ISampleProvider sourceProvider)yms

2 Answers

3
votes

Okay, let's wrap everything up in case someone encounters the same issue in the future:

With the great help of @yms, I managed to write the fade to a file by using:

WaveFileWriter.CreateWaveFile(outputPath, new SampleToWaveProvider(fadeOut));

But that caused the wave writer to only write the last two seconds which makes sense, so I tried using the DelayFadeOutSampleProvider class instead of FadeInOutSampleProvider. With that I was able to write the whole file, but it ended up causing the fading to start in a wrong position (it's more obvious when saving though. Not when playing).

I generated a 10 seconds wav file with Audacity, and used the following method for testing:

private static void PerformFadeOut(string inputPath, string outputPath, bool playNoSave = false)
{
    WaveFileReader waveSource = new WaveFileReader(inputPath);

    ISampleProvider sampleSource = waveSource.ToSampleProvider();

    // Two seconds fade
    var fadeOut = new DelayFadeOutSampleProvider(sampleSource);
    fadeOut.BeginFadeOut(8000, 2000);

    if(playNoSave)
    {
        // Here the fade is played exactly where expected (@00:08)
        var player = new WaveOut();
        player.Init(fadeOut);
        player.Play();
    }
    else
    {
        // But when saving, the fade is applied @00:04!
        WaveFileWriter.CreateWaveFile(outputPath, new SampleToWaveProvider(fadeOut));
    }
}

Here's the file, before & after writing the fade-out:

Before & after writing the fade-out

As shown above, the fade-out doesn't start at the right position.

After some investigation in the DelayFadeOutSampleProvider, I found a bug in the Read method, so I modified it like this:

public int Read(float[] buffer, int offset, int count)
{
    int sourceSamplesRead = source.Read(buffer, offset, count);

    lock (lockObject)
    {
        if (fadeOutDelaySamples > 0)
        {
            int oldFadeOutDelayPos = fadeOutDelayPosition;
            fadeOutDelayPosition += sourceSamplesRead / WaveFormat.Channels;
            if (fadeOutDelayPosition > fadeOutDelaySamples)
            {
                int normalSamples = (fadeOutDelaySamples - oldFadeOutDelayPos) * WaveFormat.Channels;
                int fadeOutSamples = (fadeOutDelayPosition - fadeOutDelaySamples) * WaveFormat.Channels;
                // apply the fade-out only to the samples after fadeOutDelayPosition
                FadeOut(buffer, offset + normalSamples, fadeOutSamples);

                fadeOutDelaySamples = 0;
                fadeState = FadeState.FadingOut;
                return sourceSamplesRead;
            }
        }
        if (fadeState == FadeState.FadingIn)
        {
            FadeIn(buffer, offset, sourceSamplesRead);
        }
        else if (fadeState == FadeState.FadingOut)
        {
            FadeOut(buffer, offset, sourceSamplesRead);
        }
        else if (fadeState == FadeState.Silence)
        {
            ClearBuffer(buffer, offset, count);
        }
    }
    return sourceSamplesRead;
}

And now everything works just fine.

Here's my fork of the whole class if someone is interested, and I already asked the author (@mark-heath) to update the original gist with this fix.

1
votes

You original code was using the original waveSource as input, which is why you had no fade.

The following alternative uses the fadeOut object:

WaveFileWriter.CreateWaveFile16(outputPath, fadeOut);

The signature of CreateWaveFile16 would be:

public static void CreateWaveFile16(string filename, ISampleProvider sourceProvider)

This can be seen in the source code of the class WaveFileWriter.

Another alternative is to use the class SampleToWaveProvider and covert your fadeOut object into an IWaveProvider, and that allows you to use the regular CreateWaveFile method.

WaveFileWriter.CreateWaveFile(outputPath, new SampleToWaveProvider(fadeOut))

As you know, in all cases your output file will only contain the last k seconds corresponding to the fadeout, a different class is needed if you want the whole file with a fadeout.