Saturday, 4 July 2009

Audio WaveForm Drawing Using WPF

A while ago I blogged about how to display audio waveforms in WinForms. Porting this code to WPF is not as simple as it may first appear. The rendering models are quite different. In Windows Forms, you can draw points or lines using GDI functions. You are responsible for drawing them all every time the window is invalidated and its Paint method is called. However, in WPF, you create objects to put on a Canvas, and WPF manages the rendering and invalidation.

My first attempt was to stay as close to the WinForms model as I could. I have a sample aggregator that looks for the highest sample value over a short period of time. It then uses that to calculate the height of the line it should draw. Every time we calculate a new one, we add a line to our Canvas at the next X position, wrapping round if necessary, and deleting any old lines.

wpf-waveform-1

As can be seen, this gives a reasonable waveform display. I made use of a LinearGradientBrush to try to improve the visual effect a little (although this requires we cheat and keep the waveform symmetrical). There is a big performance problem however - it is very inefficient to keep throwing new lines onto a Canvas and removing old ones. The solution was to start re-using lines once we had wrapped past the right-hand edge of the display.

private Line CreateLine(float value)
{
    Line line;
    if (renderPosition >= lines.Count)
    {
        line = new Line();
        lines.Add(line);
        mainCanvas.Children.Add(line);
    }
    else
    {
        line = lines[renderPosition];
    }
    line.Stroke = this.Foreground;
    line.X1 = renderPosition;
    line.X2 = renderPosition;
    line.Y1 = yTranslate + -value * yScale;
    line.Y2 = yTranslate + value * yScale;
    renderPosition++;
    line.Visibility = Visibility.Visible;
    return line;
}

This solves our performance issue, but I still wasn’t too happy with the visual effect – it is too obviously composed of vertical lines. I tried a second approach. This added two instances of PolyLine to the canvas. Now, we would add a point to each line when a new maximum sample was created. Again the same trick of re-using points when we had scrolled off the right-hand edge was used for performance reasons.

wpf-waveform-2

As nice as this approach is, there is an obvious problem that we are not able to render the bit in between the top and bottom lines. This requires a Polygon. However, we can’t just add new points to the end of the Polygon’s Points collection. We need all of the top line points first, followed by all of the bottom line points in reverse order if we are to create a shape.

The trick is that when we get a new sample maximum and minimum in, we have to insert those values into the middle of the existing Points collection, or calculate the position in the points array. Notice that I create a new Point object every time to make sure that the Polygon is invalidated correctly.

private int Points
{
    get { return waveForm.Points.Count / 2; }
}

public void AddValue(float maxValue, float minValue)
{
    int visiblePixels = (int)(ActualWidth / xScale);
    if (visiblePixels > 0)
    {
        CreatePoint(maxValue, minValue);

        if (renderPosition > visiblePixels)
        {
            renderPosition = 0;
        }
        int erasePosition = (renderPosition + blankZone) % visiblePixels;
        if (erasePosition < Points)
        {
            double yPos = SampleToYPosition(0);
            waveForm.Points[erasePosition] = new Point(erasePosition * xScale, yPos);
            waveForm.Points[BottomPointIndex(erasePosition)] = new Point(erasePosition * xScale, yPos);
        }
    }
}

private int BottomPointIndex(int position)
{
    return waveForm.Points.Count - position - 1;
}

private double SampleToYPosition(float value)
{
    return yTranslate + value * yScale;
}

private void CreatePoint(float topValue, float bottomValue)
{
    double topYPos = SampleToYPosition(topValue);
    double bottomYPos = SampleToYPosition(bottomValue);
    double xPos = renderPosition * xScale;
    if (renderPosition >= Points)
    {
        int insertPos = Points;
        waveForm.Points.Insert(insertPos, new Point(xPos, topYPos));
        waveForm.Points.Insert(insertPos + 1, new Point(xPos, bottomYPos));
    }
    else
    {
        waveForm.Points[renderPosition] = new Point(xPos, topYPos);
        waveForm.Points[BottomPointIndex(renderPosition)] = new Point(xPos, bottomYPos);
    }
    renderPosition++;
}

This means that our minimum and maximum lines join together to create a shape, and we can fill in the middle bit.

wpf-waveform-3

Now we are a lot closer to the visual effect I am looking for, but it is still looking a bit spiky. To smooth the edges, I decided to only add one point every two pixels instead of every one:

wpf-waveform-4

This tidies up the edges considerably. You can take this a step further and have a point every third pixel, but this highlights another problem – that our polygons have sharp corners as they draw straight lines between each point. The next step would be to try out using Bezier curves, although I am not sure what the performance implications of that would be. Maybe that is a subject for a future post.

The code for these waveforms will be made available in NAudio in the near future.

Thursday, 2 July 2009

Where are you going to put that code?

Often we want to modify existing software by inserting an additional step. Before we do operation X, we want to do operation Y. Consider a simple example of a LabelPrinter class, with a Print method. Suppose a new requirement has come in that before it prints a label for a customer in Sweden, it needs to do some kind of postal code transformation.

Approach 1 – Last Possible Moment

Most developers would immediately gravitate towards going to the Print method, and putting their new code in there. This seems sensible – we run the new code at the last possible moment before performing the original task.

public void Print(LabelDetails labelDetails)
{
    if (labelDetails.Country == "Sweden")
    {
        FixPostalCodeForSweden(labelDetails);
    }
    // do the actual printing....
}

Despite the fact that it works, this approach has several problems. We have broken the Single Responsibility Principle. Our LabelPrinter class now has an extra responsibility. If we allow ourselves to keep coding this way, before long, the Print method will become a magnet for special case features:

public void Print(LabelDetails labelDetails)
{
    if (labelDetails.Country == "Sweden")
    {
        FixPostalCodeForSweden(labelDetails);
    }
    if (printerType == "Serial Port Printer")
    {
        if (!CanConnectToSerialPrinter())
        {
            MessageBox.Show("Please attach the printer to COM1");
            return;
        }
    }
    // do the actual printing....
}

And before we know it, we have a class where the original functionality is swamped with miscellaneous concerns. Worse still, it tends to become untestable, as bits of GUI code or hard dependencies on the file system etc creep in.

Approach 2 – Remember to Call This First

Having seen that the LabelPrinter class was not really the best place for our new code to be added, the fallback approach is typically to put the new code in the calling class before it calls into the original method:

private void DoPrint()
{
    LabelDetails details = GetLabelDetails();
    // remember to call this first before calling Print
    DoImportantStuffBeforePrinting(details);
    // now we are ready to print
    labelPrinter.Print(details);
}

This approach keeps our LabelPrinter class free from picking up any extra responsibilities, but it comes at a cost. Now we have to remember to always call our DoImportantStuffBeforePrinting method before anyone calls the LabelPrinter.Print method. We have lost the guarantee we had with approach 1 that no one call call Print without the pre-requisite tasks getting called.

Approach 3 – Open – Closed Principle

So where should our new code go? The answer is found in what is known as the “Open Closed Principle”, which states that classes should be closed for modification, but open for extension. In other words, we want to make LabelPrinter extensible, but not for it to change every time we come up with some new task that needs to be done before printing.

There are several ways this can be done including inheritance or the use of the facade pattern. I will just describe one of the simplest – using an event. In the LabelPrinter class, we create a new BeforePrint event. This will fire as soon as the Print function is called. As part of its event arguments, it will have a CancelPrint boolean flag to allow event handlers to request that the print is cancelled:

public void Print(LabelDetails labelDetails)
{
    if (BeforePrint != null)
    {
        var args = new BeforePrintEventArgs();
        BeforePrint(this, args);
        if (args.CancelPrint)
        {
            return;
        }
    }
    // do the actual printing....
}

This approach means that our LabelPrinter class keeps to its single responsibility of printing labels (and thus remains maintainable and testable). It is now open for any future enhancements that require an action to take place before printing.

There are a couple of things to watch out for with this approach. First, you would want to make sure that whenever a LabelPrinter is created, all the appropriate events were hooked up (otherwise you run into the same problems as with approach 2). One solution would be to put a LabelPrinter into your IoC container ready set up.

Another potential problem is the ordering of the event handlers. For example, checking if you have permission to print would make sense as the first operation. The simplest approach is to add the handlers in the right order.

Conclusion

Always think about where you are putting the new code you write. Does it really belong there? Can you make a small modification to the class you want to change so that it is extensible, and then implement your new feature as an extension to that class? If you do, you will not only keep the original code maintainable, but your new code is protected from accidental breakage, as it is isolated off in a class of its own.