Posted in:

Every now and then I get asked about how to concatenate different pieces of audio with NAudio. For example, you might have two WAV or MP3 files and want to play one after the other. The way I usually do this is to create my own custom ISampleProvider (or IWaveProvider) that took all the inputs to be concatenated in the constructor. So long as the WaveFormat of all the inputs was the same, it’s simply a case of reading from the first one until it is finished (i.e. Read returns 0), then reading from the second, and so on, until you reach the end of the list.

However, I thought it was about time I added something into NAudio, so I’ve created a simple ConcatenatingSampleProvider class.

The constructor simply takes the providers you want to concatenate, and does a few sanity checks on them:

public ConcatenatingSampleProvider(IEnumerable<ISampleProvider> providers)
{
    if (providers == null) throw new ArgumentNullException("providers");
    this.providers = providers.ToArray();
    if (this.providers.Length == 0) throw new ArgumentException("Must provide at least one input", "providers");
    if (this.providers.Any(p => p.WaveFormat.Channels != WaveFormat.Channels)) throw new ArgumentException("All inputs must have the same channel count", "providers");
    if (this.providers.Any(p => p.WaveFormat.SampleRate != WaveFormat.SampleRate)) throw new ArgumentException("All inputs must have the same sample rate", "providers");
}

And then the Read method simply has to keep track of which one we are reading from, and move on to the next one if necessary:

public int Read(float[] buffer, int offset, int count)
{
    var read = 0;
    while (read < count && currentProviderIndex < providers.Length)
    {
        var needed = count - read;
        var readThisTime = providers[currentProviderIndex].Read(buffer, read, needed);
        read += readThisTime;
        if (readThisTime == 0) currentProviderIndex++;
    }
    return read;
}

It’s really straightforward to use, but I also wanted to take the opportunity to put the first few steps in place for a fluent API that I’ve intended to bring to NAudio for a long time. What if you could say something like this:

var file1 = new AudioFileReader("something.mp3");
var file2 = new AudioFileReader("another.mp3");
waveOut.Init(file1.FollowedBy(file2));

Well that’s now possible thanks to a very simple extension method I’ve added:

public static ISampleProvider FollowedBy(this ISampleProvider sampleProvider, ISampleProvider next)
{
    return new ConcatenatingSampleProvider(new[] { sampleProvider, next});
}

And I added another for the case of when you want to insert some silence between the concatenated files. This implementation uses an OffsetSampleProvider to do the silence insertion, as it already has a convenient LeadOut property that adds some silence to the end of a ISampleProvider.

public static ISampleProvider FollowedBy(this ISampleProvider sampleProvider, TimeSpan silenceDuration, ISampleProvider next)
{
    var silenceAppended = new OffsetSampleProvider(sampleProvider) {LeadOut = silenceDuration};
    return new ConcatenatingSampleProvider(new[] { silenceAppended, next });
}

For completeness, I suppose it would be good to do the same and make a ConcatenatingWaveProvider and a ConcatenatingWaveStream. The WaveStream version would be a bit more complex, because WaveStreams in NAudio can report Position and Length and support repositioning. But it would be quite possible to do, and would be useful if you wanted to support looping or repositioning.

These new methods will be part of the next release of NAudio.

Want to get up to speed with the the fundamentals principles of digital audio and how to got about writing audio applications with NAudio? Be sure to check out my Pluralsight courses, Digital Audio Fundamentals, and Audio Programming with NAudio.

Comments

Comment by Anderson Nunes

It's perfect for my radio automation solution.
Ex. To concatenate hour and minutos to play a automated time announcer.
I swaped other old lib (Z) for NAudio and rewrite all of the start,
Thanks for your amazing job.
I'm writing the software in VB.Net and NAudio is simply a heart of it.
(8 years of a solitary project lol)

Anderson Nunes
Comment by Anderson Nunes

Hi Mark, me again. :þ
It's possible convert Sample Provider from 2 concatenated to MediaFoundationReader?
Thank you.

Anderson Nunes
Comment by Mark Heath

I don't understand what you are trying to achieve? MediaFoundationReader is for reading audio files from disk

Mark Heath
Comment by Anderson Nunes

Sorry, i misexplained.
Now, I changed all MediaFoundationReader on project to AudioFileReader
I have a function that concatenate 3 WAV files to play a automatic time.
I need to do the same at especific rotine where i get lenght and position from a AudioFileReader source.
I need to pass the result from concatenated files to this resource.
Any ideas?
'reader(1) is fixed, the payload to change it is too much
'reader(1) = New AudioFileReader(pathhora & Now.Hour.ToString.PadLeft(2, "0") & Now.Minute.ToString.PadLeft(2, "0") & ".mp3")

Dim mug As New AudioFileReader(pathhora & "TOP.wav")
Dim h As New AudioFileReader(pathhora & "H" & Now.Hour.ToString.PadLeft(2, "0") & ".wav")
Dim m As New AudioFileReader(pathhora & "M" & Now.Minute.ToString.PadLeft(2, "0") & ".wav")
Dim hora As ConcatenatingSampleProvider = New ConcatenatingSampleProvider({mug, h, m})

'I need to do something as this (fake):
'reader(1) = hora

lblDuracao1.Text = (TimeSpan.FromMilliseconds((reader(1).TotalTime.TotalMilliseconds)).ToString.PadRight(12, "0")).ToString.Substring(0, 12)
barProgresso1.Maximum = reader(1).TotalTime.TotalMilliseconds
barProgresso1.SetMixSize = 500
decks(1).Init(reader(1))

Anderson Nunes
Comment by Mark Heath

NAudio's support for this isn't brilliant, but you can reposition the readers to the place where their audio starts, and use the Take extension method to only read a certain period. For your case though I'd usually recommend implementing my own custom ISampleProvider

Mark Heath
Comment by Anderson Nunes

Great.
I will research about this.
Thank you Mark.

Anderson Nunes
Comment by Dean Allan

Hi Mark, how about playing the same file more than once? I have three file readers and I build my output by adding those files multiple times in the the 'playlist'. I reset the reader position each time the file is added to my List<isampleprovider>. On debug I can see the files in there and their lengths are correct but when I send that list to the output device I only get each sample played once. Is there a way around this?

Dean Allan
Comment by Elektro Csg

Thanks but... and where is the complete source code of that class?. I really can't understand why you share just a piece of incomplete code. I suppose that constructor is for a class that implements ISampleProvider, but I can't figure out how to implement its WaveFormat property.

Elektro Csg