Product SiteDocumentation Site

11.4.7. Optimizing the Code

Hopefully, while working through the previous sections, you got an idea of how tedious, boring, difficult-to-read, and error-prone this sort of copy-and-paste programming can be. It's ridiculous, and it's poor programming:
  • We're using a lot of variables and variable names. They're all just used once or twice, too.
  • When you copy-and-paste code, but change it a little, you might make a mistake in that little change.
  • When you copy-and-paste code, when you make a mistake, you have to copy-and-paste to fix it everywhere.
  • Repetition is the enemy of high-quality code. It is much better to write something once and re-use that same code.
Thankfully, SuperCollider provides three things that will greatly help to solve these problems - at least for our current situation:
  • Arrays can be used to hold multiple instances of the same thing, all referred to with essentially the same name. We're already doing something similar, (sinosc1, sinosc2, etc.) but arrays are more flexible.
  • Functions can be written once, and executed as many times as desired.
  • Loops also provide a means to write code once, and execute it many times. As you will see, they are useful in situations different from functions.
It should be noted that, while it is good practise to program like this, it is also optional. You will probably find, though, that writing your programs well in the first place ends up saving huge headaches in the future.
  1. The first thing we'll do is write a function to deal with generating the stereo arrays of SinOsc's.
  2. Take the code required to generate one stereo array of SinOsc's with a pseudo-random frequency. Put it in a function, and declare a variable for it (I used the name "func").
  3. Now remove the frequency1 (etc.) variables, and change the sinosc1 (etc.) variables to use the new function. Make sure that the code still works in the same way. It's much easier to troubleshoot problems when you make only one change at a time!
  4. At this point, we've eliminated ten lines of code, and made ten more lines easier to read by eliminating the subtle copy-and-paste changes. If you can't manage to work it out, refer to the FSC_method_1.sc file for tips.
  5. We can eliminate ten more lines of code by using a loop with an array. Let's change only one thing at a time, to make it easier to find a problem, if it should arise. Start by commenting out the lines which declare and initialize sinosc1, sinosc2, and so on.
  6. Then declare a ten-element array in the same place: var sinosc = Array.new( 10 );
  7. The next part is to write code to get ten func.value's into the array. To add something to an array in SuperCollider, we use the "add" method: sinosc.add( thing_to_add ); There is a small wrinkle to this, described in the SuperCollider documentation. It's not important to understand (for musical reasons, that is - it is explained on this help page), but when you add an element to an array, you should re-assign the array to the variable-name: sinosc = sinosc.add( thing_to_add ) Basically it works out like this: if you don't re-assign, then there is a chance that the array name only includes the elements that were in the array before the "add" command was run.
  8. With this, we are able to eliminate a further level of redundancy in the code. Ten exact copies of sinosc = sinosc.add( { func.value; } ); Now, ten lines that look almost identical actually are identical. Furthermore, we don't have to worry about assigning unique names, or even about index numbers, as in other programming languages. SuperCollider does this for us!
  9. This still won't work, because we need to adjust the rest of the function to work with this array. The scheduling commands be changed to look something like this: t_c.sched( 1, { so1 = sinosc[0].play; } ); Since arrays are indexed from 0 to 9, those are the index numbers of the first ten objects in the array.
  10. Remember that you need to put all of your variable declarations before anything else.
  11. It should still work. Let's use a loop to get rid of the ten identical lines.
  12. In SuperCollider, x.do( f ); will send the value message to the function f x times. So, to do this ten times, we should write 10.do( { sinosc = sinosc.add( { func.value; } ); } ); and get rid of the other ones. This is very powerful for simple things that must be done multiple times, because you are definitely not going to make a copy-and-paste error, because it's easy to see what is being executed, and because it's easy to see how many times it is being executed.
  13. Now let's reduce the repetitiveness of the scheduling. First, replace so1, so2, etc. with a ten-element array. Test it to ensure that the code still works.
  14. Getting the next two loops working is a little bit more complicated. We know how to run the exact same code in a loop, but we don't know how to change it subtly (by supplying different index numbers for the array, for example). Thankfully, SuperCollider provides a way to keep track of how many times the function in a loop has already been run. The first argument given to a function in a loop is the number of times that the function has already been executed. The first time it is run, the function receives a 0; if we're using a 10.do( something ); loop, then the last time the function is run, it receives a 9 because the function has already been executed 9 times. Since our ten-element array is indexed from 0 to 9, this works perfectly for us.
  15. The code to free is shorter: 10.do( { arg index; t_c.sched( 51, { so[index].free; } ); } ); This can look confusing, especially written in one line, like it is. If it helps, you might want to write it like this instead:
    10.do
    ({ arg index;
    	t_c.sched( 51, { so[index].free; } );
    });
    
    Now it looks more like a typical function.
  16. The next step is to simplify the original scheduling calls in a similar way, but it's slightly more complicated because we have to schedule a different number of measures for each call. With a little math, this is also not a problem - it's just a simple linear equation: number_of_measures = 5 * array_index + 1 Try to write this loop by yourself, before going to the next step.
  17. If you missed it, my solution is
    10.do( { arg index; t_c.sched( ((5*index)+1), { so = so.add( sinosc[index].play; ); } ); } );
    which includes some extra parentheses to ensure that the math is computed in the right order.
  18. The code is already much shorter, easier to understand, and easier to expand or change. There is one further optimzation that we can easily make: get rid of the sinosc array. This simply involves replacing sinosc[index] with what all of its elements are: { func.value; }
  19. The resulting program is a little different from what ended up in FSC_method_1.sc, but produces the same output. What I have is this:
    var t_c = TempoClock.default;
    
    {
       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; } ); } );
       
    }.value;
    
  20. Finally, assign this Function to a variable (called "secondPart", perhaps), and remove the "value" Function-call. If we leave that in, the Function will execute before the rest of the program begins!