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.




Friday, February 27, 2009

Hip Hop for the Intelectual's

Some call it Nerd Core as if it was some alternate form of hip hop but I still don't know how I feel about being called a Nerd, so let me just introduce it as intellectual Hip Hop - not raping about guns, bling and bitches - makes it a worth while audible experience.

Don't know what I'm talking about still? Check out Optimus Rhyme's new EP - Full for Free Download it's a gloriours selection of beats and rhyme.

Find out more about them here, @ home.

Sunday, February 15, 2009

Introduction to Using NAudio

About NAudio
NAudio, is an Open Source* audio mixing libary written in C#, for the windows platform. It supports PInvoke methods for WaveOut, ASIO, DirectX and some other functions only available on Vista (WASAPI - Windows Vista Core Audio). It doesn't currently include abstraction support for PortAudio, OpenAL or GStreamer but here's hoping for some cross platform compatibility in the future.

The SubVersion repository for NAudio is very clean and easy to distinguish what you may need. My development environment is Windows XP x64 so there was an additional step required for the 64 bit environment, which was to set the platform to build as x86 by default.

You can do this by:

Clicking Build > Configuration Manager and then change all of the projects platform to x86

With the platform set, everything was ready to go. I recompiled and had a working x86 version, that runs on the 64 bit OS.

Introduction: Playing a Sample
This post is going to focus on loading an audio sample, playing the file and being able to reset the position of playback for the sample.

Start a new project, copy the NAudio.dll file over and add it in as a reference. Then set-up the using statements:

using NAudio.Wave;
using NAudio.CoreAudioApi;

In the class, we need to define the items which are going to be visible to all of our methods. We have two declarations we are concerned with in this introduction:
public partial class NAudioTest : Form
{
IWavePlayer waveOutDevice;
WaveStream mainOutputStream;
string fileName = null;

waveOutDevice which is an instance of IWavePlayer is going to be used to define the interface to a device that we will be using to play our audio.

mainOutputStream is used to store the audio sample we load and provides a level of abstraction from the Stream so that we don't need to manually move the stream data around when we want to adjust properties such as position - which allows us to easily seek positions within the sample.

fileName will just store the file name of the wave.

With these class declarations in place, the next logical step for us is to setup the device which is going to be used to output the Audio. At an appropriate location (under a button click event etc.) we can now initialise the instance. In this tutorial we are going to use the default available ASIO interface. This may not be suitable for all sound cards, it doesn't work on mine by default - but thanks to ASIO4ALL it does. Grab a copy of it, it's free and works wonders providing a lowlatency interface for mixing audio on windows.

try
{
waveOutDevice = new AsioOut();
}
catch (Exception driverCreateException)
{
MessageBox.Show(String.Format("{0}", driverCreateException.Message));
return;
}
The default ASIO device has hopefully been created (if it didn't work for you have you downloaded ASIO4ALL yet?). Now we have an audio device declared but nothing too exciting to your sense have started to occur - for that we need to load an audio file and we need to find one first. For the purpose of this tutorial you can either assume a fixed location and assign it to the fileName variable or add something similar to the OpenFileDialog snipet below:
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "Wave Files (*.wav)|*.wav|All Files (*.*)|*.*";
openFileDialog.FilterIndex = 1;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
fileName = openFileDialog.FileName;
}
I'll assume that needs no explanation of its own. This sample now needs to be loaded and stored in to a wave stream, which we have called mainOutputStream. We will use the CreateInputStream method from the NAudio Demo application to complete this task.
mainOutputStream = CreateInputStream(fileName);
Passing in the name of the file and having the input stream retuned. This doesn't require a lot of understanding if your just looking for the wave file to be loaded and the stream retuned, lets assume that's all you care about from this tutorial.
private WaveStream CreateInputStream(string fileName)
{
WaveChannel32 inputStream;
if (fileName.EndsWith(".wav"))
{
WaveStream readerStream = new WaveFileReader(fileName);
if (readerStream.WaveFormat.Encoding != WaveFormatEncoding.Pcm)
{
readerStream = WaveFormatConversionStream.CreatePcmStream(readerStream);
readerStream = new BlockAlignReductionStream(readerStream);
}
if (readerStream.WaveFormat.BitsPerSample != 16)
{
var format = new WaveFormat(readerStream.WaveFormat.SampleRate,
16, readerStream.WaveFormat.Channels);
readerStream = new WaveFormatConversionStream(format, readerStream);
}
inputStream = new WaveChannel32(readerStream);
}
else
{
throw new InvalidOperationException("Unsupported extension");
}
return inputStream;
}
Now for assembly of all of the pieces. We have a waveOutDevice defined where we will be sending audio to and a wave file has been loaded in to our mainOutputStream. All that's left is to connect the two and hit play:

try
{
waveOutDevice.Init(mainOutputStream);
}
catch (Exception initException)
{
MessageBox.Show(String.Format("{0}", initException.Message), "Error Initializing Output");
return;
}
waveOutDevice.Play();
Assuming everything was setup right, you should now hear a wave file being played. If you have difficulties check ASIO4ALL is installed. And the finishing touch is to reset the wave file to play back from the beginning. Find a friendly on-click button event and add the following:

mainOutputStream.CurrentTime = TimeSpan.FromSeconds(0);

Ahem. Done.

Thanks to Mark Heath for this great library - check out Mark's Blog Here. All of this code was ripped from the NAudio Demo Application and is available within the source code package available on codeplex.

Next Time

I'll be looking at loading multiple wave files simultaneously and how to use the included Mixing functions. Stay tuned.

* It is licensed under the Microsoft Public License (Ms-PL), which I personally am not to farmiular with. However according to the FAQ there doesn't seem to be any issues distributing it along with another Open Source application. As I use the GPL I need to look a little further in to the licensing compatability but based on what I have read, I am hopeful for the moment that it is fine.