How can I start looping from a buffer with a message?

I’m having trouble searching the docs well enough to answer this, so perhaps someone can give me a hint. (This is my first post here, so apologies in advance if this is not the right venue for this sort of question.)

I’m trying to create something similar to a looper pedal: The user creates a synth, by sending an OSC message to the SC server, which begins recording to a buffer. Later, at a time of their choosing, the user sends a second message to begin playback from the buffer, looping with a loop period (as close as possible to) equal to time since the recording began. Recording continues into the buffer. The user does not send any additional messages to re-trigger the loop.

I don’t know a good way to compute the loop period when I send the second message (which can create a new synth, or change the state of an existing one).

Hi Patrick, welcome to the forum!
As you can imagine, there might be quite many ways of accomplishing this, I just want to share with you a few ideas to start experimenting with:

  1. To measure the time interval between to inputs, you might want to save the time of the first input, and subtract it from the time of the second. Now, how to get those timestamps? You could use the Date class: http://doc.sccode.org/Classes/Date.html#*getDate.
    A simple example:
~recordTrigTime = nil;

// call this function to record
~record = {
	// store trigger time
	~recordTrigTime = Date.getDate().rawSeconds
	// ... then start recording
};

// call this function to playback
~playback = {
	// assure recordTrigTime is stored
	if(~recordTrigTime.notNil){
		// compute duration (in seconds)
		var loopDur = Date.getDate().rawSeconds - ~recordTrigTime;
		loopDur.postln;
		// do something with your loopDur
	}
};

// TEST
~record.();

~playback.(); 
  1. Let’s get more specific about your intentions: you want this duration to define a slice of a continuously recording buffer, to be extracted and looped. So we want:
  • a buffer to be recorded all the time
  • a synth that records all the time
  • a way to ask the synth at which position in the buffer it is currently recording: we can do this with a trigger, SendReply and OSCFunc ( example below )
  • when we know between which samples our loop is, we need a way to copy that data out from our recording buffer, into a new one

Here is an example, it is not complete, but it has the most important information you would need IMO:


SynthDef(\rec){|buf,in=0|
	// get input signal
	var inSig = SoundIn.ar(in);
	// compute write position
	var writePos = Phasor.ar(start:0,end:BufSamples.ir(buf));
	// record signal to buffer continuously and looping
	var rec = BufWr.ar(inSig,buf,writePos,loop:1);
	// \poll.tr is a trigger control:
	// we can send a trigger from the language to get back an OSC message
	// from this synth, containing the current write position in samples.
	SendReply.kr(\poll.tr,'/recSample',writePos);
}.add;


// start recording
r = Synth(\rec,[buf:~recBuf])

// listen to replies from our synth
f = OSCdef(\getSample,{|msg|
	var writePos = msg[3].asInt;
	msg.postln;
	writePos.postln;
	
	// writePos is a position is samples
	// now you could store it and use it to copy a slice of recBuf 
	// to a new buf
},'/recSample',argTemplate:[r.nodeID])

// ask for positions
r.set(\poll,1)

// example function to get a new slice
~getBufSlice = {|source,start,end|
	// 1. allocate a new buffer.
	Buffer.alloc(s,end-start,1,{|allocatedBuf|
		// completionMsg: send this message when buffer is allocated
		// it has to be an OSC message, so we use copyMsg
		// to get sounds from our source buffer to the new one
		source.copyMsg(allocatedBuf,0,start,end-start)
	});
}

// you would use it like this:
// ~recBuf is our source buffer, 
// then we give a start and end sample
// and we get a new buffer in b!
b = ~getBufSlice.(~recBuf,0,210944)
b.play

Hope this can get you started!

Many thanks for the detailed reply! These approaches will get me most of the way.

If I’m understanding correctly, all of these suggestions (and all of the ones I’ve been able to google) involve sending data back to sclang (or, in my case, Processing), collecting it, and using it to compose a second message.

Perhaps I’m asking too much from the server side, but is there a way to accomplish this purely in the context of the server (scsynth) and a pair of OSC messages?

The looping workflow is a super common use case, people ask about it all the time - share your progress here, I’m sure it’ll be helpful to others as well!

I’ll throw some other ideas then!
The only thing that we can’t do in a SynthDef here,I believe, is to allocate new buffers.
And we need to do it: otherwise your continuously recording synth will sooner or later overwrite your old loops.
You should also know that there is a problem in SC with long buffers: above a certain length (6.2 minutes at 44.1kHz, not that long!) you loose precision in reading them. In case you want to know more, the only reference I found in just a quick search for this issue is this post from hjh.

(EDIT: couple more resources about the precision issue are this issue and this plugin)

Maybe a minimal flow could be like this:

  1. allocate recording buffer
  2. start recording synth
  3. trigger synth to store start position
  4. trigger synth to poll start position and number of frames
    A modified synthdef that can do 3 and 4:
SynthDef(\rec){|buf,in=0|
	var inSig = SoundIn.ar(in);
	var writePos = Phasor.ar(start:0,end:BufSamples.ir(buf));
	
	var startTrig = \saveStart.tr;
	var endTrig = \getLoop.tr;
	var startPos = Latch.ar(writePos,startTrig)
	var loopDur = Sweep.ar(startTrig,BufSampleRate.ir(buf))
	
	var rec = BufWr.ar(inSig,buf,writePos,loop:1);
	SendReply.kr(endTrig,'/recSample',[startPos,loopDur]);
}.add;
  1. receive reply from synth: send buffer allocation.
  2. when allocation is done, server replies with [/done /b_alloc]: then we can send a copy message. ( sending a new message afer [/done /b_alloc] is easy in sclang, using the completionMethod argument of Buffer.alloc ).
  3. when copy is done server replies with /done, then you can start a looping synth.
    In sclang, you can accomplish points 5, 6 and 7 with an OSCdef and Server.sync or Server.sendMsgSync
// this function receives a /recSample message from the server
OSCdef(\makeBuf,{|msg|
    var start,durSamples; 
    # start, durSamples = msg.drop(3);
    // fork a new function for async operations:
    fork{
        var newBuffer = Buffer.alloc(start,durSamples,1);
        s.sync;
        ~recBuf.copyData(newBuffer,0,start,durSamples);
        s.sync;
        Synth(\myTrustedLooper, [buf: newBuffer]);  
    }
},'/recSample')

EDIT:
I noticed that Buffer.copyData doesn’t loop into the source buffer: it means that if you start recording when your recorder is near the end of ~recBuf and it loops back to the start, you need two copy messages:

~copyLoop = {|start,dur|
	var end = start + dur;
	var sourceDur = b.numFrames;
	// if our end is within bounds, all normal
	if(end < sourceDur){
		~recBuf.copyData(c,0,start,dur);
	}{
		// otherwise copy the first part
		~recBuf.copyData(c,0,start,-1);
		// and then the second
		~recBuf.copyData(c,sourceDur-start,0,end-sourceDur);
	}
};

Thanks for the further ideas!

Allocating new buffers isn’t an issue for me - I want to continue recording because I want the option to have multiple layered loops, but the max length of the buffer itself can be chosen ahead of time.

I am not familiar with this syntax - is this a “symbol”? why?

var startTrig = \saveStart.tr;

This still involves the client (sclang), which I’m trying to avoid. That means that I don’t want to use SendReply.kr. I want to send a single OSC message (or bundle) which creates the playback synth (or changes the state of an existing playback synth), looping to exactly the point when the message/bundle arrived. Is that possible?

About symbols: it’s an alternative syntax for synthdef arguments, that makes it easier to specify their rate (e.g. tr is trigger rate, a convenient way to send triggers to synths). Find more info on this post about NamedControl.
About SendReply: I’m quite sure (not tested yet) that you can receive those OSC messages also in your client, regardless of it being sclang or not. I think it would be the way to achieve most flexibility and precision.

But ok, if you are ok setting up your buffers beforehand with a predefined length, then you could just have a synthdef that records the whole buffer when the synth is created, and then keeps looping. Something super simple like

SynthDef(\recLoop){|in=0,out=0,buf=0,gate=1|
	var sig = SoundIn.ar(0);
	var rec = RecordBuf.ar(sig,buf,loop:0);
	// start playback when rec is done
	var recDone = Done.kr(rec);
	var play = PlayBuf.ar(1,buf,1,recDone,loop:1);
	// this env helps avoiding clicks
	var env = EnvGen.kr(Env.asr(0.02,1,0.02),recDone*gate,doneAction:2);
	Out.ar(out, play * env);
}.add

This would record always “the future”: you create a synth and it starts recording. If you want to loop “the past”, then you need to have a cyclical buffer like we discussed before, and to handle it you need to copy to other buffers the material you want to loop, otherwise it will be overwritten at one point (soon, considering that you might not want a buffer to be longer than 5 minute).
I now realize that you could combine the two approaches:

  1. have a synth recording continuously to a cyclical buffer
  2. when you want to loop a certain duration of time, allocate a buffer of that length and create a synth like the one above, but record from the cyclical buffer rather than from an input:
// modified version to rec from source buffer
SynthDef(\recLoop){|in=0,out=0,buf=0,sourceBuf=0,sourceStart=0,gate=1|
	var sig = PlayBuf.ar(
		1,sourceBuf,1,1,sourceStart*BufSampleRate.ir(sourceBuf)
	);
	var rec = RecordBuf.ar(sig,buf,loop:0);
	// start playback when rec is done
	var recDone = Done.kr(rec);
	var play = PlayBuf.ar(1,buf,1,recDone,loop:1);
	// these envs helps avoiding clicks
	var recEnv = EnvGen.kr(Env.asr(0.02,1,0.02),1-recDone);
	var env = EnvGen.kr(Env.asr(0.02,1,0.02),recDone*gate,doneAction:2);
        // while recording, play rec, when rec is done, play play :)
	Out.ar(out, (rec*recEnv) + (play * env));
}.add

Maybe this is more like something you want?

tl;dr seems like it works with one buffer, one BufRd, and two Phasors?

Lots of great stuff here, and thanks very much for the tip about NamedControl!

From the examples above, if I know in my client (scsynth or whatever way I’m sending OSC to scsynth) how long I want the loop to be, then there’s a good way to proceed. Make a synth play a loop of the required length. I’m being difficult, though, in the interest of having less client-side logic and also (maybe only marginally) improving the accuracy of the loop triggering.

To clarify my intended question, pretend that I only have access to a very very minimal client. I only have the ability to send a pair of OSC pre-determined messages to scserver. In particular, these messages don’t contain the loop length! The second message is sent some time after the first. Is it possible to compute the loop length on the server?

Below is my current attempt. It resolves the main question, I think (yes), but I’m of course never sure I’m doing things correctly/efficiently/robustly/safely/idiomatically, so I’d love more comments.

First, to demonstrate the approach, here’s an example which fills a buffer with the test sound, sends a first message to create the a looping synth, and sends a second message to start looping. The trick is to use a pair of Phasors, which move in parallel until the loop is triggered. The value of the first determines the loop length of the second. The second is used to read from the buffer. When the loop is triggered, the first phasor freezes.

I like this approach because, in addition to satisfying my criteria about only controlling things with a pair of (pre-determined) messages to the server, this only involves one buffer and one BufRd.

// Test to start looping from a buffer with a simple message to the server
(
// Read the test sound into a mono buffer
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

SynthDef(\looper_prerec, {
	arg
	   out     = 0,
	   bufnum  = 0,
	   loop_on = 0;
	var
	   play,
	   play_head,
	   loop_length,
	   looping;

	// Allow a single change from 0.0 to 1.0
	looping = SetResetFF.kr(loop_on);

	// The loop length tracks the "play head" until looping = 1.0, when it freezes
	loop_length = Phasor.ar(
		trig:  0,
		rate:  (1.0-looping) * BufRateScale.kr(bufnum),
		start: 0,
		end:   BufFrames.kr(bufnum)
	);

	// Play from the buffer with the given loop length
	play_head = Phasor.ar(
		trig: 0,
		rate: BufRateScale.kr(bufnum),
		start: 0,
		end: loop_length
	);
    play= BufRd.ar(1, bufnum, play_head);

    Out.ar(out,Pan2.ar(play))
}).send(s);
)

~my_synth = Synth("\looper_prerec",[\bufnum,b]);

~my_synth.set("\loop_on",1.0);

Once that works, here’s my attempt at doing it while recording to your own buffer.

// Similar, but record from an audio input
(
b = Buffer.alloc(
	server:      s,
	numFrames:   s.sampleRate * 10.0, // 10 seconds maximum
	numChannels: 1
);

// Define Synth
SynthDef(\looper_thing, {
	arg
	   in_channel    = 1, // 1-based for AudioIn.ar
	   out           = 0,
	   bufnum        = 0,
	   start_looping = 0;
	var
	   play,
	   play_head,
	   loop_length,
	   looping;

	// Only allow loop to be turned on, and only once
	looping = SetResetFF.kr(start_looping);

	// The loop length is the same as the play head until "frozen" by looping = 1.0
	loop_length = Phasor.ar(
		trig:  0,
		rate:  (1.0-looping) * BufRateScale.kr(bufnum),
		start: 0,
		end:   BufFrames.kr(bufnum));

	// Record to our buffer
	RecordBuf.ar(
		inputArray: AudioIn.ar(in_channel),
		bufnum:     bufnum,
		loop:       0
	);

	// Play from our buffer with the computed loop length
    play_head = Phasor.ar(
		trig:  0,
		rate:  BufRateScale.kr(bufnum),
		start: 0,
		end:   loop_length);
	play = BufRd.ar(1, bufnum, play_head);

    Out.ar(out,Pan2.ar(play));
}).send(s);
)

// First message - create the synth. Start making noise immediately!
~my_synth = Synth("\looper_thing",[\bufnum,b]);

// Second message - start looping, once you've made enough noise
// (Note: if you wait until after the buffer is full, it'll start looping the whole things, and sending this message will loop part of it)
~my_synth.set("\start_looping",1.0);

Hey Patrick, it is nice and clever with the freezing Phasor!
I see a potential problem if you send another message after you defined the loop, am I right? What would happen is that loop end start growing again from where it stopped, until it reaches the end of the buffer: then loop_length would drop to 0 and start growing again from there.

It also doesn’t seem like you want to loop “in the past”, like saying “loop last 5 seconds of audio”. From your approach it looks like you want to say “start recording now, later I’ll tell you to loop from the start”. If this is what you want to do, you can do it IMO more easily with 3 messages:

  1. allocate new loop buffer
  2. run synth to record and play with that buffer
  3. trigger rec stop: looping start

A simple synthdef could use Timer for this:

SynthDef(\recLoop){|buf=0,in=0,out=0,recording = 1|
    var input = SoundIn.ar(in);
	// Timer measures times between triggers
	// Changed emits a trigger when recording changes value
	// Gate updates recEnd only when not recording
	// BufSampleRate because Timer measures seconds and we need samples instead
	var recEnd = Gate.kr(
		Timer.kr(Changed.kr(recording)),
		1-recording
	)*BufSampleRate.ir(buf);
	
	// record buf from start to end
	// run only when recording
	// jump to start when recording is started
    var rec = RecordBuf.ar(input,buf,run:recording,loop:0,trigger:recording);
	
	var play = LoopBuf.ar(1,buf,1,
		// when gate is positive: loop,
		// when gate is negative: play normally
		// (don't loop when recEnd is still at 0)
		gate:recEnd>1,
		startPos:0,
		startLoop:0,
		endLoop:recEnd
	);
	
	Out.ar(0,play);
}.add

~buf = Buffer.alloc(s,44100*10); // alloc buffer
x = Synth(\recLoop,[buf:~buf]); // start synth
x.set(\recording,0); // start looping
x.set(\recording,1); // new rec
x.set(\recording,0); // start new loop

My last example using the language is more flexible, uses less memory and allows looping in the past. But if you don’t want any of these features, here you have a quick and simple one.

Let us know how it goes!