Looper with a variable length?


#1

Hi,
I’m trying to build a basic looper using RecordBuf and PlayBuf, but I’m having a problem with looping the buffer at the point I ended the recording rather than at the end of the buffer.

I’m starting from:

~buf1 = Buffer.alloc(s, s.sampleRate * 10, 1); // 10 second buffer

(
SynthDef(\rec, { arg out = 0, bufnum, rec = 0;
var in;
in = SoundIn.ar(0);
RecordBuf.ar(in, bufnum, run: rec, doneAction: 0, loop: 1);
}).add;

SynthDef(\play, {
arg bufnum, rate = 1, start = 0, loop = 1;
var sig;
sig = PlayBuf.ar(1, bufnum, rate, 0, start, loop, 0);
Out.ar(0, sig);

}).add;

)

~rec = Synth(\rec, [\bufnum, ~buf1, \rec, 1]); // Starts recording in ~buf1
~rec.set(\rec, 0); // Stops the recording

~play = Synth(\play, [\bufnum, ~buf1, \loop, 1]) // Plays it, looped

So if I record for example 5 seconds into the 10 second buffer, it will play the 5 seconds, then 5 seconds of silence (the remainder of the buffer), and only then loop around. What I want is to loop just those 5 seconds of sound.
(Also as this is for use in beat-less music, I need a solution that works outside of tempo and bar syncronicity, unlike some projects I found online for beat-based music.)

My first thougth (and only so far) was to have a DetectSilence on the PlayBuf trigger, so that it jumps to the startPos when the buffer becomes silent, however it seems that there is a problem with having the DetectSilence inside the Ugen it’s detecting.
(On the sig line above I tried and got an error:
sig = PlayBuf.ar(1, bufnum, rate, DetectSilence.ar(sig), start, loop);
)

I’m not experienced at all with more complex controlling like that, so I would be very thankful for some help (and I bet there is a very simple and also very clever way to solve this)


#2

Sorry, no time to work out a solution right now, but some thoughts.

I think the design of an appropriate soultion would depend very much on how accurate things must be. Consider: starting the rec synth as well as stopping it are - as its defined now - actions, where you don’t know exactly when things happen. Without latency starting and stopping happen as soon as the concerned messages arrive at the server.
That might be fine, but then - if you want to know exact recording time in lang - you must measure it in the recording synth, e.g. with the help of a Timer UGen, and send it back to lang (SendReply).

Then you can playback and loop the exact time span and maybe treat the special case of a recording that is longer than the buffer. I’d recommend BufRd instead of PlayBuf as it’s more flexible with subtle playback demands.

An alternative strategy would be to start and stop with latency (see sendBundle), a small latency of 50 ms wold be sufficient. Then you could measure the exact recording time in language (e.g. with Main.elapsedTime) and use that directly for playback. For latency issues see also:
http://doc.sccode.org/Guides/ServerTiming.html

A third rather rough solution would be this: ignore OSC messaging time, work without latency and measure the recording time in lang. Then take this time (or a little less) for looping.

Another point is if you want strict looping or crossfading to avoid clicks. Here PlayBufCF (wslib quark) would be a straight solution, otherwise you’d have to define crossfading of two playbacks yourself.

BTW: don’t worry, there have been many recording questions like this on the lists. It often turns out that a simple idea of recording and playback isn’t so simple at all and a lot of details has to be regarded in the concrete application.


#3

The way I have been dealing with stuff like this in the past has been to 1. record to harddisk, 2. load the sample into a buffer, 3. play back the buffer with PlayBuf. I don’t have the code at hand right now, but this approach (even though it is far from sample-accurate), will work in most situations when you don’t know the length of the recording in advance. If you are worried about clogging up your harddisk, you can always set the path to /tmp if you are on *nix, alternatively put in a cleanup-function to delete the files when you close the document…


#4

Yeah, I was hoping I would be able to keep it simple by not needing a perfectly accurate bpm looping, but it seems like I still need time measuring to know what portion of the buffer to play. I’ll look into your suggestions, thanks a lot!

This is interesting, I’d appreciate taking a look at the code if it’s easily available to you. I’m wondering how to set up all these steps in a practical manner. Thanks!


#5

On my phone now, so can’t be too comprehensive, unfortunately, but the general idea is something like this:


p = “path/to/recfile.wav”;

b = Bus.audio(s, 1);

s.prepareForRecord(p);

~buf = Array.new;

i = Synth(\someInput, [\out, b]);

~startRec = { s.record(p, b, 1) };

~stopRec = Routine ({
    s.stopRecording;
    s.sync;
    ~buf = ~buf.add( Buffer.read(s, p));
    s.sync;
    ~player = Synth(playbuf, [\buf, ~buf.size - 1]);
});

~startRec.value();
~stopRec.value();

Presupposes a playbuf synthdef with a buf argument. This will be monophonic, though. in order to get polyphony you need to put the players in an array as well. Check Eli’s excellent tutorials on that!


#6

If you use RecordBuf, then you have no idea of the position in the buffer that’s being written.

If you use Phasor and BufWr, then you do know the exact position that’s being written. So then, when you stop recording, you can send this information back to the language using SendReply.

(
// oh, also, you don't need an 'out' because you're not 'Out.ar'-ing anything
SynthDef(\rec, { arg bufnum, rec = 1;
	var in = SoundIn.ar(0),
	phase = Phasor.ar(0, 1, 0, BufFrames.kr(bufnum)),
	stopTrig = (rec <= 0);
	BufWr.ar(in, bufnum, phase);
	SendReply.ar(K2A.ar(stopTrig), '/recEnded', phase);
	FreeSelf.kr(stopTrig);
}).add;
)

b = Buffer.alloc(s, 44100 * 10, 1);

OSCdef(\ended, { |msg|
	// msg is ['/recEnded', nodeID, replyID, value0...]
	// so the data point is msg[3]
	e = msg[3];  // save ending frame index
}, '/recEnded', s.addr);

a = Synth(\rec, [bufnum: b]);  // recording...

a.set(\rec, 0);  // stop

e
-> 144385.0

Now we know that, in my test, I hit ‘stop’ exactly 144385 / 44100 = 3.274036 seconds after starting.

IMO this is more efficient than recording temp files to disk. There’s a reason why SC has UGens to send information back to the language – exactly for cases like this.

hjh


#7

Ooh, there’s the clever full of things I haven’t seen before awnser I was expecting. Going to (slowly) work on understanding everything that has been suggested so far and see where that leads me. Thanks everyone!


#8

Or, a few changes in the SynthDef can make it stop recording at the end of the buffer.

(
SynthDef(\rec, { arg bufnum, rec = 1;
	var in = SoundIn.ar(0),
	frames = BufFrames.kr(bufnum),
	// if only 'frames' here, then it will loop at the end
	// instead, go a little extra, and check the boundary in 'stopTrig'
	phase = Phasor.ar(0, 1, 0, frames + 1000),
	stopTrig = (rec <= 0) + (phase >= frames);
	BufWr.ar(in, bufnum, phase);
	SendReply.ar(stopTrig, '/recEnded', phase);
	FreeSelf.kr(stopTrig);
}).add;
)

hjh


#9

Ok, here’s where I got to:

The OSCdef is perfect, I’m now working on the play SynthDef with BufRd and Phasor.
The first problem was making it loop after ~e seconds, so I’m using an Impulse.ar(1/~e) on the Phasor trigger argument. This worked great for playing the whole recording unaltered, but now I’m having some trouble figuring out how to manipulate it inside Phasor.

Varying rate is fine, changing the trig to Impulse.ar(rate/~e) to adjust the trig time and BufRateScale.kr(bufnum) * rate on the rate argument.

The start, end, and reset is where I’m having trouble. Normally I would use BufFrames.kr * start/end, and I think ~e * s.sampleRate * start is equal to that same behavior inside the recorded portion?
As soon as I use different start and end positions it has weird results, so I think I’m doing something wrong here. I also have to figure out a way for the trigger to follow the shorter length of different start/end positions, but I’m still working on that.

For the resetPos, it seems I just don’t understand how it works, since the tests I’ve done don’t follow what I understood from the help files at all.

This is a lot of text and I’m somewhat of a confused begginer, so thanks a lot for all the attention and help :slight_smile:

(
SynthDef(\rec, { arg bufnum, rec = 1;
	var in = SoundIn.ar(0),
	phase = Phasor.ar(0, 1, 0, BufFrames.kr(bufnum)),
	stopTrig = (rec <= 0);
	BufWr.ar(in, bufnum, phase);
	SendReply.ar(K2A.ar(stopTrig), '/recEnded', phase);
	FreeSelf.kr(stopTrig);
}).add;

)

b = Buffer.alloc(s, 44100 * 10, 1);

(
OSCdef(\ended, { |msg|
	~e = msg[3];  
	~e = ~e/s.sampleRate;       // already converts to seconds here (felt more natural but might just not make sense)
}, '/recEnded', s.addr);
)


(
~e = 0;  // needs a value to register the SynthDef (I think?)    
SynthDef(\play, {
	arg envtime = 10, loop = 1, int = 2, pan = 0, amp = 1, bufnum =0, start = 0, end = 1, rate = 1;
	var sig, env, phs, trig;
	trig = Impulse.ar(rate /~e , 0);      // Needs change
	phs = Phasor.ar(
		trig: trig, 
		rate: BufRateScale.kr(bufnum) * rate,  
		start: (~e * s.sampleRate) * start,     // Might be wrong
		end: (~e * s.sampleRate) * end,       // Might be wrong
                resetPost: 0
	);
	env = EnvGen.kr(Env([0,1,1,0], [0.001, envtime, 0.001],), doneAction: 2);
	sig = BufRd.ar(1, bufnum, phs, loop, int);
	sig = sig * env;
	Out.ar(0, Pan2.ar(sig,  pan, amp));
}).add;
)



a = Synth(\rec, [bufnum: b]); 

a.set(\rec, 0);  

Synth( \play, [\rate, 1, \envtime, 30, \pan, 0, \loop, 1, \amp, 1, \bufnum, b, \start, 0, \end, 1]) // works fine

Synth( \play, [\rate, 1, \envtime, 30, \pan, 0, \loop, 1, \amp, 1, \bufnum, b, \start, 0.1, \end, 1]) // doesn't seem to

#10

@jamshark70 Thanks for dragging me out of my tmp-file misery! I officially love this solution.


#11

By the way: if you put your code

in code tags like this with three ```s above and three ```s below

it is both easier to read the code and it looks cool as well :wink:


#12

Ok, it seems that using ~e inside the SynthDef only updates the value when I evaluate the code rather than every time ~e is updated. Any guidance here? Not sure what to do or how to call this behavior so I can search for it.


#13

This is a quite common point of confusion in SC.

  • I have a variable.
  • I used the variable inside a SynthDef.
  • I expected changes in the variable to affect the SynthDef, but it doesn’t.

First, SC doesn’t have “lazy operations” on variables.

a = 10;
b = a + 1;
a = 1;
b
-> 11  // not 2

b = a + 1 does not mean that b becomes bound to an operation that adds 1 to a. It means instead: 1. Look up a's value right now. 2. Add 1 to it. 3. Save that in b. In #1, it doesn’t matter if 10 comes from a or anywhere else. After resolving the variable to its value, the variable is forgotten.

That’s relevant to your SynthDef because rate / ~e does:

  1. Resolve rate to its value (a control-rate signal coming from the argument).
  2. Resolve ~e to its value (a fixed number set in the language).
  3. Divide.

#2 forgets all about ~e. There is no permanent link to the variable. You change ~e but the SynthDef doesn’t know anything about ~e – it knows only that ~e had some value at the time of building. It’s using that value only.

You already know how to put changeable values into a SynthDef: arg. But for some reason, for this one value, you haven’t done that. And, unsurprisingly, you’re having problems with it.

SynthDef(\play, {
	arg envtime = 10, loop = 1, int = 2, pan = 0, amp = 1, bufnum =0, start = 0, end = 1, rate = 1;
	arg e = 1;
	..... then use `e` instead of `~e`
}).add;

(
OSCdef(\ended, { |msg|
	~e = msg[3] / s.sampleRate;
	if(playSynth.notNil) { playSynth.set(\e, ~e) };
}, '/recEnded', s.addr);
)

With that said – I would suggest writing the SynthDef to play only one time through the loop. Then use a pattern to play a new synth for each loop cycle.

  1. With an envelope, you’ll get an automatic crossfade. Looping with a single BufRd is very likely to make a click at every loop point. You don’t want that.
  2. Scsynth’s sample clock will slowly drift out of sync with sclang’s clocks – you don’t notice it now, but if you run the thing for 10 minutes, you might. One synth per cycle will continually resync to the language clock, and everything stays together.

hjh


#14

I have been experimenting with the “XL Live Looper” available from here: https://sccode.org/tag/category/looper
That I think does what you what. Its a function but you can wrap it as a synthdef. You pass a buffer of a given length to the looper-synth, then you send a trigger to start and stop recording, and then it loops playback, based on the length of the recording [from the timestamps saved from the trigger signals].


#15

I’ve decided to go with looping a single BufRd instead of a pattern, because I want to control it with a GUI, and a Pbindef doesn’t seem to respond very smoothly to the discrete values of sliders and buttons.
Now I’m (of course) running into the click problem. It seems to me that knowing the lenght of the loop, I could just set a trigger of that length to retrigger an envelope (with Impulse.ar(1/length)), but that is not working. Is it not supposed to go at the gate argument of an EnvGen?
Aside from that, it’s been working as intended, and it’s quite fun to play with - thanks everyone for the help so far. And I’ll check out the XL Looper, thanks!


#16

Hi,
I’m the author of XL Looper and several other SC live loopers. I’ve tried many concepts to achieve this over the years and I’m going to try to list a few of them here along with some problems that you may run into:

  1. What James suggested:
    0.1. Allocate a buffer that’s much bigger than what you would need for the loop
    1.1. start a recording synth and save the time you started it in a var.
    1.2.1 Stop (and free) the recording synth,
    1.2.2. calculate the recording time (now - time you started recording),
    1.2.3. create a TempoClock with a tempo calculated from the recording time and the number of beats your recording has,
    1.2.4 play a pattern that plays your play/overdub synth on the TempoClock

Discussion: This works really well. Its probably the most flexible of the concept but there are some quirks to work around if you want features that most looping pedals have. The calculations and the TempoClock creation happen so fast there is no noticable lag at all. In order to be able to adjust parameters of the play/overdub synth in between loop points, you need to put those synths in a group and when you change a parameter use the group as a target. Also set the new val in a var in sclang and have the pattern read the var when the new play/overdub synth is fired. This way, you can use this concept to have overdub run for more than one repetitions of the loops.

  1. XL Looper: This is basically the same concept but all the logic is in one SynthDef. It’s far less flexible but very cool to have to only allocate a buffer and start a synth. It also has some “Beat Repeat”-like capabilities. The original version contains some mistakes however. I remember I rewrote it a few years ago and I’m not sure I updated the code on scscode. I’ll look into it soon, I promise.

  2. Last year, I implemented a completely different concept that’s very close to a “digital tape loop”. Here, a long buffer is looping and recording constantly and the loop length is determined by the sample position of the play synth. It uses a Phasor in combination with BufRd and IBufWr. This makes it possible to create cool pitch shift and dangerous feedback fx. It’s here: https://github.com/X2theL/CaesarLooper
    The documentation is extensive. I’ll update that one soon as well.