Sunday, April 16, 2006

lab 62 - software synth csp style


lab 62 - software synth CSP style


This is the next iteration of my software synth code for Inferno. Of particular note is the embrace of CSP style as implementation technique. This was true of the code in lab 60. But this time much more of it actually works and the basic framework, the interfaces, are in place for me to extend it further. I think it makes a nice show case of CSP style programming. I thought my lexis database did too (lab 34) but this code is probably easier and more fun to play with.

I want to post this now before I move onto the next phase, which may add a lot more complexity but won't illustrate any better the CSP style of programming in this application. You are encouraged to edit this code to create your own synthesizer, and use that as a way into studying CSP style.

This synth comes with a basic GUI. Here is a screen shot.


The GUI is bare bones, designed just so that it very easy to add new knobs for the control of filters. (One of the things that worries me is that I'll get bogged down designing a pretty GUI.)

I'll now try and describe in more detail the code.

The interface for the processes has changed a little since lab 60. It is more general in that all the processes see the same interface and can a be plugged together in many different ways.

The interface is as follows,

 Inst: adt {
  c: Sample;
  ctl: Control;

  mk: fn(insts: Source, f: Instrument): ref Inst;

 Source: type array of ref Inst;
 Sample: type chan of (array of real, chan of array of real);
 Control: type chan of (int, real);
 Instrument: type ref fn(s: Source, c: Sample, ctl: Control);

The main type here is the Instrument function. Each instrument is spawned and interacts with other processes, usually other instruments, through the source, sample, and control channels. An instrument will typically loop forever receiving on the sample or control channel. The message on the sample channel will be an array of real that the instrument should fill with samples or filter the incoming samples, or whatever it likes. It sends the array back down a receive channel that came with the request. Control requests are things such as change frequency, or gain, for example. Finally, an instrument may optionaly take as argument and array of Source instruments it can use when answering requests for an array of samples. A good example is the mixer instrument which takes an array of any other instruments and mixes their samples down to a single stream of samples.

Here's an example of a one-pole filter

# y(n) = b₀x(n) - a₁y(n-1)
onepole(nil: Source, c: Sample, ctl: Control)
 lastout := array[channels] of {* => 0.0};
 b := array[1] of {0.4};  #gain
 a := array[2] of {1.0, -0.9};

 for(;;) alt {
 (x, rc) := <-c =>
  for(j := 0; j < channels; j++){
   x[j] = b[0] * x[j] - a[1] * lastout[j];
   for(i := channels+j; i < len x; i += channels)
    x[i] = b[0] * x[i] - a[1] * x[i-channels];
  lastout[0:] = x[len x-channels:];
  rc <-= x;
 (m, n) := <-ctl =>
  case m {
  CPOLE =>
   a[1] = -n;
   if(n > 0.0)
    b[0] = 1.0 - n;
    b[0] = 1.0 + n;

Here is some example code to use this filter, and add a knob to control the filter parameter then add the filter to the main loop. See also gui.b:209

 filter :=, onepole);
 spawn knob(filter.ctl, "onepole", 0.0, 1.0, 0.01);

  filter.c <-= (<-wrc, wrc);
  rc <-= <-wrc;

I'll show another example that illustrates plugging the instruments together.

mypoly(nil: Source, c: Sample, ctl: Control)
  voice :=[2] of 
     {* =>[2] of 
        {, waveloop),, adsr)}, 
     }, poly);

  for(;;) alt {
    (a, rc) := <-c =>
        poly.c <-= (a, rc);
    (m, n) := <-ctl =>
        poly.ctl <-= (m, n);

I'll pull that first statement apart to make it clearer what's going on., waveloop) creates the sample and control channels and spawns the waveloop process, passing it the channels and returning an object that stores references to those channels. The same is done for the adsr process, which is an envelope filter. These two sources are passed to the instrument process, which is expecting, say, a generator and filter which it combines to form the monophonic voice that responds to keyon and keyoff control events. Finally, two instances of this instrument voice are passed to the poly process which knows how to mix the input sources into a single polyphonic sound.

Is this an example of higher order function in CSP style?

As before, you need the jit turned on to run this program for it to sound acceptable.


No comments: