.loadToFloatArray with functions and routines in loops to create wavetables in SuperCollider

There are some advantages to generating wavetables with code (as opposed to loading them from SoundFiles or other means). The execution of an application is not reliant on accompanying sources, and there are far fewer files and folders to mismanage. And more importantly, the wavetables are not “static” in the sense that they’ll always be slightly different every time they’re created. You’ll likely not to hear the exact same wavetable twice.

While trying to write a function to create wavetables from audio functions, I learned that SuperCollider sometimes needs more time to execute sever-side objects and functions (as opposed to the languange-side). Its caused me enough anguish that I thought it might be worth a blog post.

Server-language synchronization is not really an issue when lines of code are manually evaluated (one by one) in the IDE. This is not true though when evaluating multiple statements together in a block of code, or for language-controlled loops. A Routine or a function/Routine combination may be used to manage the execution and maintain synchronization. Using a Routine becomes highly relevant when a block or function contains code for both the server and language.

Defining a Routine inside of a function allows arguments to be passed into the function, and subsequently into the Routine. When iterating over a collection in a loop, I like to use one Routine to manage the iteration, and a different Routine to execute code on each item of that collection.

Here is a simple example of the basic strategy:
(
~fnRout01 = {
    arg arArgs;
    var fn01, rout02;

    // this function does stuff with each argument of ~fnRout01
    fn01 = {
        arg array;
        var rout01;
        rout01 = Routine({
            array.do({
                arg value;
                "array:".post; array.postln;
                "value:".post; value.postln;
                0.5.wait;
            }); // end .do

            rout02.play;

        }); // end rout01

        rout01.play;

    }; // end fn01

    // this routine manages the iteration of arguments for ~fnRout01
    rout02 = Routine({
        arArgs.do({
            arg item, i;
            item.postln; i.postln;
            fn01.value(item);

            nil.yield;
            "fished item:".post; i.postln;

        }); // end .do

    }); // end rout02

    rout02.play;

    // if the function returns an object, call it here as the last thing in the function

}; // end fnRout
)

~ar01 = Array.newFrom([[1, 2, 3], [4, 5, 6, 7, 8], [9, 10, 11]]);
~fnRout01.value(~ar01);

Routine rout02 iterates over the elements of an array (which are also arrays in this case). Calling nil.yield pauses rout02 while fn01 is playing rout01, which eventually resumes playing rout02 after it has finished executing most (all) of its code.

In the next, slightly more complex example, .loadToFloatArray and subsequent code generates an array of wavetables in buffers from an audio function and some other args. It uses the same strategy. A function/Routine combination creates a wavetable buffer and assigns it to a key in a dictionary. A different, more simple Routine iterates over a collection of arrays, with each array providing a unique set of args for the function. The .loadToFloatArray function requires a little more time to execute than is afforded by s.sync. In such cases, a sufficient (static) wait time can be established with .wait instead.

(
~fnFnToDictWT = {
    arg arArgs;
    var dictBuf, fnWT, routArgs;

    dictBuf = Dictionary.new(arArgs.size);

    fnWT = {
        arg arArg;
        var routWT, wtBuf, key, fn, num, freq, numFrames;

        routWT = Routine({
            key = arArg[0];
            fn = arArg[1];
            if(arArg[2] == nil, {num = 5}, {num = arArg[2]}); // end if
            if(arArg[3] == nil, {freq = 220}, {freq = arArg[3]}); // end if
            if(arArg[4] == nil, {numFrames = 512}, {numFrames = arArg[4]}); // end if
            wtBuf = Array.newClear(num);

            num.do({
                arg i;
                var ar;
                fn.loadToFloatArray(1/freq, s, action:{arg array; ar = array}); // end action
                0.2.wait; // do not use s.sync here
                ar = ar.resamp1(numFrames);
                ar = ar.as(Signal).normalize;
                ar = Buffer.sendCollection(s, ar.asWavetable);
                s.sync;
                ar.postln;
                wtBuf.put(i, ar);
            }); // end .do

            dictBuf.put(key, wtBuf);
            "dict.put (key, value)".postln;
            routArgs.play;
        }); // end routWT

        routWT.play

    }; // end fnWT

    routArgs = Routine({
        arArgs.do({
            arg item, i;
            fnWT.value(item);
            nil.yield;

            "routWT finished".postln;
            0.5.wait;

        }); // end .do

    });

    routArgs.play;

    dictBuf;
};
)

// some audio functions
(
~freq01 = 220;
~aFn01 = {SinOsc.ar(~freq01) * LFNoise1.ar(~freq01 * 2)};
~aFn02 = {SinOsc.ar(~freq01) * LFNoise1.ar(~freq01 * 8)};
~aFn03 = {SinOsc.ar(~freq01) * LFNoise1.ar(~freq01 * 32)};
)

// a double array of args
// args:  [key, fn, num, freq, numFrames]
(
~arArgs01 = [
    [\sinNoise01, ~aFn01, 3, ~freq01, 512],
    [\sinNoise02, ~aFn02, 7, ~freq01, 512],
    [\sinNoise03, ~aFn03, 5, ~freq01, 512],
];
)

// create a dictionary by calling the function with a double array of argument values
~dictWT01 = ~fnFnToDictWT.value(~arArgs01);

// use the dictionary
~key01 = ~dictWT01.keys.choose;
~value01 = ~dictWT01.at(~key01);
~value01.do({arg buffer; buffer.plot});

// don't forget to free the buffers
~dictWT01.values.do({arg arBuf; arBuf.do({arg buffer; buffer.free})}); // frees all buffers in the dictionary

// or
Buffer.freeAll; // frees all buffers on the server

 

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Leave a Reply

Your email address will not be published. Required fields are marked *