Generate new buffers from Onset Detection on a Buffer

I am trying to have a sound file play, detect the onsets, then use those onsets to record into different smaller buffers to have multiple chunks of sound. Like take a sound file that has 4 drum hits, detect the drum hits and allocate those individual hits to seperate buffers for later use. I see a similar idea in another thread where someone suggested: SoX tutorial: Split by silence :: Mads Kjeldgaard — Composer and developer

But I’m trying to do this all in SC, and I can’t even get to the point of detecting the onsets, and then trying to use those onsets to trigger a RecordBuf or something:

~b = Buffer.readChannel(s, "C:/Users/melas/Samples/4_rand_perc.wav", channels:[0]);

(
SynthDef.new(\onsetBuf, {
	arg buffer=~b, rate=1, trig=1, loop=0, da=2, amp=0.7, out=0;
	var sig, chain, onsets, read;
	sig = PlayBuf.ar(1, ~b, rate, trig, 0, loop, da);
	chain = FFT(LocalBuf(1024), sig).poll;
	onsets = Onsets.kr(chain, 0.5);
	//read = BufWr.ar(sig, buffer, Phasor.ar(onsets, BufRateScale.kr(0) * rate, 0, BufFrames.kr(0)));
	
	sig = Pan2.ar(sig,0);
	sig = sig * amp;
	Out.ar(out, sig);
}).add;
)

I know the above example is probably not even on the right path, but I’ve been going through example codes for things like loopers and such in hopes of finding insight for weeks now and I just can’t win.

Any help greatly appreciated. For my mental health, ha.

-p0

1 Like

Have a look at SendTrig.kr, you need that to send a OSC message from the synthdef (server) to trigger something on the language side. Something like this could be a start:

b = Buffer.readChannel(s, "C:/Users/melas/Samples/4_rand_perc.wav", channels:[0]);

//b= Buffer.read(s, Platform.resourceDir +/+ "sounds/break");

// Move the mouse to vary the threshold
(
SynthDef(\beat_track, {|bufnum= 0|
    var sig, chain, onsets, pips;
    sig = PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum), loop: 1);
    chain = FFT(LocalBuf(512), sig);
    onsets = Onsets.kr(chain, MouseX.kr(0,1), \rcomplex);
	SendTrig.kr(onsets, id: 10, value: 1); // send trigger with value 1
	Out.ar(0 , sig);
}).add;
)

x= Synth(\beat_track, [\bufnum, b.bufnum]);


o = OSCFunc({ arg msg, time; // receive trigger
    [time, msg].postln; //msg[2] is id, msg[3] is value
    //trigger a synth to record or do something else from here
},'/tr', s.addr);

x.free;
o.free;

I’m not sure what is the most precise way to detect percussive sounds or if doing this in real time and recording will actually give you the full onset of the sound in the recording but good luck :slight_smile:

The Flucoma toolkit has advanced options for spectral onset detection, e.g. a drum beat can be broken down into separate onset detectors for kick, snare, hihat etc. I have only tested the Flucoma stuff briefly, from what I experienced and what you a describing it sounds like a good place to start. I have had little luck with the SC onset detector on semi-complex signals myself.

Also, you don’t have to create new buffers, you can simply index into the original buffer using the onset detection points. There are various ways you can do this, PlayBuf is pretty straight forward, BufRd and LoopBuf are alternatives with more control.

@blindmanonacid Doesn’t Onsets already produce 0-1 triggers? Couldn’t that be used to index into the same buffer? Like when it goes to 1 start recording the buffer into a new buffer?

@Thor_Madsen Thank you, I’ll definitely check it out, I’m just really invested in trying to make this work in default SC.

Yes, you can also just copy from one buffer to another, using Phasor to track which frame is the onset and end of the sound. Something like this:

(
b = Buffer.readChannel(s, "C:/Users/melas/Samples/4_rand_perc.wav", channels:[0]);

c= 20.collect({Buffer.alloc(s, s.sampleRate*4, 1)}); //buffer pool of 20 x 4 secs

~lastStartPosition= 0;
~bufferPoolIndex= 0;

SynthDef(\beat_track, {|bufnum= 0|
    var sig, chain, onsets, phase, silence;
	phase= Phasor.ar(1, BufRateScale.kr(bufnum), 0, BufFrames.kr(bufnum));
    sig = BufRd.ar(1, bufnum, phase, loop: 0);
    chain = FFT(LocalBuf(512), sig);
    onsets = Onsets.kr(chain, 0.5, \power);
	silence= DetectSilence.ar(sig);
	SendTrig.kr(onsets, id: 10, value: phase); // send trigger with start pos
	SendTrig.ar(silence, id: 11, value: phase); // send trigger with end pos
	Out.ar(0 , sig);
}).add;

o = OSCFunc({ arg msg, time; // receive trigger
    //[time, msg].postln; //msg[2] is id, msg[3] is value
	case 
	{msg[2] == 10} {
		("startposition:"+msg[3]).postln;
		~lastStartPosition= msg[3];
	}
	{msg[2] == 11} {
		("endposition:"+msg[3]).postln;
		if(~bufferPoolIndex > 19, {~bufferPoolIndex= 0});
		b.copyData(c[~bufferPoolIndex], 0, ~lastStartPosition, msg[3] - ~lastStartPosition);
		~bufferPoolIndex= ~bufferPoolIndex+1;
	};
},'/tr', s.addr);

)

x= Synth(\beat_track, [\bufnum, b.bufnum]);

x.free;


//check the copied buffers
c[0].play;
c[1].play;
c[2].play;
//etc

o.free;
b.free;
c.do({|it| it.free});

I fiddled around with it a bit. I am indexing into the original buffer without creating a new buffer for each segment.

s.reboot;
(
Pdef.removeAll;
s.newBufferAllocators;

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav"); 

SynthDef(\onsets, {arg buf, threshold = 0.5;
	var sig = PlayBuf.ar(1, buf, BufRateScale.kr(buf));
	var chain = FFT(LocalBuf(512), sig);
	SendTrig.kr(Onsets.kr(chain, threshold));
}).add;

SynthDef(\playbuf, {arg buf, sPos, dur, playBackRate = 1, out = 0;
	// sPos & dur values are in seconds.
	var sig = PlayBuf.ar(1, buf, playBackRate, 1, sPos * SampleRate.ir);
	// sPos converted from time in seconds to number of Frames
	var env = Env.perc(0.001, dur, 8).kr(2);
	Out.ar(out, sig!2)
}).add;

o = OSCFunc({ arg msg, time;
	~onsets.add(Main.elapsedTime - ~initTime);
    ~onsets.last.postln;
},'/tr', s.addr);
)

(
// Analyse onsets and add times (in seconds) to ~onsets;
~onsets = List.new;
~initTime = Main.elapsedTime; // time = 0
x = Synth(\onsets, [\buf, b, \threshold, 0.8]); 
)

// wait for the post window to stop outputting onset values, this will last the duration of the sample (there are tools to this offline, eg. in the Flucoma toolkit, don't think that any are built in to SC, not sure...

(
// now =calculate the duration of each segment. The length of a segment 'i' is (~onsets[i + 1] - ~onsets[i]), except for the last segments which is (duration of whole sample - ~onsets.last). Duration of the whole sample in seconds = b.numFrames/s.sampleRate.
~durs = (~onsets[1..] ++ [b.numFrames/s.sampleRate]) - ~onsets;
)

(
Pdef.removeAll;
// reconstruct the original sample from the segments
Pdef(\test,
	Pbind(
		\instrument, \playbuf,
		\sPos, Pseq(~onsets, inf).trace,
		\dur, Pseq(~durs, inf).trace,
		\buf, b
	)
).play
)

(
// randomize the order and pitch of segments
Pdef(\test,
	Pbind(
		\instrument, \playbuf,
		\playBackRate, Pwhite(0.5, 2.0),
		\segment, Pwhite(0, ~onsets.size-1).trace, // produces an index
		\sPos, Pfunc{|ev|~onsets[ev.segment]}, // use the index from \segment to index in to the ~onsets list
		\dur, Pfunc{|ev|~durs[ev.segment]/ev.playBackRate},
		// use the index from \segment to index in to the ~durs list, scale by playBackRate
		\buf, b
	)
).play
)

(
// stop and clean up;
Pdef(\test).stop;
Pdef.removeAll;
o.free; // free the OSCFunc
x.free; // free the onset-detection synth
)

(
// random single shot sample at random rate
var i = rand(~onsets.size);
var rate = rrand(0.5, 2); // rate between halfspeed and doublespeed will produce pithes in the range [octave down - octave up]
Synth(\playbuf, [\buf, b, \sPos, ~onsets[i], \dur, ~durs[i]/rate, \playBackRate, rate]);
)
1 Like

This makes a lot more sense. I don’t know why I was thinking about copying the buffer when a buffer with the recording already exists.

1 Like

Let me know how it works for percussive material. The onset Ugen is kr (no ar version) so for very fast attacks the control rate detection might not be optimal. At 44100 Hz each control cycle is about 1.45 ms so it might not cut if for some types of signals. There is probably a way to build an ar detection mechanism with what’s already available in SC if need be.

Take a look at this thread OSCFunc latency testing. Bottom line is that you should use the Sweep ugen and get the time from the ugen instead of relying on the timestamp of OSCfunc. Since Onset is kr only, you will have to use Sweep.kr or wrap the ar ugen in a ar-to-control rate ugen, ie. A2K.kr(Sweep.ar). I don’t think you will gain any precision from using the Sweep.ar converted to kr so probably just use Sweep.kr