Product SiteDocumentation Site

11.4.8. Making a Useful Section out of the Second Part

This section describes the reasons for the differences between the second part's Function that was just created, and the Function that appears in "FSC_method_1-short.sc". It all comes down to this: the current solution is tailor-made for this particular program, and would require significant adaptation to be used anywhere else; I want to re-design the Function so that it can be used anywhere to begin with, while still defaulting to the behaviour desired for this program.
You can skip this section, and return later. The actions for the rest of the tutorial remain unchanged whether you do or do not make the modifications in this section.
Here's what I have from the previous step:
var t_c = TempoClock.default;

var secondPart =
{
   var so = Array.new( 10 );
   
   var func = 
   {
      var frequency = 200 + 600.rand;
      [ SinOsc.ar( freq:frequency, mul:0.01 ), SinOsc.ar( freq:frequency, mul:0.01 ) ];
   };
   
   10.do( { arg index; t_c.sched( ((5*index)+1), { so = so.add( {func.value;}.play; ); } ); } );
   10.do( { arg index; t_c.sched( 51, { so[index].free; } ); } );
   
};
This Function is the perfect solution if you want ten pseudo-random pitches between 200 Hz and 800 Hz, and a five-second pause between each one. If you want nine or eleven pitches, if you want them to eb between 60 Hz and 80Hz, if you want a six-second pause between each - you would have to modify the Function. If you don't remember how it works, or if you give it to a friend, you're going to have to figure out how it works before you modify it. This is not an ideal solution.
Let's solve these problems one at a time, starting with allowing a different number of SinOsc synths to be created. We know that we'll have to create an argument, and that it will have to be used wherever we need the number of SinOsc's. Also, to preserve functionality, we'll make a default assignment of 10. Try to accomplish this yourself, making sure to test your Function so that you know it works. Here's what I did:
var t_c = TempoClock.default;

var secondPart =
{
   arg number_of_SinOscs = 10;
   
   var so = Array.new( number_of_SinOscs );
   
   var func = 
   {
      var frequency = 200 + 600.rand;
      [ SinOsc.ar( freq:frequency, mul:0.01 ), SinOsc.ar( freq:frequency, mul:0.01 ) ];
   };
   
   number_of_SinOscs.do( { arg index; t_c.sched( ((5*index)+1), { so = so.add( {func.value;}.play; ); } ); } );
   number_of_SinOscs.do( { arg index; t_c.sched( 51, { so[index].free; } ); } );
   
};
The "do" loop doesn't need a constant number; it's fine with a variable. What happens when you pass a bad argument, like a string? This would be an easy way to sabotage your program, and in almost any other programming context it would concern us, but this is just audio programming. If somebody is going to try to create "cheese" SinOsc's, it's their own fault for mis-using the Function.
Now let's modify the Function so that we can adjust the range of frequencies that the Function will generate. We know that we'll need two more arguments, and that they'll have to be used in the equation to calculate the frequency. But we'll also need to do a bit of arithmetic, because of the way the "rand" Function works (actually we don't - see the "rand" Function's help file). Also, to preserve functionality, we'll make default assignments of 200 and 800. Try to accomplish this yourself, making sure that you test the Function so you know it works. Here's what I did:
var t_c = TempoClock.default;

var secondPart =
{
   arg number_of_SinOscs = 10,
       pitch_low = 200,
       pitch_high = 800;
   
   var so = Array.new( number_of_SinOscs );
   
   var func =
   {
      var freq = pitch_low + (pitch_high - pitch_low).rand;
      [ SinOsc.ar( freq:freq, mul:0.01),
        SinOsc.ar( freq:freq, mul:0.01) ];
   };
   
   number_of_SinOscs.do( { arg index; t_c.sched( ((5*index)+1), { so = so.add( {func.value;}.play; ); } ); } );
   number_of_SinOscs.do( { arg index; t_c.sched( 51, { so[index].free; } ); } );
   
};
Notice that I changed the name of the variables, and the indentation in the "func" sub-Function, to make it easier to read. This isn't a particularly difficult change.
Now let's allow the user to set the length of time between each SinOsc appears. We will need one more argument, used in the scheduling command. Try to accomplish this yourself, and if you run into difficulty, the next paragraph contains some tips.
The change to the "do" loop which schedules the SinOsc's to play is almost trivial. My new argument is called "pause_length", (meaning "the length of the pause, in seconds, between adding each SinOsc"), so I get this modification: number_of_SinOscs.do(
{
   arg time;
   secondPart_clock.sched( (1+(time*5)), { sounds = sounds.add( func.play ); } );
});
Again, I changed the indentation, and the names of the variables in this sub-Function. Recall that the "1+" portion is designed to add a one-second pause to the start of the Function's execution. The problem comes in the next "do" loop, where we have to know how the number of beats from now will be five seconds after the last SinOsc is added. We'll have to calculate it, so I added a variable to store the value after it's calculated. This also allows us to return it, as a convenience to the Function that called this one, so that it knows how long until this Function is finished. Try adding this yourself, then testing the Function to ensure that it works. I got this:
var t_c = TempoClock.default;

var secondPart =
{
   arg number_of_SinOscs = 10,
       pitch_low = 200,
       pitch_high = 800,
       pause_length = 5;
   
   var so = Array.new( number_of_SinOscs );
   
   var when_to_stop = ( 1 + ( pause_length * number_of_SinOscs ) );
   
   var func =
   {
      var freq = pitch_low + (pitch_high - pitch_low).rand;
      [ SinOsc.ar( freq:freq, mul:0.01),
        SinOsc.ar( freq:freq, mul:0.01) ];
   };
   
   number_of_SinOscs.do(
   {
      arg time;
      t_c.sched( (1+(time*5)), { so = so.add( func.play ); } );
   });

   t_c.sched( when_to_stop,
              {
                 number_of_SinOscs.do( { arg index; so[index].free; } );
                 nil;
              });
   
   when_to_stop;
};
I decided to "invert" the "free-ing" of the SinOsc's. Rather than scheduling number_of_SinOscs Function-calls at some point in the future, I decided to schedule one thing: a "do" loop that does the work. The indentation looks strange, but sometimes there's not much you can do about that. The "when_to_stop" variable must be the last thing in the Function, so that the interpreter returns it to the Function's caller.
In order to retain the "bare minimum" robustness to be used elsewhere, we can't rely on the "TempoClock.default" clock having the tempo we expect, and we certainly can't rely on it being declared as "t_c". The solution is quite easy: create a new TempoClock within the Function.
var t_c = TempoClock.new; // default tempo is one beat per second
We could hypothetically use the "SystemClock", since we're measuring time strictly in seconds. But, using a TempoClock is preferred for two reasons:
  1. It has the word "tempo" in its name, and it's designed for scheduling musical events; the "SystemClock" is for system events.
  2. We can easily extend this in the future to use a "TempoClock" set to a different tempo.
There are some further ways to improve this Function, making it more robust (meaning that it will work consistently in a greater range of circumstances). Here are things that could be done to improve the Function, with an explanation of why it would make the Function more robust:
  • Made the clock an argument, allowing this Function to schedule events on a clock belonging to some other Function. Since all clocks respond to the "sched" message, we could even accept the "SystemClock" or "AppClock". The default value would still be TempoClock.new
  • Use absolute scheduling rather than relative scheduling. Depending on how long the server and interpreter take to process the commands, it could lead to significant delays if the Function is asked to create a lot of SinOsc's.
  • Create one SynthDef (with an argument) for all of the synths. Especially when asked to create a large numbe of SinOsc's, this will lead to faster processing and lower memory consumption. On the other hand, it increases the complexity of the code a little bit, requiring more testing.
  • Each SinOsc is currently created with the same "mul" argument, regardless of how many SinOsc's are created. Set as it is, when asked to create 51 SinOsc's, the signal would become distorted. If you're puzzled about why 51, remember that for each SinOsc the Function is asked to create, it currently creates two: one for the left and one for the right audio channel.
  • Allow the SynthDef to be passed in as an argument, with the requirement that such a SynthDef would need to accept the "freq" and "mul" arguments. This is going out on a limb a bit, and requires careful explanation in comments to ensure that the Function is used correctly. You will also need to test what happens if the Function is used incorrectly. Crashing the server application is a bad thing to do, especially without a warning.
  • Use a Bus to cut the number of synths in half, so that one synth will be sent both to the left and right channels. Alternatively, you could add special stereo effects.
As you can see, there are a lot of ways to improve this Function even further; there are almost certainly more ways than listed here. Before you distribute your Function, you would want to be sure to test it thoroughly, and add helpful comments so that the Function's users know how to make the Function do what they want. These are both large topics in themselves, so I won't give them any more attention here.