diff --git a/.gitignore b/.gitignore index b3ab3b4..6615466 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ schelp interfaces/* *.log **/.DS_Store +.vscode diff --git a/doc/AmpGate.rst b/doc/AmpGate.rst index 30ffdf8..4581a8c 100644 --- a/doc/AmpGate.rst +++ b/doc/AmpGate.rst @@ -1,16 +1,16 @@ -:digest: Amplitude-based Gating Slicer +:digest: Gate Detection on a Signal :species: slicer :sc-categories: Libraries>FluidDecomposition :sc-related: Guides/FluidCorpusManipulationToolkit :see-also: BufAmpGate, AmpSlice, OnsetSlice, NoveltySlice, TransientSlice -:description: This class implements an amplitude-based slicer, with various customisable options and conditions to detect absolute amplitude changes as onsets and offsets. -:discussion: - FluidAmpSlice is based on an envelop follower on a highpassed version of the signal, which is then going through a Schmidt trigger and state-aware time contraints. The example code below is unfolding the various possibilites in order of complexity. +:description: Absolute amplitude threshold gate detector on a real-time signal - The process will return an audio steam with square envelopes around detected slices the different slices, where 1s means in slice and 0s mean in silence. +:discussion: + AmpGate outputs a audio-rate, single-channel signal that is either 0, indicating the gate is closed, or 1, indicating the gate is open. The gate detects an onset (opens) when the internal envelope follower (controlled by ``rampUp`` and ``rampDown``) goes above a specified ``onThreshold`` (in dB) for at least ``minLengthAbove`` samples. The gate will stay open until the envelope follower goes below ``offThreshold`` (in dB) for at least ``minLengthBelow`` samples, which triggers an offset. -:output: An audio stream with square envelopes around the slices. The latency between the input and the output is **max(minLengthAbove + lookBack, max(minLengthBelow,lookAhead))**. + The latency between the input and the output is **max(minLengthAbove + lookBack, max(minLengthBelow,lookAhead))**. +:output: An audio stream of gate information, always either 1, indicating the gate is open, or 0, indicating the gate is closed. :control rampUp: @@ -22,41 +22,40 @@ :control onThreshold: - The threshold in dB of the envelope follower to trigger an onset, aka to go ON when in OFF state. + The threshold in dB of the envelope follower to trigger an onset: go from 0 (closed) to 1 (open). :control offThreshold: - The threshold in dB of the envelope follower to trigger an offset, , aka to go ON when in OFF state. + The threshold in dB of the envelope follower to trigger an offset: go from 1 (open) to 0 (closed). :control minSliceLength: - The length in samples that the Slice will stay ON. Changes of states during that period will be ignored. + The minimum length in samples for which the gate will stay open. Changes of states during this period after an onset will be ignored. :control minSilenceLength: - The length in samples that the Slice will stay OFF. Changes of states during that period will be ignored. + The minimum length in samples for which the gate will stay closed. Changes of states during that period after an offset will be ignored. :control minLengthAbove: - The length in samples that the envelope have to be above the threshold to consider it a valid transition to ON. The Slice will start at the first sample when the condition is met. Therefore, this affects the latency. + The length in samples that the envelope must be above the threshold to consider it a valid transition to 1. The gate will change to 1 at the first sample when the condition is met. Therefore, this affects the latency (see latency equation in the description). :control minLengthBelow: - The length in samples that the envelope have to be below the threshold to consider it a valid transition to OFF. The Slice will end at the first sample when the condition is met. Therefore, this affects the latency. + The length in samples that the envelope must be below the threshold to consider it a valid transition to 0. The gate will change to 0 at the first sample when the condition is met. Therefore, this affects the latency (see latency equation in the description). :control lookBack: - The length of the buffer kept before an onset to allow the algorithm, once a new Slice is detected, to go back in time (up to that many samples) to find the minimum amplitude as the Slice onset point. This affects the latency of the algorithm. + When an onset is detected, the algorithm will look in the recent past (via an internal recorded buffer of this length in samples) for a minimum in the envelope follower to identify as the onset point. This affects the latency of the algorithm (see latency equation in the description). :control lookAhead: - The length of the buffer kept after an offset to allow the algorithm, once the Slice is considered finished, to wait further in time (up to that many samples) to find a minimum amplitude as the Slice offset point. This affects the latency of the algorithm. - + When an offset is detected, the algorithm will wait this duration (in samples) to find a minimum in the envelope follower to identify as the offset point. This affects the latency of the algorithm (see latency equation in the description). + :control highPassFreq: - The frequency of the fourth-order Linkwitz-Riley high-pass filter (https://en.wikipedia.org/wiki/Linkwitz%E2%80%93Riley_filter). This is done first on the signal to minimise low frequency intermodulation with very fast ramp lengths. A frequency of 0 bypasses the filter. + The frequency of the fourth-order Linkwitz-Riley high-pass filter (https://en.wikipedia.org/wiki/Linkwitz%E2%80%93Riley_filter) applied to the input signal to minimise low frequency intermodulation with very short ramp lengths. A frequency of 0 bypasses the filter. :control maxSize: - How large can the buffer be for time-critical conditions, by allocating memory at instantiation time. This cannot be modulated. - + The size of the buffer to allocate at instantiation time for keeping track of the time-critical conditions (``minSliceLength``, ``minSilenceLength``, ``minLengthAbove``, ``minLengthBelow``, ``lookBack``, and ``lookAhead``). This cannot be modulated. diff --git a/doc/BufAmpGate.rst b/doc/BufAmpGate.rst index d23cb15..f27a28f 100644 --- a/doc/BufAmpGate.rst +++ b/doc/BufAmpGate.rst @@ -1,86 +1,82 @@ -:digest: Amplitude-based Gating Slicer for Buffers +:digest: Gate Detection on a Bfufer :species: buffer-proc :sc-categories: Libraries>FluidDecomposition :sc-related: Guides/FluidCorpusManipulationToolkit :see-also: AmpGate, BufAmpSlice, BufOnsetSlice, BufNoveltySlice, BufTransientSlice -:description: This class implements an amplitude-based slicer, with various customisable options and conditions to detect absolute amplitude changes as onsets and offsets. +:description: Absolute amplitude threshold gate detector on audio in a buffer + :discussion: - FluidBufAmpGate is based on an envelop follower on a highpassed version of the signal, which is then going through a Schmidt trigger and state-aware time contraints. The example code below is unfolding the various possibilites in order of complexity. - The process will return a two-channel buffer with the addresses of the onset on the first channel, and the address of the offset on the second channel. + BufAmpGate outputs a two-channel buffer containing open and close positions of the gates. Each frame of the buffer contains an onset (opening) position on channel 0 and the corresponding offset (closing) position on channel 1 (both in samples). The buffer will have as many frames as gate events detected. + + The gate detects an onset (opens) when the internal envelope follower (controlled by ``rampUp`` and ``rampDown``) goes above a specified ``onThreshold`` (in dB) for at least ``minLengthAbove`` samples. The gate will stay open until the envelope follower goes below ``offThreshold`` (in dB) for at least ``minLengthBelow`` samples, which triggers an offset. :output: Nothing, as the destination buffer is declared in the function call. - :control source: - The index of the buffer to use as the source material to be sliced through novelty identification. The different channels of multichannel buffers will be summed. + The buffer to analyse for gate information. Multichannel buffers will be summed to mono for analysis. :control startFrame: - Where in the srcBuf should the slicing process start, in sample. + Where in ``source`` to begin the analysis (in samples). The default is 0. :control numFrames: - How many frames should be processed. + How many frames (audio samples) to analyse. The default of -1 indicates to analyse through end of the buffer :control startChan: - For multichannel sources, which channel should be processed. + For multichannel sources, at which channel to begin the analysis. The default is 0. :control numChans: - For multichannel sources, how many channel should be summed. + For multichannel sources, how many channels should be included in the analysis (starting from ``startChan``). The default of -1 indicates to include all the channels from ``startChan`` through the rest of the buffer. If more than one channel is specified, the channels will be summed to mono for analysis. :control indices: - The index of the buffer where the indices (in sample) of the estimated starting points of slices will be written. The first and last points are always the boundary points of the analysis. + The buffer to write the gate information into. Buffer will be resized appropriately so each frame contains an onset (opening) position on channel 0 and the corresponding offset (closing) position on channel 1 (both in samples). The buffer will have as many frames as gate events detected. :control rampUp: - The number of samples the envelope follower will take to reach the next value when raising. + The number of samples the envelope follower will take to reach the next value when rising. :control rampDown: - The number of samples the envelope follower will take to reach the next value when falling. + The number of samples the envelope follower will take to reach the next value when falling. :control onThreshold: - The threshold in dB of the envelope follower to trigger an onset, aka to go ON when in OFF state. + The threshold in dB of the envelope follower to trigger an onset. :control offThreshold: - The threshold in dB of the envelope follower to trigger an offset, , aka to go ON when in OFF state. + The threshold in dB of the envelope follower to trigger an offset. :control minSliceLength: - The length in samples that the Slice will stay ON. Changes of states during that period will be ignored. + The minimum length in samples for which the gate will stay open. Changes of states during this period after an onset will be ignored. :control minSilenceLength: - The length in samples that the Slice will stay OFF. Changes of states during that period will be ignored. + The minimum length in samples for which the gate will stay closed. Changes of states during that period after an offset will be ignored. :control minLengthAbove: - The length in samples that the envelope have to be above the threshold to consider it a valid transition to ON. The Slice will start at the first sample when the condition is met. Therefore, this affects the latency. + The length in samples that the envelope must be above the threshold to consider it a valid onset. The onset will be triggered at the first sample when the condition is met. :control minLengthBelow: - The length in samples that the envelope have to be below the threshold to consider it a valid transition to OFF. The Slice will end at the first sample when the condition is met. Therefore, this affects the latency. + The length in samples that the envelope must be below the threshold to consider it a valid offset. The offset will be triggered at the first sample when the condition is met. :control lookBack: - The length of the buffer kept before an onset to allow the algorithm, once a new Slice is detected, to go back in time (up to that many samples) to find the minimum amplitude as the Slice onset point. This affects the latency of the algorithm. + When an onset is detected, the algorithm will look in the recent past (this length in samples) for a minimum in the envelope follower to identify as the onset point. :control lookAhead: - The length of the buffer kept after an offset to allow the algorithm, once the Slice is considered finished, to wait further in time (up to that many samples) to find a minimum amplitude as the Slice offset point. This affects the latency of the algorithm. - + When an offset is detected, the algorithm will wait this duration (in samples) to find a minimum in the envelope follower to identify as the offset point. + :control highPassFreq: - The frequency of the fourth-order Linkwitz–Riley high-pass filter (https://en.wikipedia.org/wiki/Linkwitz%E2%80%93Riley_filter). This is done first on the signal to minimise low frequency intermodulation with very fast ramp lengths. A frequency of 0 bypasses the filter. - -:control maxSize: - - How large can the buffer be for time-critical conditions, by allocating memory at instantiation time. This cannot be modulated. - + The frequency of the fourth-order Linkwitz-Riley high-pass filter (https://en.wikipedia.org/wiki/Linkwitz%E2%80%93Riley_filter) applied to the signal signal to minimise low frequency intermodulation with very short ramp lengths. A frequency of 0 bypasses the filter. diff --git a/example-code/sc/AmpGate.scd b/example-code/sc/AmpGate.scd index e29369b..1cfb41f 100644 --- a/example-code/sc/AmpGate.scd +++ b/example-code/sc/AmpGate.scd @@ -1,76 +1,44 @@ - +strong::Watch the gate:: code:: -//basic tests: threshold sanity -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12); - [source, env] -}.plot(0.1); -) -//basic tests: threshold hysteresis -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -16); - [source, env] -}.plot(0.1); -) -//basic tests: threshold min slice -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, minSliceLength:441); - [source, env] -}.plot(0.1); -) -//basic tests: threshold min silence -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, minSilenceLength:441); - [source, env] -}.plot(0.1); -) -//mid tests: threshold time hysteresis on -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, minLengthAbove:441); - [DelayN.ar(source,0.1,441/44100), env] -}.plot(0.1); -) -//mid tests: threshold time hysteresis off -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, minLengthBelow:441); - [DelayN.ar(source,0.1,441/44100), env] -}.plot(0.1); -) -//mid tests: threshold with lookBack -( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, lookBack:441); - [DelayN.ar(source,0.1,441/44100), env] -}.plot(0.1); -) -//mid tests: threshold with lookAhead + +// make sure the third bus is only output. ( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, lookAhead:441); - [DelayN.ar(source,0.1,441/44100), env] -}.plot(0.1); +s.options.numOutputBusChannels_(4); +s.options.numInputBusChannels_(4); +s.reboot; ) -//mid tests: threshold with asymetrical lookBack and lookAhead + +~src = Buffer.read(s,FluidFilesPath("Nicol-LoopE-M.wav")); + ( -{var env, source = SinOsc.ar(320,0,LFTri.ar(10).abs); - env = FluidAmpGate.ar(source, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, lookBack:221, lookAhead:441); - [DelayN.ar(source,0.1,441/44100), env] -}.plot(0.1); +{ + var source = PlayBuf.ar(1,~src,BufRateScale.ir(~src),loop:1); + var env = FluidAmpGate.ar(source, rampUp:441, rampDown:2205, onThreshold:-27, offThreshold: -31, minSilenceLength:4410, lookBack:441, highPassFreq:20).poll; + var sig = DelayN.ar(source,delaytime:441/44100) * env.lag(0.02); // compenstate for latency. + [sig,sig,env]; +}.scope; ) -//drum slicing, many ways -//load a buffer -b = Buffer.read(s,FluidFilesPath("Nicol-LoopE-M.wav")); -//have fun with a gate (explore lookahead and lookback, but correct for latency, which will be the greatest of the lookahead and lookback) + +:: +strong::Use for manipulating FX:: +code:: + +~src = Buffer.read(s,FluidFilesPath("Harker-DS-TenOboeMultiphonics-M.wav")); + ( -{var env, source = PlayBuf.ar(1,b); - env = FluidAmpGate.ar(source, rampUp:441, rampDown:2205, onThreshold:-27, offThreshold: -31, minSilenceLength:4410, lookBack:441, highPassFreq:20); - [DelayN.ar(source,delaytime:441/44100), env] -}.plot(2, separately:true); +{ + arg thresh = -35; + var src = PlayBuf.ar(1,~src,BufRateScale.ir(~src),loop:1); + var localbuf = LocalBuf(s.sampleRate).clear; + var gate = FluidAmpGate.ar(src,10,10,thresh,thresh-5,441,441,441,441).poll; + var phs = Phasor.ar(0,1 * gate,0,localbuf.numFrames/2); // only write into the buffer when the gate is open + var trig = Dust.kr(50) * (1-gate); // only trigger grains from that buffer when the gate is closed + var dur = 0.1; + var pos = ((phs + localbuf.numFrames/2) - TRand.kr(dur*SampleRate.ir,localbuf.numFrames/2)) / localbuf.numFrames; + var sig = GrainBuf.ar(2,trig,dur,localbuf,1,pos,4,TChoose.kr(trig,[-0.9,0.9]),mul:6.dbamp); + BufWr.ar(src,localbuf,[phs,phs+(localbuf.numFrames/2)]); + sig + src; +}.scope; ) -:: + +:: \ No newline at end of file diff --git a/example-code/sc/BufAmpGate.scd b/example-code/sc/BufAmpGate.scd index ebda8ee..5df8790 100644 --- a/example-code/sc/BufAmpGate.scd +++ b/example-code/sc/BufAmpGate.scd @@ -1,12 +1,131 @@ +CODE:: +~src = Buffer.read(s,FluidFilesPath("Constanzo-PreparedSnare-M.wav")); + +// detect gates and post some info +( +~indices = Buffer(s); +FluidBufAmpGate.processBlocking(s,~src,indices:~indices,rampUp:110,rampDown:2205,onThreshold:-40, offThreshold:-41,minSilenceLength:1100,lookBack:441); +~indices.loadToFloatArray(action:{ + arg fa; + var events = (fa.size / 2).asInteger; + var avg = fa.clump(2).collect{arg arr; arr[1] - arr[0]}.mean / ~src.sampleRate; + "found % gate events averaging % seconds".format(events,avg).postln; +}); +) + +//loops over a gate event from onset to offset using MouseX to choose which gate event +( +{ + var gate_index = MouseX.kr(0,~indices.numFrames).poll(label:"gate index"); + var start, end, phs; + # start, end = BufRd.kr(2,~indices,gate_index,1,1); + phs = Phasor.ar(0,BufRateScale.ir(~src),start,end); + BufRd.ar(1,~src,phs,1,4).dup; +}.play; +) +:: +strong::Visualize it with FluidWaveform:: +code:: + +~src = Buffer.read(s,FluidFilesPath("Constanzo-PreparedSnare-M.wav")); +( +~indices = Buffer(s); +FluidBufAmpGate.processBlocking(s,~src,indices:~indices,rampUp:110,rampDown:2205,onThreshold:-40, offThreshold:-41,minSilenceLength:1100,lookBack:441); +FluidWaveform(~src,~indices,bounds:Rect(0,0,1600,400)); +) + +~src = Buffer.read(s,FluidFilesPath("Nicol-LoopE-M.wav")); +( +~indices = Buffer(s); +FluidBufAmpGate.processBlocking(s,~src,indices:~indices,rampUp:110,rampDown:441,onThreshold:-20, offThreshold:-24,minSilenceLength:441,lookBack:441); +FluidWaveform(~src,~indices,bounds:Rect(0,0,1600,400)); +) + +:: +strong::Remove silence from a buffer:: +code:: + +~src = Buffer.read(s,FluidFilesPath("Olencki-TenTromboneLongTones-M.wav")); + +( +var concat_buf = Buffer(s); +var indices = Buffer(s); +var display_buffer = Buffer(s); +FluidBufCompose.processBlocking(s,~src,destination:display_buffer); +FluidBufAmpGate.processBlocking(s,display_buffer,indices:indices,onThreshold:-30,offThreshold:-40,minSliceLength:0.1*s.sampleRate,minSilenceLength:0.1*s.sampleRate,rampDown:0.01*s.sampleRate); +indices.loadToFloatArray(action:{ + arg fa; + var current_frame = 0; + + // this array is initally flat, but is alternating [ onset0 , offset0 , onset1 , offset1 , onset2 ... ], + // so by using .clump(2) we clump each onset and offest together to get an array like this: + // [ [ onset0 , offset0 ] , [ onset1 , offset1 ] , [ onset2 , offset2 ] , ... ] + fa = fa.clump(2); + + fa.do{ + arg arr, i; + var startFrame = arr[0]; + var numFrames = arr[1] - startFrame; + "%\tstart: %\tend: %".format(i,startFrame,numFrames).postln; + FluidBufCompose.processBlocking(s,display_buffer,startFrame,numFrames,destination:concat_buf,destStartFrame:current_frame); + current_frame = current_frame + numFrames; + }; + + FluidBufCompose.processBlocking(s,concat_buf,destination:display_buffer,destStartChan:1); + s.sync; + + { + var win = Window(bounds:Rect(0,0,1600,400)); + FluidWaveform(display_buffer,parent:win,bounds:win.bounds,standalone:false); + UserView(win,win.bounds) + .drawFunc_{ + ["original","with silence removed"].do{ + arg txt, i; + var y_ = (win.bounds.height * 0.5 * i) + (win.bounds.height * 0.1); + Pen.stringAtPoint(txt,Point(10,y_),color:Color.red); + }; + }; + win.front; + display_buffer.play; + }.defer; +}); +) +:: +strong::Visualizing Parameters:: code:: -// define a test signal and a destination buffer + +~drum_hit = Buffer.read(s,FluidFilesPath("Nicol-LoopE-M.wav"),350854,21023); + +// we'll try to tweak some parameters to slice out this drum hit just how we want +( +~drum_hit.play; +FluidWaveform(~drum_hit); +) + +// just putting in some thresholds probably won't provide what we're looking for ( - b = Buffer.sendCollection(s, Array.fill(44100,{|i| sin(i*pi/ (44100/640)) * (sin(i*pi/ 22050)).abs})); - c = Buffer.new(s); +~indices = Buffer(s); +FluidBufAmpGate.processBlocking(s,~drum_hit,indices:~indices,onThreshold:-20,offThreshold:-20); +FluidWaveform(~drum_hit,~indices); ) -b.play -b.plot + +// adjusting the ramp times (units are in samples) will help smooth out the envelope follower +( +~indices = Buffer(s); +FluidBufAmpGate.processBlocking(s,~drum_hit,indices:~indices,rampUp:441,rampDown:4410,onThreshold:-20,offThreshold:-20); +FluidWaveform(~drum_hit,~indices); +) + +:: +strong::Basic Tests:: +code:: + +( +b = Buffer.sendCollection(s, Array.fill(44100,{|i| sin(i*pi/ (44100/640)) * (sin(i*pi/ 22050)).abs})); +c = Buffer.new(s); +) + //basic tests: threshold sanity FluidBufAmpGate.process(s, b, indices:c, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12) c.query @@ -51,66 +170,4 @@ c.getn(0,c.numFrames*2,{|item|item.postln;}) FluidBufAmpGate.process(s, b, indices:c, rampUp:5, rampDown:25, onThreshold:-12, offThreshold: -12, lookBack:221, lookAhead:441) c.query c.getn(0,c.numFrames*2,{|item|item.postln;}) -:: - -STRONG::A musical example.:: -CODE:: -//load a buffer -( - b = Buffer.read(s, FluidFilesPath("Nicol-LoopE-M.wav")); - c = Buffer.new(s); -) - -// slice the samples -( -Routine{ - FluidBufAmpGate.process(s, b, indices:c, rampUp:110, rampDown:2205, onThreshold:-27, offThreshold: -31, minSilenceLength:1100, lookBack:441, highPassFreq:40).wait; - c.query; - c.getn(0,c.numFrames*2,{|item|item.postln;}); - //reformatting to read the onsets and offsets as pairs - c.getn(0,c.numFrames*2,{|items|items.reshape(c.numFrames,2).do({|x| x.postln});}); -}.play -) -//loops over a splice with the MouseX, taking the respective onset and offset of a given slice -( - { - BufRd.ar(1, b, - Phasor.ar(0,1, - BufRd.kr(2, c, - MouseX.kr(0, BufFrames.kr(c)), 0, 1)[0], - BufRd.kr(2, c, - MouseX.kr(1, BufFrames.kr(c)), 0, 1)[1], - BufRd.kr(2,c, - MouseX.kr(0, BufFrames.kr(c)), 0, 1)[0] - ), 0, 1); - }.play; -) -:: - -STRONG::A stereo buffer example.:: -CODE:: -// make a stereo buffer -b = Buffer.alloc(s,88200,2); - -// add some stereo clicks and listen to them -((0..3)*22050+11025).do({|item,index| b.set(item+(index%2), 1.0)}) -b.play - -// create a new buffer as destinations -c = Buffer.new(s); -//run the process on them -( - // with basic params - Routine{ - var t = Main.elapsedTime; - var proc= FluidBufAmpGate.process(s, b, indices: c, rampUp:1, rampDown:10, onThreshold: -30); - proc.wait; - (Main.elapsedTime - t).postln; - }.play -) - -// list the indicies of detected attacks - the two input channels have been summed. The two channels of the output, respectively onset and offset indices, are interleaved as this is the SuperCollider buffer data formatting -c.getn(0,c.numFrames*2,{|item|(item*2).postln;}) -// a more readable version: deinterleave onsetand offset -c.getn(0,c.numFrames*2,{|items|items.reshape(c.numFrames,2).do({|x| (x*2).postln});}) -:: +:: \ No newline at end of file