Julian Rubisch

September 30, 2021

Supercollider Object Oriented Patterns, Part 3 - Subclassing Buffer

Now I'm in a position to create views for my buffer collection on the fly - which is great, but I still don't have the ability to record into them, and store the respective end frame to allow looping. Doing so isn't actually that hard:

SynthDef(\rec, {
    |in=0, bufnum=0, rec=1|
    var sig = SoundIn.ar(in),
    stopTrig = (rec <= 0),
    phase = Phasor.ar(0, 1, 0, BufFrames.kr(bufnum));

    BufWr.ar(sig, bufnum, phase);
    SendReply.ar(K2A.ar(stopTrig), '/recEnded', [phase.poll, bufnum]);
    FreeSelf.kr(stopTrig);
}).add;

// see https://scsynth.org/t/looper-with-a-variable-length/818/6
OSCdef(\ended, { |msg|
    var bufnum = msg[4].asInteger;
    ~rec_end[bufnum] = msg[3];  // save ending frame index
}, '/recEnded', s.addr);

These 2 defs basically care for the recording into the buffer and storing the end frame into a global ~rec_end array. What annoys me about this solution is that it splits the knowledge about the state of each recorded buffer into the buffer itself (which holds that data) and said global variable.

Having learned my lessons of similar problems in Ruby, the solution would be to have the Buffer itself being responsible for both recording, holding the actual samples, and metadata.


RecEnhancedBuffer

My take on this looks as follows:

RecEnhancedBuffer : Buffer {
    var recSynth, <>recEnd;

    *registerDefs { |server|
        SynthDef(\bufferRec, {
	    |in=0, bufnum=0, rec=1|
	    var sig = SoundIn.ar(in),
	    stopTrig = (rec <= 0),
	    phase = Phasor.ar(0, 1, 0, BufFrames.kr(bufnum));

	    BufWr.ar(sig, bufnum, phase);
	    SendReply.ar(K2A.ar(stopTrig), '/bufferRecEnded', [phase.poll, bufnum]);
	    FreeSelf.kr(stopTrig);
	}).add;

	// see https://scsynth.org/t/looper-with-a-variable-length/818/6
	OSCdef(\bufferRecEnded, { |msg|
	    var bufnum = msg[4].asInteger;
	    server.cachedBufferAt(bufnum).recEnd = msg[3];
	}, '/bufferRecEnded', server.addr);
    }

    startRec {
        recSynth = Synth(\bufferRec, [\bufnum, this.bufnum]);
    }

    stopRec {
        recSynth.set(\rec, 0);
    }
}

The first thing of note is that our enhanced Buffer class has two new instance variables: recSynth and recEnd.

The class method *registerDefs takes care of, well, registering the recording Synthdef and the OSC listener. I don't know if this is a best practice, but it seemed feasible to me. The major caveat is probably that we're using magic strings for the definition symbols \bufferRec, \bufferRecEnded, but they could be passed in as a parameter to the class method, too.  

Observe that the OSCdef reaches for the buffer via server.cachedBufferAt(bufnum) to set the recEnd on the respective buffer. This probably has side effects, but worked okay in my preliminary tests.

The instance methods startRec and stopRec perform the actual instantiation and stopping of the recSynth.

So we can now do this:

~test_buffer = RecEnhancedBuffer.alloc(s, s.sampleRate * 10.0, 1);

~test_buffer.startRec;
~test_buffer.stopRec;

~test_buffer.play;
~test_buffer.recEnd;

Applying it on the VBufferCollection

Even more than this, if we swap the regular Buffers for RecEnhancedBuffers in our collection class, we get the same functionality for each of the buffers.

Here's a short video showing that off:

https://cln.sh/Y3LjaO

And here's the gist containing all of the source code:

https://gist.github.com/julianrubisch/3ff82eec0f015de104426f569e621f7d