-
-
Save markheath/8fb396a5fe4bf117f361 to your computer and use it in GitHub Desktop.
// Define other methods and classes here | |
/// <summary> | |
/// Sample Provider to allow fading in and out | |
/// </summary> | |
public class DelayFadeOutSampleProvider : ISampleProvider | |
{ | |
enum FadeState | |
{ | |
Silence, | |
FadingIn, | |
FullVolume, | |
FadingOut, | |
} | |
private readonly object lockObject = new object(); | |
private readonly ISampleProvider source; | |
private int fadeSamplePosition; | |
private int fadeSampleCount; | |
private int fadeOutDelaySamples; | |
private int fadeOutDelayPosition; | |
private FadeState fadeState; | |
/// <summary> | |
/// Creates a new FadeInOutSampleProvider | |
/// </summary> | |
/// <param name="source">The source stream with the audio to be faded in or out</param> | |
/// <param name="initiallySilent">If true, we start faded out</param> | |
public DelayFadeOutSampleProvider(ISampleProvider source, bool initiallySilent = false) | |
{ | |
this.source = source; | |
this.fadeState = initiallySilent ? FadeState.Silence : FadeState.FullVolume; | |
} | |
/// <summary> | |
/// Requests that a fade-in begins (will start on the next call to Read) | |
/// </summary> | |
/// <param name="fadeDurationInMilliseconds">Duration of fade in milliseconds</param> | |
public void BeginFadeIn(double fadeDurationInMilliseconds) | |
{ | |
lock (lockObject) | |
{ | |
fadeSamplePosition = 0; | |
fadeSampleCount = (int)((fadeDurationInMilliseconds * source.WaveFormat.SampleRate) / 1000); | |
fadeState = FadeState.FadingIn; | |
} | |
} | |
/// <summary> | |
/// Requests that a fade-out begins (will start on the next call to Read) | |
/// </summary> | |
/// <param name="fadeDurationInMilliseconds">Duration of fade in milliseconds</param> | |
public void BeginFadeOut(double fadeAfterMilliseconds, double fadeDurationInMilliseconds) | |
{ | |
lock (lockObject) | |
{ | |
fadeSamplePosition = 0; | |
fadeSampleCount = (int)((fadeDurationInMilliseconds * source.WaveFormat.SampleRate) / 1000); | |
fadeOutDelaySamples = (int)((fadeAfterMilliseconds * source.WaveFormat.SampleRate) / 1000); | |
fadeOutDelayPosition = 0; | |
//fadeState = FadeState.FadingOut; | |
} | |
} | |
/// <summary> | |
/// Reads samples from this sample provider | |
/// </summary> | |
/// <param name="buffer">Buffer to read into</param> | |
/// <param name="offset">Offset within buffer to write to</param> | |
/// <param name="count">Number of samples desired</param> | |
/// <returns>Number of samples read</returns> | |
public int Read(float[] buffer, int offset, int count) | |
{ | |
int sourceSamplesRead = source.Read(buffer, offset, count); | |
lock (lockObject) | |
{ | |
if (fadeOutDelaySamples > 0) | |
{ | |
fadeOutDelayPosition += sourceSamplesRead / WaveFormat.Channels; | |
if (fadeOutDelayPosition >= fadeOutDelaySamples) | |
{ | |
fadeOutDelaySamples = 0; | |
fadeState = FadeState.FadingOut; | |
} | |
} | |
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; | |
} | |
private static void ClearBuffer(float[] buffer, int offset, int count) | |
{ | |
for (int n = 0; n < count; n++) | |
{ | |
buffer[n + offset] = 0; | |
} | |
} | |
private void FadeOut(float[] buffer, int offset, int sourceSamplesRead) | |
{ | |
int sample = 0; | |
while (sample < sourceSamplesRead) | |
{ | |
float multiplier = 1.0f - (fadeSamplePosition / (float)fadeSampleCount); | |
for (int ch = 0; ch < source.WaveFormat.Channels; ch++) | |
{ | |
buffer[offset + sample++] *= multiplier; | |
} | |
fadeSamplePosition++; | |
if (fadeSamplePosition > fadeSampleCount) | |
{ | |
fadeState = FadeState.Silence; | |
// clear out the end | |
ClearBuffer(buffer, sample + offset, sourceSamplesRead - sample); | |
break; | |
} | |
} | |
} | |
private void FadeIn(float[] buffer, int offset, int sourceSamplesRead) | |
{ | |
int sample = 0; | |
while (sample < sourceSamplesRead) | |
{ | |
float multiplier = (fadeSamplePosition / (float)fadeSampleCount); | |
for (int ch = 0; ch < source.WaveFormat.Channels; ch++) | |
{ | |
buffer[offset + sample++] *= multiplier; | |
} | |
fadeSamplePosition++; | |
if (fadeSamplePosition > fadeSampleCount) | |
{ | |
fadeState = FadeState.FullVolume; | |
// no need to multiply any more | |
break; | |
} | |
} | |
} | |
/// <summary> | |
/// WaveFormat of this SampleProvider | |
/// </summary> | |
public WaveFormat WaveFormat | |
{ | |
get { return source.WaveFormat; } | |
} | |
} |
void Main() | |
{ | |
using(var reader = new AudioFileReader(@"D:\Audio\Music\Example.mp3")) | |
{ | |
var fadeOut = new DelayFadeOutSampleProvider(reader); | |
fadeOut.BeginFadeOut(10000, 2000); | |
using(var player = new WaveOutEvent()) | |
{ | |
player.Init(fadeOut); | |
player.Play(); | |
while(player.PlaybackState == PlaybackState.Playing) | |
{ | |
Thread.Sleep(500); | |
} | |
} | |
} | |
} | |
I just fixed a bug in the
Read
method which caused the fade out to be applied in the wrong position. Please update the gist. Here's my fork for reference: https://gist.github.com/Ahmed-Abdelhameed/b867a8cfe739cd7a128b73a33d317402. You can also refer to this SO question: https://stackoverflow.com/a/45997576/4934172.
I tried with your fix , but there are clicks when the audio file is cross-faded with itself , but no clicks were found with the original code
I'm getting clicks. The Read() function assumes the entire buffer is in one state (e.g. FadingIn or FadingOut) so it can't accommodate a short sample (or large buffer) where the start is fading in and the end is fading out.
I've had a go at reworking class. It will correctly fade in and out on short samples or large buffers, including if the fade out begins before the fade in has finished.
https://gist.github.com/pengowray/621ec0566199a0a22d51a51e1be77784
I've renamed BeginFadeOut() to SetFadeOut() to make it clear that it doesn't begin fading when you call it but that you're "queuing up" the fading. Also added some methods that take TimeSpans instead of milliseconds.
If you set a fade in, with SetFadeIn(), you can queue up when it starts too, and all samples prior to the fade in will be silence. No need to set "initiallySilent = true;" (though maybe there's some use-case where that might be needed?)
Please feel free to use my code in NAudio. My code's not especially efficient (I probably do too many range checks on each every sample), and it might be missing some comments. I also haven't tested everything thoroughly. But it's working for me, so hopefully it can be a helpful starting point.
I don't know if any subclass of ISampleProvider gives the sample length when it's known, but it would be nice if there was a way to fade out the last X seconds without specifying exactly when it needs to start fading.
Oops, just noticed my code assumes a mono source
trying to use the fadeout, but now it only plays 3 seconds of the song :(
``
using (var ms = File.OpenRead("C:\Users\Wellumies\source\repos\SquareDance\Data\music\" + name))
{
using (var rdr = new Mp3FileReader(ms))
{
// Apply fade-in effect
var fadeInProvider = new FadeInOutSampleProvider(rdr.ToSampleProvider(), true);
fadeInProvider.BeginFadeIn(3000); // 3-second fade-in
// Apply fade-out effect
var fadeOutProvider = new FadeInOutSampleProvider(fadeInProvider, false);
fadeOutProvider.BeginFadeOut(3000); // 3-second fade-out
// Create a WaveOutEvent to play the audio
using (var waveOut = new WaveOutEvent())
{
// Skip to the desired start time
rdr.Skip(start * 1000);
waveOut.Init(fadeInProvider);
waveOut.Init(fadeOutProvider);
waveOut.Play();
// Wait for the desired duration (60 seconds)
await Task.Delay(60 * 1000);
// Update the outputTextBox with numbers 3-2-1
Dispatcher.Invoke(() => outputTextBox.Text = "3");
await Task.Delay(1000);
Dispatcher.Invoke(() => outputTextBox.Text = "2");
await Task.Delay(1000);
Dispatcher.Invoke(() => outputTextBox.Text = "1");
await Task.Delay(1000);
waveOut.Stop();
}
}
}
``
The way the fadeout works is that it starts fading out immediately when you call BeginFadeOut. So you'd need to sleep 57 seconds and then start the fade-out. Or make a custom sampleprovider that tracks how far through it is, and then starts fading out when it detects it is 3 seconds from the end (which requires knowing in advance how long the source material is)
I'm facing one weird issue with this quick solution. It works fine until I move trackbar of tha song. For e.g if I move the trackbar then fadeout never happens.