c0dehunter
c0dehunter

Reputation: 6150

NAudio display both channels in WaveViewer

I am using NAudio's WaveViewer UI control where I display the waveform using

this.customWaveViewer1.WaveStream = new NAudio.Wave.WaveFileReader(filename);

I am wondering how can I display each channel in it's own WaveViewer?

Upvotes: 1

Views: 4211

Answers (3)

Craig D
Craig D

Reputation: 507

Though this is an old question, I too have had some challenges with the WaveViewer showing what I was expecting, and especially since other third party controls were showing nice funky graphics, yet the WaveViewer (using an AudioFileReader) looked like blobs.

This lead me to the following understanding (which I should have known, but it only became clear after a lot of hair pulling). AudioFileReader returns 32-bit (4 byte) signed floating point samples, in sequence, starting at the first sample channel 1, cycling through each channel and then on to the next sample.

My source was stereo (2 channel) and so the first 4 bytes contain left channel data and the next 4 bytes are the right channel.

So I worked out that I wanted to see a 1 pixel width line extending up and down from the middle of the control showing average and peak values in different colours. Once you know what the data is, you can decide however you want to show the peaks and troughs.

How it works is to iterate over the stream and take 1 pixel's worth of data to work out how to display it. WaveViewer has a setting which identifies how many samples should be displayed per pixel (ie granularity), and then you just need an algorithm to work out how you want to display it.

First, create a buffer array of floating point numbers that can hold enough samples for 1 pixel that needs to be displayed. This is done with:

float[] buffer = new float[samplesPerPixel * waveStream.WaveFormat.Channels]; 

Then each time you want to get a set of samples that constitute 1 pixel, you can just read it from the stream into the buffer.

samplesRead = ((ISampleProvider)waveStream).Read(buffer, 0, buffer.Length);

For a two channel wave format, you can calculate the left channel data peak and average value for positive and negative using the following loop:

for(int n=0; n<buffer.Length; n += waveStream.WaveFormat.Channels)
{
    var sample = buffer[n];

    if (sample > 0)
    {
        numPositive++; sumPositive += sample;
        if (sample > peakPositive) peakPositive = sample; 
    }
                                
    if (sample < 0)
    {
        numNegative++; sumNegative += sample;
        if (sample < peakNegative) peakNegative = sample;
    }
}

..and for the right channel, you start at array element 1

for (int n = 1; n < buffer.Length; n += waveStream.WaveFormat.Channels)...

You then just draw sets of lines for each channel, peak/average, positive/negative combination. My understanding of the floating point sample is that a value of +/- 1 is maximum, so all I did was use that as a percentage of how high or low in the client the line needed to be drawn. If you want both channels in the same control, you would start the left channel line from this.ClientHeight/4 as origin, and the right channel would be 3 * this.ClientHeight/ 4 (being the 1/4 line and the 3/4 line, splitting each half of the control into 2).

The height up/down from the origin is the value (being a percentage) of the ClientHeight/4. So a set of samples that have a peakPositive of 0.7 on the left channel, you draw a line extending from the origin up 0.7 * ClientHeight/4 (remembering that in cordinate space on a control, the y-axis starts at the top at 0, and extends down).

For my example, I decided to have separate controls for the left and right channel, so I was using ClientHeight/2 as the origin, and only using samples from the left or right channel based on a setting in the control.

float averagePositive = sumPositive / numPositive;

var start = this.ClientSize.Height / 2;   // start from the middle of the control
var finish = this.ClientSize.Height / 2;

finish = this.ClientSize.Height / 2 - (int)(this.ClientSize.Height/2 * peakPositive);
if (finish < start)
    e.Graphics.DrawLine(peakLinePen, x, start, x, finish);

finish = this.ClientSize.Height / 2 - (int)(this.ClientSize.Height/ 2 * averagePositive);
if (finish < start)
    e.Graphics.DrawLine(averageLinePen, x, start, x, finish);

float averageNegative = sumNegative / numNegative;

finish = this.ClientSize.Height / 2 - (int)(this.ClientSize.Height/ 2 * peakNegative);   //note the peak negative < 0 and X axis is down the screen!
if (finish > start)
    e.Graphics.DrawLine(peakLinePen, x, start, x, finish);

finish = this.ClientSize.Height / 2 - (int)(this.ClientSize.Height/ 2 * averageNegative);
if (finish > start)
    e.Graphics.DrawLine(averageLinePen, x, start, x, finish);

I hope that helps some people as a starting point for their own rendering. I am very sure there are better ways but this suited me to being with. I have attached the output from my algorithm based on a famous song which has had the vocals split from the backing guitar. No prizes (but extreme awe) if you can work out the song!

enter image description here

Upvotes: 0

Guest8876
Guest8876

Reputation: 1

You Can Use This In The WaveViewer.cs ( Inside OnPaint )

for(int i = 0; i < bytesRead.Length; i += 4){ // 0 + 4 = Data Of Left Channel
    short sample = BitConverter.ToInt16(bytes, i)

    if(sample < low) low = sample;
}

for(int i = 2; i < bytesRead.Length; i += 4){ // 2 + 4 = Data Of Right Channel
    short sample = BitConverter.ToInt16(bytes, i)

    if(sample > high) high = sample;
}

It Worked For Me. To Make Seperate Waveforms, You Will Require 2 Waveforms And Follow Steps:-

First, Add This Global Property:-

public int ShowChannelWaveform {get; set;}

Then, In OnPaint, Replace The For Loop Which Is Inside The First For Loop With This Code:-

if(ShowChannelWaveform = 1){

for(int i = 0; i < bytesRead.Length; i += 4){ // 0 + 4 = Data Of Left Channel
    short sample = BitConverter.ToInt16(bytes, i)

    if(sample < low) low = sample;
    if(sample > high) high = sample;
}

} else if(ShowChannelWaveform = 2){

for(int i = 2; i < bytesRead.Length; i += 4){ // 2 + 4 = Data Of Right Channel
    short sample = BitConverter.ToInt16(bytes, i)

    if(sample < low) low = sample;
    if(sample > high) high = sample;
}

}

And Set "ShowChannelWaveform" Of First WaveViewer To 1 And That Of Second WaveViewer To 2

Upvotes: 0

Mark Heath
Mark Heath

Reputation: 49482

WaveViewer is a very simple example of how to show waveforms. If you want stereo, I'd recommend copying the source code for it, and modifying the OnPaint method to draw two lines, one for the left channel and one for the right.

Upvotes: 2

Related Questions