Variable length buffer for live looping


#1

Hi, new-ish to SC and struggling to implement a basic variable length sampler/looper. I can allocate a buffer with some maximum length, then record into it with RecordBuf ugen. But when my recorded loop is shorter than the max buffer length, what is the best strategy to constrain the buffer to just the recorded material and ignore/trim the empty space that follows? Can I return the index of the last frame recorded into the buffer, or keep track of clock time at start and end of record (then convert to num of frames)?

Basic example:
I allocate a 30 sec. buffer. User hits REC and records 8 seconds of audio, then hits STOP/LOOP/whatever. How to accurately know the endpoint of recorded audio so I can loop just the 8 seconds, not the whole buffer?

Many thanks in advance,
Johnny


#2

I’m don’t know how to best solve your problem as I have very little insight in how you approached (design-wise) the whole project, but I can point out that other people have tried to do what you are trying to do and they somehow solved the problems they encountered. Here’s something that may be interesting to watch:


#3

Your description is correct. You can use Main.elapsedTime to get the time when you start recording, then again when you stop. Subtract those numbers and now you have the duration of your buffer. If your buffer is N seconds long, create a routine that plays a new Synth, which will last N seconds every N seconds.

Sam


#4

Thanks for the replies on this. After asking the question, I had done what @Sam_Pluta suggested with Main.elapsedTime, though a) it feels clunky to manage precise timing on the language side and b) I would assume that it will always be a little bit off due to latency from sending and receiving messages between client and server. Following Eli’s tutorial I was able to implement the same functionality using the Timer UGen which sends the recorded loop length to my playback buffer over a control bus. Not sure if this really makes a huge difference overall but feels more sound.


#5

Also, a comment/question about the Synchronous Looping tutorial code from above: in the SynthDef args he hard codes the bus indexes and then creates his synth using the predefined values (no need to provide arguments in the Synth creation):

//input synth
SynthDef.new(\input, {
	arg		amp=1, recBus=50, dirBus=60;
	var		in, recSig, dirSig;
        // etc...

//create synth
~in = Synth.new(\input, [], ~inGroup); // works without providing arguments

In my version, I used allocated bus indexes using Bus object, and put them into their own IdentityDictionary to keep them organized. But when I create a Synth without additional arguments (as above), it doesn’t work and buses are not mapped correctly. If I add the arguments in the Synth creation it works:

~abus = ();
~abus.rec = Bus.audio(s, 2);
~abus.dir //......etc.

SynthDef.new(\input, { //input synth
	arg		amp=1, recBus = ~abus.rec, //......etc.

//create synth
~in = Synth.new(\input, [], ~inGroup); // doesn't work..
~in = Synth.new(\input, [\recBus, ~abus.rec], ~inGroup); // this works! 

Is there some logical reason for this? Environment variables can’t be provided for arg values in SynthDefs, or storing the Bus objects in the dictionary is causing a problem?


#6

It doesn’t have to do with storing bus objects in a dictionary. It has to do with the boundary between the server and the language.

A SynthDef exists on the server whereas a ~ variable exists in the language. Basically there are two scopes existing in parallel but with a boundary between. Mixing them by referencing a tilde variable in a SynthDef won’t work the way you might expect when just considering the source code but not the execution context.

~in = Synth.new(\input, [\recBus, ~abus.rec], ~inGroup); 

would be the correct and preferred way to proceed. Here you’re passing a value from the language to the server which makes sense.

With

~in = Synth.new(\input, [], ~inGroup);

you’re expecting a variable existing the language context to be referenced by a synth existing in the server context which doesn’t work.

So, additionally I would change

recBus = ~abus.rec

to

recBus = 0

That is the more typical way of specifying a default value for a synthdef argument.


#7

@johnnyvenom as far as I can tell, environment variables cannot be passed as arguments into a SynthDef function, although they can be passed as variables:

(
~a = 5;
SynthDef(\dummy, { arg argument = ~a;
  var variable = ~a;
  argument.poll(label: "argument");
  variable.poll(label: "variable");
}).add;
)

Synth(\dummy)

posts:

argument: 0
variable: 5

This seems to be the result of how the SynthDef class converts function arguments into Synth Controls, getting values from the prototypeFrame method of a FunctionDef. This from the implementation of SynthDef:addControlsFromArgsOfFunc :

...
values = def.prototypeFrame[skipArgs..].extend( names.size );
...

And prototypeFrame doesn’t give you the values of any environment variables you use in arguments:

~a = 5;
~f = { arg environmentVar = ~a, literalArg = 9; [environmentVar, literalArg].postln };
~f.value; // posts [ 5, 9 ]
~f.def.prototypeFrame; // posts [ nil, 9 ]

If you really want to use the value from an environment variable as the default for a SynthDef control you can use a NamedControl like so:

(
~a = 5;
SynthDef(\dummy2, {
  var control = \control.kr(~a);
  control.poll(label: "NamedControl");
}).add;
)

~s = Synth(\dummy2);

// then you can change the value like so:
~s.set(\control, 10);

But it might be better to do what droptableuser suggests because the default value in the SynthDef will not update if you change the value of the environment variable later.


#8

Great - that makes sense of the results I was getting. The buses were sending straight to audio out (bus 0).

Thanks!