Concatenating Sample Providers in NAudio
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.
Comments
It's perfect for my radio automation solution.
Anderson NunesEx. 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)
Hi Mark, me again. :þ
Anderson NunesIt's possible convert Sample Provider from 2 concatenated to MediaFoundationReader?
Thank you.
I don't understand what you are trying to achieve? MediaFoundationReader is for reading audio files from disk
Mark HeathSorry, i misexplained.
Anderson NunesNow, 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))
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 HeathGreat.
Anderson NunesI will research about this.
Thank you Mark.
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 AllanThanks 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