Tuesday 8 July 2014

Karplus Strong Fine Tuning in Silverlight

Following on from my previous post which had a basic implementation of KS in Silverlight I wanted to take this a bit further and address some of the problems in the simple implementation. Firstly as is well referenced, the problem with the basic KS implementation using an integer length delay line is that as the frequencies get higher there is an increasing problem with the pitch of the notes being out of tune.

There are two typical ways of addressing the integer delay line problem:
  • Linear Interpolation
  • All-pass filter
Linear Interpolation
A linear interpolation delay filter can be made by simply taking a portion equivalent to the fractional delay between the two last samples. Having already taken the lead from the STK implementation, we're going to continue to implement this using a similar class called DelayL (header, source).

As referenced above a Linearly Interpolated delay line (1st order FIR) has the following block diagram:






With a transfer function and difference equation for the fractional delay part of:






Where the delay fractional delay (n) above we will reference as alpha in the code implementation.

For more information on Interpolated Delay lines take a look here.

Firstly a few tidying up operations need to be made to the Delay class:

protected const int MAXLENGTH = 1000;

protected int length = 0;

public virtual double pop()

In addition, the increment operation has also been moved into the push operation:


        // increment now on pushing into delay
        public void push(double value)
        {
            set(0, value);
            increment();
        }


DelayL.cs
The new DelayL class inherits from Delay and modifies the pop operation to implement the Linear Interpolation algorithm and the Length setter now handles setting a non-integer value.

using System;
using System.Net;


namespace AgSynth
{
    public class DelayL : Delay
    {

        private double alpha = 0;

        public DelayL() : base()
        {
        }

        double xdel = 0;

        public override double pop()
        {
            double x = get(length);

            double y = ((1-alpha) * x) + (alpha*xdel);
            xdel = x;

            return y;
        }


        public double Length
        {
            set
            {
                base.Length = Helpers.Truncate(value);
                alpha = value - length;
            }
        }

    }
}

This all works very nicely, and sounds pretty good, but the unfortunate effect of Linear Interpolation is that it adjusts the pitch and can sharpen and flatten some of the notes.

Allpass Filter
Again taking the lead from STK to implement a First-Order All-pass Interpolation we can follow the implementation in the class DelayA (header, source).

As referenced above a First-Order All-pass Interpolation has the following block diagram:





With a transfer function and difference equation of the fractional part of:






In this case to calculate the delay, the parameters need to be calculated as follows:





Where n should be between 0.5 - 1.5 for the best characteristic. Refer here again for more treatment of the theory behind this.


DelayA.cs
The new DelayA class inherits from Delay and modifies the pop operation to implement the all-pass algorithm and the Length setter now handles setting a non-integer value.


using System;
using System.Net;


namespace AgSynth
{
    public class DelayA : Delay
    {
        private double alpha = 0;
        private double coeff = 0;

        public DelayA() : base()
        {
        }

 
        double ydel = 0;
        double xdel = 0;

        public override double pop()
        {
            double x = get(length);

            double y = (-coeff * ydel) + xdel + (coeff * x);

            xdel = x;
            ydel = y;

            return y;
        }

        public double Length
        {
            set
            {
                int l = Helpers.Truncate(value);
                alpha = 1.0 + value - l;

                // optimal allpass range is between  0.5 - 1.5 to achieve flattest phase delay response
                if (alpha < 0.5)
                {
                    l++;
                    alpha += 1.0;
                }

                // coefficient for filter
                coeff = (1.0 - alpha) / (1.0 + alpha);

                base.Length = l;
            }
        }

    }
}

Again, this works very well and overcomes the previous problems. To show this off working to it's best some additions and modifications need to be made to the other code that I will cover in a follow-on post.

No comments:

Post a Comment