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");

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.
Vote on HN
comments powered by Disqus