Saturday, February 28, 2009

NAudio, Tutorial 2, Mixing multiple wave files together in real time.

Introduction:

Assuming you have read my last post about NAudio, if you haven't there is still time - here; we will move on to mixing multiple audio files and outputting to a single audio device, which would be your sound card. For the purposes of simplicity this tutorial is going to only focus on 4 samples, although you could extend this to as many as you like following this structure.

Also please note that this Tutorial is not focusing on the most optimal way to implement the complete set of functions for a wave stream in terms of mixing and as such no additional inheritance or polymorphism has been used - the focus of this tutorial is to demonstrate the capabilities present within the NAudio library and what can be done when using these interfaces directly with out additional abstraction. I say this now because I have actually hacked to bits, what looks like a great level of abstraction that Mark has written in his demo application MixDiff - so after you have finished reading this check out the class MixDiffStream.cs for a really useful set of functions grouping many of these control arrays in to a single object.

Moving on with the Tutorial; I've created a form with 4 buttons on it. We will load a sample against each of these buttons and then use the buttons to trigger the sample for playback, giving us a form to mix the audio samples together in real time - the start of any great Synthesizer or Beat Box. For those of you not using your imagination today we have:


The Code - Declarations:

I'll assume you have read the first tutorial and already know how to add a reference to NAudio and that you remember you need to setup a using statement:

using NAudio.Wave;

We will define the key elements of the API we will be using for this project, within the class. IWavePlayer which you may recall from last time is our waveOutDevice = Sound Card and an instance of WaveMixerStream32, which will handle all of the mixing of the actual wave streams. Like last time we will be using an ASIO device for our waveOutDevice - so make sure you have the ASIO4ALL drivers installed if you don't have a sound card that already supports ASIO (Like me):

namespace NAudioMixTest

{

public partial class frmMixTest : Form

{

//Declarations required for mixing

private IWavePlayer waveOutDevice;

private WaveMixerStream32 mixer;


To reduce explanation latter, I've defined a simple array which will store the file names of the samples we are loading. This will also be used when checking if we have already loaded a sample for that position, by virtue of the absence of a filename.


// File names for the loaded samples

private string[] sampleLoaded = new string[4];


These following settings are the ones which have been included in the abstraction layer I mentioned earlier but for the purpose of demonstration have been striped out for individual specification here. The WaveFileReader is used to load the wave file, resulting in us having access to a stream of data for the wave file.


// Reader instance for the wave file

WaveFileReader[] reader = new WaveFileReader[4];


We don't directly make any modifications to the WaveOffsetStream in this example but it is required to be passed to the WaveChannel32 instance when being setup. The WaveChannel32 instance is then used to control the properties of the stream we are interested in - it's position.


// Other properties of the stream

WaveOffsetStream[] offsetStream = new WaveOffsetStream[4];
WaveChannel32[] channelSteam = new WaveChannel32[4];


The Code - Setup:


Now that we have finished with the formality of the definitions we need, we can start initializing the components. First is to define the mixer and set that it will not AutoStop playback - this allows us to assume that the status of the Mixer is always "Playing" - otherwise it would automatically stop when all inputs have been read, which would be an issue if we want some silence between beats - think boom . tis . boom . tis etc.


The waveOutDevice is then declared on the Form load and set to the status of Play, which means we can start sending audio to our sound card. Again ASIO defaults are being used here, so if it errors for you on this step - get ASIO4ALL.


public frmMixTest()

{

InitializeComponent();


//Setup the Mixer

mixer = new WaveMixerStream32();

mixer.AutoStop = false;

}


private void Form1_Load(object sender, EventArgs e)
{

for (int i = 0; i < 4; i++)

{
sampleLoaded[i] = "";

}

if (waveOutDevice == null)

{

waveOutDevice = new AsioOut();

waveOutDevice.Init(mixer);

waveOutDevice.Play();

}

}


The Code - Load a Sample:


We are almost there, believe it or not. I'll cover the loading of a sample now and then show how it all wraps together, skipping over the boring file loader dialog bit. Skip..


private void loadSample(int position)
{

// prompt for file load

OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "WAV Files (*.wav)|*.wav";

if (openFileDialog.ShowDialog() == DialogResult.OK)

{


Now we pass the FileName from the openFileDialog to the WaveFileReader instance. Which as far as we are concerned has given us a stream for the audio file to be read in to.

reader[position] = new WaveFileReader(openFileDialog.FileName);



Humble reader, I'll be honest with you here, I don't know this inside and out so there is an option to use an WaveOffsetStream but I have also found that it doesn't do anything we need for this actual demo. So if you want to use it you set it up like so and then pass it to WaveChannel32:

offsetStream[position] = new WaveOffsetStream(reader[position]);
channelSteam[position] = new WaveChannel32(offsetStream[position]);

Or to be really simple replace those two lines with this one, which just passes the WaveFileReader stream directly to the WaveChannel32 instance, which lets us control the stream as required for this example.


channelSteam[position] = new WaveChannel32(reader[position]);

After the stream has been defined we need to let the mixer know it exists by using AddInputStream and passing in the WaveChannel32 instance. This only needs to be done once and coincidently because we have set all the statuses to play, this audio file will automatically play when it is added. Then we clean up and store the FileName in the array so we know we have loaded a sample at this position.


// You only need to do this once per stream

mixer.AddInputStream(channelSteam[position]);

sampleLoaded[position] = openFileDialog.FileName;

}

}



The Code - Tying it all together:


You would have noticed that the code in the loadSample method is referring to "position" in the array, that is because we call this from each button click once we know we need to load a sample. I.e.


private void cmbSound1_Click(object sender, EventArgs e)

{

if (sampleLoaded[0] == "")

{

loadSample(0);

}

Else, we have loaded the sample and want to re-trigger the playback, so set the position of the sample to 0 - which is the beginning.


else

{

channelSteam[0].Position = 0;

}

}


& Then effectively the same code for the other 3 buttons, just with the positions set to the incremental position in the array.


Conclusion:


This 2nd Tutorial for NAudio has covered loading and mixing samples in real time in a relatively straight forward solution. This functionality almost represents the feature set of audio API functionality used by OpenSebJ currently. Loading Sample, Positioning Samples and Mixing Samples in real time. For next time we will look at covering the additional set of functionality currently supported by other Audio API's - such as setting the volume, pan and looping of the samples being mixed.



Until next time.