Thursday 8 October 2009

Playback of Sine Wave in NAudio

In this post I will demonstrate how to create and play a sine wave using NAudio. To do this, we need to create a derived WaveStream, or more simply, a class that implements IWaveProvider.
One awkwardness of implementing IWaveProvider or WaveStream, is the need to provide the data into a byte array, when it would be much easier to write to an array of floats for 32 bit audio (or shorts for 16 bit). To help with this, I have created the WaveProvider32 class (likely to be committed into the source for NAudio 1.3), which uses the magic of the WaveBuffer class, to allow us to cast the target byte array into an array of floats.
public abstract class WaveProvider32 : IWaveProvider
{
    private WaveFormat waveFormat;
    
    public WaveProvider32()
        : this(44100, 1)
    {
    }

    public WaveProvider32(int sampleRate, int channels)
    {
        SetWaveFormat(sampleRate, channels);
    }

    public void SetWaveFormat(int sampleRate, int channels)
    {
        this.waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
    }

    public int Read(byte[] buffer, int offset, int count)
    {
        WaveBuffer waveBuffer = new WaveBuffer(buffer);
        int samplesRequired = count / 4;
        int samplesRead = Read(waveBuffer.FloatBuffer, offset / 4, samplesRequired);
        return samplesRead * 4;
    }

    public abstract int Read(float[] buffer, int offset, int sampleCount);

    public WaveFormat WaveFormat
    {
        get { return waveFormat; }
    }
}

Now we can derive from WaveProvider32 to supply our actual sine wave data:
public class SineWaveProvider32 : WaveProvider32
{
    int sample;

    public SineWaveProvider32()
    {
        Frequency = 1000;
        Amplitude = 0.25f; // let's not hurt our ears            
    }

    public float Frequency { get; set; }
    public float Amplitude { get; set; }

    public override int Read(float[] buffer, int offset, int sampleCount)
    {
        int sampleRate = WaveFormat.SampleRate;
        for (int n = 0; n < sampleCount; n++)
        {
            buffer[n+offset] = (float)(Amplitude * Math.Sin((2 * Math.PI * sample * Frequency) / sampleRate));
            sample++;
            if (sample >= sampleRate) sample = 0;
        }
        return sampleCount;
    }
}

Using it is straightforward. We choose our output sample rate, the frequency of the sine wave and amplitude. You can even adjust them in real-time.
private WaveOut waveOut;

private void button1_Click(object sender, EventArgs e)
{
    StartStopSineWave();
}

private void StartStopSineWave()
{
    if (waveOut == null)
    {
        var sineWaveProvider = new SineWaveProvider32();
        sineWaveProvider.SetWaveFormat(16000, 1); // 16kHz mono
        sineWaveProvider.Frequency = 1000;
        sineWaveProvider.Amplitude = 0.25f;
        waveOut = new WaveOut();
        waveOut.Init(sineWaveProvider);
        waveOut.Play();
    }
    else
    {
        waveOut.Stop();
        waveOut.Dispose();
        waveOut = null;
    }
}

Please note, that you will need to be using the latest NAudio code from source control to use this (until 1.3 is released).

59 comments:

Unknown said...

Great post Mark.

do you happen to know if this would be possible for mpeg2 audio?

Unknown said...

hi Andri, I'm not sure what you mean. Do you want to convert the sine wave to mpeg2?

Unknown said...

no to display a cine wave of mpeg2 audio file (actually a mplayer audiodump of a mpeg2-ts file)

Unknown said...

this code doesn't display anything. It creates an audio sine wave.

Unknown said...

Hi Mark,

Thanks for the run down, it's really helpful.

My problem is that I need to be able to create complex noises comprised of many waves.

Any idea how I could achieve this?

Thanks,

Tom

Unknown said...

Hi Tom, you would make one WaveProvider for each simple waveform, and then another WaveProvider that read out of each one and summed them together.

Have a look at the WaveMixerStream in NAudio for an example

Laserbeak43 said...

Hello Mark,
I'm really excited about this source, but I haven't used a third party library in a very long time and am not sure how to set your library up in Visual Stuido, without just altering the provided solution. Do you think you could show me how to do this?

Thanks
Malik

Unknown said...

Hi Malik,
Simply do an add reference to your project in Visual Studio and browse to the NAudio dll (downloaded from Codeplex).

Laserbeak43 said...
This comment has been removed by the author.
Laserbeak43 said...

Hello Mark,
That was great, thanks! I've added a slider to the form in an attempt to update the frequency in realtime, realizing that the sine wave's settings are set when the startStop button is toggled on.
Is there a way to update things in realtime?
------------added-------------
private int Frequency;
private void trackBar1_Scroll(object sender, EventArgs e)
{
Frequency = trackBar1.Value;
}
------------------------------

Unknown said...

you just need to keep a reference to the sine wave provider and set the Frequency property on that when your slider moves

Laserbeak43 said...

VERY COOL! i just made the SineWaveProvider object viewable to whole class and updated it in the slider's scroll method. It works great, but is there a way to update it faster? seems to change values kind of choppy? would that be the fact that i'm no using Soundcard settings made for low latency response?

Thanks!!
Malik

Unknown said...

the chopiness is most likely because you are not transitioning smoothly between frequencies, as well as due to latency settings (the Read method will run quickly and fill an entire buffer of say 100ms), so your changing Frequency will not kick in until the next buffer is processed.

Frank said...

I have the example working in a form. Clicking the button starts the tone, and clicking it again stops it. However, when I try to get it to run from Main() itself, the sound only lasts a fraction of a second, then stops. If I put a breakpoint in the Read() routine, I see it only gets called 3 times.

Here is my code in Main:

Worker worker = new Worker();
worker.StartStopSineWave();
System.Threading.Thread.Sleep(5000);
worker.StartStopSineWave();
worker = null;

And here is the class:

public class Worker
{
WaveOut waveOut;

public void StartStopSineWave()
{

if (waveOut == null)
{
SineWaveProvider32 sineWaveProvider = new SineWaveProvider32();
sineWaveProvider.SetWaveFormat(16000, 1); // 16kHz mono
sineWaveProvider.Frequency = 1000;
sineWaveProvider.Amplitude = 0.10f;
waveOut = new WaveOut();
waveOut.Init(sineWaveProvider);
waveOut.DesiredLatency = 1000;
waveOut.Play();
}
else
{
waveOut.Stop();
waveOut.Dispose();
waveOut = null;
}
}

}

I suspect this problem has something to do with threading. Any ideas on how to solve it?

Unknown said...

hi Frank,
you don't need to run WaveOut on a background thread. Try it on the GUI thread. Don't worry about blocking the GUI - things will still be responsive.

Frank said...

Thanks Mark for the reply.

Actually I need it in a batch/console process. The override I am doing (in place of the sine generator) will continually get input from an external device, process it, and pass the resulting audio to the sound card for the user to hear. I guess I could create a dummy form but would prefer this to work in the background instead.

Unknown said...

ok then, you need to create WaveOut with the Function callback (see the WaveOut constructor). Only be warned that certain laptop drivers seem to have problems with hanging in calls to WaveOutReset

Frankl said...

That works. Thanks!

Unknown said...

Hi Mark!
I have implemented the sine wave generator. I would like it to play for a certain time so I added a timer. During that time I would like all other activites to stop, how do I manage that?
What I am after is:
Call StartSine
"While playing do nothing"
Call next action

Frank said...

Coach, can't you just set some type of flag when the timer starts, clear it when the timer is done, and then check for the flag in the other activities and don't proceed (say sleep and then check again) as long as its set?

Frank

Scott said...

Hi Mark!

Fantastic API - I've been struggling with the old Managed DirectX and this is much better.

I created the SineWaveProvider example you have. I'm using Windows 7 (64-bit) and Visual Studio C# Express. When I try to run the app, I get a "BadImageFormatException" error trying to load NAudio. The exception message is:

Could not load file or assembly 'NAudio, Version=1.3.8.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. An attempt was made to load a program with an incorrect format.

Is this a Windows 7 thing? Or a 64-bit thing? Or something else? Just wondering - I'll give it a go on my XP machine and see how that works out...

-Scott

Unknown said...

Hi Scott,
NAudio is marked as x86 only due to the fact that the interop signatures have not been updated to work on x64 in all cases. When I eventually get myself an x64 system then I will be able to fix NAudio up. I suspect this is the problem you are running into

Richard Allen said...

I can't get this to work, i'm not familiar with C#. How do you structure this code, in separate classes? Is there an example I can look at?

Thanks

Richard

Scott said...

Interestingly enough, when I built the project in SharpDevelop 3.2, with the build options set to "Build for 32-bit Intel", it worked fine on my 64-bit system. Go figure. :)

sdecorme said...

Hi Mark
I try to use the naudio lib in c# with VS2005 .
Is there a way to select which channel (Left or right) with the Wavein Function.
I don't information about that.
Thanks

Unknown said...

@sdecorme you should record in stereo and then just throw away every other sample (usually it is left right left right)

d.j.z. said...

What would be the best way to do this in stereo? I'm currently using this for a soft-synth and have been struggling trying to get this to work with the mixer; i can't create a channel like this

WaveChannel32 = new WaveChannel32( new MyWaveProvider32) because MyWaveProvider32 cannot be cast to WaveStream and I can't figure out why this is the case...

Unknown said...

hi djz. NAudio doens't have a mixer that works with IWaveProvider yet. I hope to add one very soon

d.j.z. said...

Thanks for responding! I am so thankful for the time you've spent on this project. Kudos!!

Laserbeak43 said...

Hello Mark,
I was just wondering, How fast could i use this example if I separate the start and stop code and put them in KeyDown and KeyUp methods?
If I press the keys too fast, the debugger says that waveOut.Stop(); is referencing an object that doesn't exist anymore. "Object reference not set to an instance of an object."

My code is below, do you have any Ideas why this might happen?

Thanks for your library :)

private void Button_KeyDown(object sender, KeyEventArgs e)
{
...
startSine(1000, amp);
...
}

private void startSine(int freq, float amp)
{
if (waveOut == null)
{
sineWaveProvider = new SineWaveProvider32();
waveOut = new WaveOut();
sineWaveProvider.SetWaveFormat(16000, 1);
sineWaveProvider.Frequency = freq;
sineWaveProvider.Amplitude = amp;
waveOut.Init(sineWaveProvider);
waveOut.Play();
}

#endregion

#region Key Up Event Handler
private void Button_KeyUp(object sender, KeyEventArgs e)
{
...
stopSine();
...
}
private void stopSine()
{
waveOut.Stop();
waveOut.Dispose();
waveOut = null;
}
#endregion

Laserbeak43 said...

Oh, it seems that my problem is multiple key presses. I'll have to write code to fix this. Please delete my last post, if you see it fit to do so.
Thanks!!

Unknown said...

Thats really wonderful lib but I want to know is there any option available to know the frequency and amplitude of the laoded wave file ultimately to modify it???

Anonymous said...

Hi Guys,How i can read the stereo channel using NAudio(Left Speaker and Right Speaker) values of a video while it's playing.Please reply for this.I need to use this for a project.

Unknown said...

@Anonymous, I'm afraid NAudio can't access the audio channels of video files.

Matt said...

Hi Mark,

I was wondering, will the above code still work with the latest beta version of NAudio that is on codeplex, or have you included some of the functionality that you suggested you would in the update?

Unknown said...

@Matt, I can't see any reason why this wouldn't work with the latest NAudio code

Steve said...

Hey Mark-

I have been playing with the NAudio library for a couple days. I got the sine wave to play. Very cool. But now I want to play back my own arbitrary float data. What's the best way to stuff in float data to a WaveOut object? My guess is to use BufferedWaveProvider. I'm a little uncertain how to get that class to handle float data though.

I tried instantiating BufferedWaveProvider a couple of ways. My first shot was using WaveForm(fs,1). I also tried WaveFormat.CreateIeeeFloatWaveFormat(fs,1).

But then I was shaky on how to pack my float data into a byte array to be used by the AddSamples() method of BufferedWaveProvider.

What do you recommend?

Kyle said...

HI Mark

I was wondering maybe if there was a way to save the data directly to a .wav file ?

Unknown said...

@Kyle, you can use the WaveFileWriter class from NAudio to do this

Unknown said...

@Steve,
inherit from WaveProvider32 and provide your own float samples in the Read method

Murat said...

thanks a lot Mark
great work

John Dixon said...

Hi there Mark,

I've converted this into a derived WaveStream to allow mixing using WaveMixerStream.

I've added:

public override long Length
{
get { return long.MaxValue; }
}

However, I'm not sure how to get/set position.

Could you help me out?

Thanks.

Unknown said...

@John, For position, just return the number of bytes read. For a sine generator there is no point allowing position to be set, so either throw an exception or just ignore the position value.

Anonymous said...

Hello,

How could I use this to create other waveform types such as a triangle or sawtooth?

Also is there any way to avoid a click when changing frequencies during playback?

Unknown said...

creating other waveforms is easy, especially sawtooth/triangle/square, as they can be easily calcualted. You can also use a wavetable.

To eliminate clicks, either fade out and back in over a short period (a few ms), or implement portamento. I hope to blog about that at smoe point.

Unknown said...

If I want to Stop the Play automatically, after finishing the buffer, what can I do?

It seems that the read method will be executed by offset=0 each time!

Unknown said...

offset is just the offset into the buffer that you should write to so this will normally be zero. You maintain state outside the Read method to know where you were up to.

Anonymous said...

Hey Mark,

I'm a student and have the assignment to create a recording application which then records microphone input and has to determine the frequency and then display which note it is related to said frequency.

Will this article be the good way to follow in order to achieve this (retrieval of frequency)?

Thanks!

Unknown said...

hi Alain, this article is about how to generate sine waves, rather than how to detect frequency. To do that, I'd recommend researching FFT or autocorrelation algorithms.

Rudi said...

Thanks for the Post. I have noticed that it work wrong with Stereo....
To fix it i add a line in the SineWaveProviderClass to proof if byte is even Number.

for (int n = 0; n < sampleCount; n++)
{
buffer[n + offset] = (float)(Amplitude * Math.Sin((2 * Math.PI * sample * Frequency) / sampleRate));
if (this.WaveFormat.Channels == 1 || (n + offset) % 2 == 0)
sample++;
if (sample >= sampleRate) sample = 0;
}

I'm sorry for my bad english and hope it ist helpflull.

Rudi said...

Tahnk you for this great Post. I have noticed that it is working wrong in stereo-mode. To fix it i have added a line to proof if floatarray is even number:

for (int n = 0; n < sampleCount; n++)
{
buffer[n + offset] = (float)(Amplitude * Math.Sin((2 * Math.PI * sample * Frequency) / sampleRate));
if (this.WaveFormat.Channels == 1 || (n + offset) % 2 == 0)
sample++;
if (sample >= sampleRate) sample = 0;
}

I'm sorry for my bad English and hope my Coment ist helpfully.

Unknown said...

Hi!

Thanks a lot for your work on NAudio, it's much appreciated!

The last few days I have been working with additive synthesis based on this example. At first I used WaveOut (as in your code), but then I started moving my code to a Windows Store App. I know that NAudio isn't officially available for such apps, but I found some post stating how to use NuGet to get a pre-release. I changed WaveOut to WasapiOutRT like this:

_sampleMixer = new MixingSampleProvider(_voices[0].SampleProviders);
_sampleToWaveProvider = new SampleToWaveProvider(_sampleMixer);
_waveOut = new WasapiOutRT(AudioClientShareMode.Shared, 100);
await _waveOut.Init(_sampleToWaveProvider);
_waveOut.Play();

It works, but there's a horrible latency. I'm not sure, but I don't think it lagged as much when using WaveOut. I am a total beginner in programming audio like this, but when I stepped through the code I realized that the Read() method is called just once every second when using WasapiOutRT, with a buffer size of 44100, as opposed to every ~150ms or so on WaveOut (buffer size 6615), and to me that sounds like a source of latency. Or do I miss something?

Best regards,
HÃ¥kan Eriksson

Unknown said...

I think in shared mode, sometimes your requested latency can be ignored. If you can debug the NAudio code, look in the Init method of WasapiOutRT

Unknown said...

Hi!

Thanks for your swift reply!

I stepped through the NAudio code. The reported latency from the AudioClient is 11 ms.

However, when stepping through the code I noticed that in MediaFoundationTransform you explicitly read one second from the provider:

private IMFSample ReadFromSource()
{
// we always read a full second
int bytesRead = sourceProvider.Read(sourceBuffer, 0, sourceBuffer.Length);

Maybe there's something I don't understand well enough, but by reading once a second, doesn't that imply that changes I make in the audio generation can be delayed up to one second until they are heard?

Thanks a lot for your help, and a happy New Year as well! :-)

Unknown said...

Hi!

Thanks for your swift reply!

I stepped through the NAudio code. The reported latency from the AudioClient is 11 ms.

However, when stepping through the code I noticed that in MediaFoundationTransform you explicitly read one second from the provider:

private IMFSample ReadFromSource()
{
// we always read a full second
int bytesRead = sourceProvider.Read(sourceBuffer, 0, sourceBuffer.Length);

Maybe there's something I don't understand well enough, but by reading once a second, doesn't that imply that changes I make in the audio generation can be delayed up to one second until they are heard?

Thanks a lot for your help, and I wish you a happy New Year! :-)

Unknown said...

why is an MFT involved? Are you resampling? If possible work at the sample rate of the soundcard. But I probably should do something a bit cleverer with that MFT read size code.

Unknown said...

I haven't given much thought to where the sampling rate comes from, so I might be resampling without intending to. You've seen my initialization code (in my first post), and the code producing the sine wave is pretty much as your example. If I understand your code correctly, that would imply that the sampling frequency is set by the default constructor of WaveProvider32, i.e. 44100 Hz in mono?

And I see that it is possible to use other constructors to set other sample rates, but how would I know what sample rate the soundcard has?

Unknown said...

Actually, by stepping through the NAudio initialization, I learned that my sound card's sample rate indeed is different from the one I got from the default constructor. When setting the sample rate correctly there's no lag (an no glitches, which also occurred before).

Is there a way to get the sound card's sample rate from NAudio?

Unknown said...

With WasapiOut at least I think you can get it just by asking for the WaveFormat before you call Init. (It's been a while, but I think that's how it works from memory). By the way, best to ask NAudio questions over at the CodePlex discussion site if possible (naudio.codeplex.com)