One of the easiest algorithms to get an interesting sound with very little coding is the Karplus-Strong delay-line which makes a very basic simulation of a plucked string through a simplification of the more generalised concept of digital waveguide synthesis.
Basically a signal, usually white noise, is used to simulate the string pluck and then the signal is delayed and put into a low-pass filter to simulate the decay of the string oscillations.
The basic filter circuit is shown below:
There are already some good examples of this online. Probably the most accessible and closest to what we're doing is by Andre-Michelle who has posted his AS3 implementation. He also has taken this a bit further to simulate all six strings along with resonance, which we'll get onto in a future post hopefully.
The other notable, accessible way to see this algorithm and more is the well-know Synthesis Toolkit in C++ (STK). The source is described and there is a lot of reference material explaining this and the other concepts that are built on it. Andy Li has also done an excellent job of porting much of this over into AS3 (stk-in-as3).
And back to some code here... we're going to build on the previous posting by making a new Generator class called Karp:
using System; using System.Net; namespace AgSynth { public class Karp: Generator { int excite = 70; // excitation duration in samples int noise = 0; // gate if noise is being generated or not int length = 0; // length of the delay line Random r = new Random(); double Fo = 0; // fundamental frequency (pitch requested) double vlpfdelay = 0; // low pass filter internal delay value Delay delay = new Delay(); public Karp() { } public void pluck() { noise += excite; } // helper function static int Truncate(double d) { return (int)(d > 0 ? Math.Floor(d) : -Math.Floor(-d)); } public void pitch(double p) { if (p == 0) { delay.Length = 0; } else { Fo = p; // frequency double Fs = PcmStreamSource.SampleRate; double No = Fs / Fo; length = Truncate(No - 0.5); excite = Truncate(length * 0.95); double fraction = No - 0.5 - length; delay.Length = length; } } public override short tick() { short sample = 0; if (length > 0) { double amp = 0.99; // amplification double vin = 0; // push noise into circuit if still generating noise if (noise > 0) { vin = (r.NextDouble() * 2.0) - 1.0; // generate noise sample noise--; } double decay = 0.999; double vinlpf = delay.pop(); // get the delayed value // filter the output with simple LPF and decay double voutlpf = ((vlpfdelay) + (vinlpf)) * 0.5 * decay; vlpfdelay = voutlpf; // output = mix input with filter double vout = voutlpf + vin; // push the output to the start of the delay-line delay.push(vout); delay.increment(); // amplify sample = (short)(vout * short.MaxValue * amp); } return sample; } } }
There are three main methods here:
- pitch - which sets the pitch (or frequency of the note)
- pluck - which sets the counter for the gate for noise to be pumped into the algorithm
- tick - which is our main algorithm
In this implementation push puts the sample on the end of the delay-line, pop takes the last sample off the delay line and increment cycles the delay-line by a sample.
using System; using System.Net; namespace AgSynth { public class Delay { private const int MAXLENGTH = 1000; private double[] buffer = new double[MAXLENGTH + 1]; private int idx = 0; private int length = 0; public Delay() { for (int i = 0; i < MAXLENGTH + 1; i++) { buffer[i] = 0; } } public void increment() { lock (buffer) { idx = (idx + 1 > length) ? 0 : idx + 1; } } public double pop() { double value = get(length); //increment(); return value; } public void push(double value) { set(0, value); } public double get(int index) { double result = 0.0; if (buffer != null) { lock (buffer) { int pos = (idx + index >= length) ? idx + index - length : index + idx; result = buffer[pos]; } } return result; } public void set(int index, double value) { if (buffer != null) { lock (buffer) { int pos = (idx + index >= length) ? idx + index - length : index + idx; buffer[pos] = value; } } } public int Length { set { length = value; if (length < MAXLENGTH) { lock (buffer) { double[] newbuffer = new double[MAXLENGTH]; for (int i = 0; i < length; i++) { newbuffer[i] = get(i); } buffer = newbuffer; idx = 0; } } } } } }
The tick() function then implements the algorithm. Firstly noise is generated for a certain number of samples (which is scaled by the frequency). A basic two-point moving averager low-pass filter is then implemented to progressively smooth out the high-frequencies and decay the signal.
The MainPage algorithm to make this do something interesting then just needs a few tweaks to get it to play some notes:
Karp karp = new Karp(); private void PlayTune() { double bpm = 120; double barduration = 60 * 1000 * 4 / bpm; // in ms - assuming 4/4 beat = barduration / 4; List<double> tune = new List<double>() { Theory.C4, Theory.D4, Theory.E4, Theory.F4, Theory.E4, Theory.D4 }; while (true) { foreach (double p in tune) { karp.pitch(p); karp.pluck(); Thread.Sleep((int)(beat)); } } } private void ButtonPlay_Click(object sender, RoutedEventArgs e) { source.Input = karp; media.Play(); if (player == null) { player = new Thread(new ThreadStart(PlayTune)); player.Start(); } }
The Theory class puts together the note pitches:
public class Theory { public const double C4 = 261.63; public const double Cs4 = 277.2; public const double D4 = 293.67; public const double Ds4 = 311.1; public const double E4 = 329.63; public const double F4 = 349.23; public const double Fs4 = 370.0; public const double G4 = 392.00; public const double Gs4 = 415.3; public const double A4 = 440.00; public const double As4 = 466.2; public const double B4 = 493.88; }
And here is is
No comments:
Post a Comment