Search code examples
c#.netaudiowavnaudio

How to properly write a fade-out to a WAV file using NAudio?


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?


Solution

  • 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.