Wednesday, 9 July 2014

Bringing the Generator classes closer to STK

After the work just done on the Delay classes the effort can now really build some benefit by harmonising the Generator classes more closely with STK and the tick() method. We'll do this progressively and then look at making some further changes in a follow-on post.

First of all, until now the tick method has been returning a sample value to the PcmStreamSource as a short value. We're going to change it to return a double and then put the scaling into PcmStreamSource. This keeps all the generator code in the floating point domain.

    public interface ISampleSource
    {
        double tick();
    }

and then in GetSampleAsync (not all the function is shown for brevity:

        protected override void GetSampleAsync(MediaStreamType mediaStreamType)
        {
            for (int i = 0; i < numSamples; i++)
            {
                short sample = 0;
                if (this.Input != null)
                {
                    sample = (short)(this.Input.tick() * short.MaxValue);
                }

                for (int c = 0; c < ChannelCount; c++)
                {
                    //channel
                    memoryStream.WriteByte((byte)(sample & 0xFF));
                    memoryStream.WriteByte((byte)(sample >> 8));
                }
            }

The corresponding changes then need to be made in the Generator interface and in the Karp implementation:


    public class Generator : ISampleSource
    {
        public virtual double tick()
        {
            return 0.0;
        }
    }

The changes to the Karp class are quite obvious to change this to a double so I will not explicitly describe them as we've got some more fundamental changes on the way. What we're going to do is unpack the various different filtering components now from the Karp class and put them into separate filters and generators so we have more room to create a wider range of generators in the future like the STK and develop a wider range of filters.

Noise.cs
First of all, let's look at making a new Noise generator class and take the functionality outside of the Karp class:

using System;
using System.Net;


namespace AgSynth
{
    public class Noise : Generator
    {
        private int noise = 0; // gate duration for generating noise

        Random r = new Random();


        // duration to generate noise in samples
        public int Duration
        {
            set
            {
                noise = value;
            }

            get
            {
                return noise;
            }
        }

        public override double tick()
        {
            double y = 0;

            // push noise into circuit if still generating noise
            if (noise > 0)
            {
                y = (r.NextDouble() * 2.0) - 1.0; // generate noise sample
                noise--;
            }

            return y;
        }
    }
}

This is pretty simple, but slightly different from the STK. In the STK Plucked implementation, which is closest to our basic Karp class noise is added into the delay when plucked is called. We might come back and take a look at the pros and cons of this, for now we'll stick with our current implemtation which effectively generates a duration of noise which is provided every tick.

Averager.cs
One of the reasons for unpacking the functions to match more closely to STK is so that we can have a look at a wider range of filters to build up some more interesting instruments. For now, rather than complicate things let's make a new filter just for the two-point moving averager which is used for the LPF. This is subclass of Filter. For simplicity as well we're setting the phaseDelay explicitly to 0.5 rather than using the calculation. I'll try to cover that in a separate post later on. It's more important that we take a look at how the Karp class is going to change. For now the Averager looks like this:

using System;
using System.Net;


namespace AgSynth
{
    public class Averager : Filter
    {
        public override double phaseDelay(double frequency)
        {
            return 0.5;
        }

        private double ydel = 0.0;

        public override double tick(double input)
        {
            double y = (input + ydel) * 0.5 * gain;
            ydel = y;

            return y;
        }
    }
}

I'm trying to keep the notation in the tick functions close to using the x and y values from the difference equations rather than other variables so that the theory is easier to follow.

Now the changes to Karp, which becomes much leaner. A lot of the variables are teased out and the two new classes Noise and Averager are added as attributes. The gain of the Averager (lpf) is set in the constructor to the 0.9999 value used previously. Pluck is now used to kick-off the call to set the noise generation duration using the delay.Delay length as a variable. The hard-coded value for the lpf phase delay in the calculation of the length of the delay line has now using the Filter phaseDelay function which will help us in the future. The biggest changes are to the tick operation which now unbundles the algorithm into the various generators and filters. I'm going to show the whole class so all the changes can be seen and then we'll look at a final little change.

using System;
using System.Net;

namespace AgSynth
{
    public class Karp: Generator
    {
        double Fo = 0; // fundamental frequency (pitch requested)

        DelayA delay = new DelayA();
        Noise noise = new Noise();
        Averager lpf = new Averager();

        public Karp()
        {
            lpf.Gain = 0.9999;
        }

        public void pluck()
        {
            noise.Duration += Helpers.Truncate(delay.Delay * 0.95); ;
        }


        public void pitch(double p)
        {
            if (p == 0)
            {
                delay.Delay = 0;
            }
            else
            {
                Fo = p; // frequency
                double Fs = PcmStreamSource.SampleRate;

                double No = Fs / Fo;

                delay.Delay = No - lpf.phaseDelay(p);
            }
        }



        public override double tick()
        {
            double sample = 0;

            if (delay.Delay > 0)
            {
                double amp = 0.99; // amplification

                // get input which is noise added with feedback loop
                double x = delay.pop() + noise.tick();  

                // filter the output with simple LPF
                double y = lpf.tick(x);

                // push the output to the start of the delay-line
                delay.tick(y);

                // amplify
                sample = (y * amp);
            }

            return sample;
        }

    }
}

In the STL the tick operation is a little more bundled. I've kept with the x and y notation as I think this is a little easier to read and understand.

The final little bit is that we can move a Gain value into the Generatorclass which can be used in Noise and Karp.


    public class Generator : ISampleSource
    {
        protected double gain = 1.0;

        public double Gain
        {
            set
            {
                gain = value;
            }
            get
            {
                return gain;
            }
        }

        public virtual double tick()
        {
            return 0.0;
        }
    }

The tick operation in Noise can now be changed to add in gain:


        public override double tick()
        {
            double y = 0;

            // push noise into circuit if still generating noise
            if (noise > 0)
            {
                y = (r.NextDouble() * 2.0 * gain) - 1.0; // generate noise sample
                noise--;
            }

            return y;
        }

This allows the pluck operation in Karp to be given an amplitude value (how hard the string is plucked) by adjusting the gain of the noise generator. We can see how this can also be used for some different effects later.


        public void pluck(double a)
        {
            noise.Gain = a;
            noise.Duration += Helpers.Truncate(delay.Delay * 0.95); ;
        }

And likewise the gain value can be used for the Karp tick operation itself:


        public override double tick()
        {
            double sample = 0;

            if (delay.Delay > 0)
            {
                // get input which is noise added with feedback loop
                double x = delay.pop() + noise.tick();  

                // filter the output with simple LPF
                double y = lpf.tick(x);

                // push the output to the start of the delay-line
                delay.tick(y);

                // amplify
                sample = y * gain;
            }

            return sample;
        }


So, this refactoring gives no real difference in what is being generated (so no new app to show), but it builds a stronger framework that we can now explore and reference the STK a bit more closely. More in due course....




No comments:

Post a Comment