Slicing at Zero Crossings?

Hello all,
i was wondering: do the slicing tools in flucoma set slicing points at zero crossings, or could they if necessary?

1 Like

Hello!

No they don’t have that option, they yield as precisely as the selected algorithm can where it detects a slice. To add a zero crossing is interesting and can be done relatively easily in the buffer version if Max/SC/Pd: when you retrieve the indices, check the value and read backwards in time until it changes sign… for real time you’d have to add a delay to do that, or to take the next zero crossing with the same idea.

Hello @tremblap,
thank you for your suggestion! While i understand the idea im not sure about the execution though: FluidBufOnsetSlice returns a buffer with frame indices. Would i need another object (or specific method of FBOS) to check on the value at those indices then and move in time to find the nearest zero (change of sign)? And how can the original slices frames be updated to account for the difference to the next zc?
What would be interesting about this is that one could work without envelopes and similarly to Waveset approaches!
Jan

from your opcode spelling I gather you are in SuperCollider.

One idea would be to get the indices in the language in an array, then for each item retrieve the previous 50 samples and run the condition of finding the zero crossing. That way you can update each slice index to a ‘better’ value for your use case.

This could be fun indeed for Waveset… speaking of which, I don’t know if you saw my modest implementation of a buffer process doing dirty waveset stretching?

yes, i always forget to mention its sc!:slight_smile:

im a bit at loss on how to “run the condition of finding the zero crossing”, probably an example would help to make it clearer.

(
b = Buffer.read(s,File.realpath(FluidBufOnsetSlice.class.filenameSymbol).dirname.withTrailingSlash ++ "../AudioFiles/Nicol-LoopE-M.wav");
c = Buffer.new(s);
)

(
Routine{
    t = Main.elapsedTime;
    FluidBufOnsetSlice.process(s,b, indices: c, threshold:0.5).wait;
    (Main.elapsedTime - t).postln;
}.play
)

c.loadToFloatArray(action:{ |i| x=i}) //moving indices into array

x //now retrieve the previous 50 samples and run condition to adjust to zc's, but how?

and i didn’t know about those offline buffer processes will check them, thanks @tremblap !

ok here goes a basic example… which almost works. As I am not as good in SC as @tedmoore @spluta @francesco.cameli etc… they will know a better way I’m sure but that is a start - I am just stalled in my iterative array processing here. But it might help you get started until someone else can help.

(
fork{
	var cond = Condition.new;
	y = Array.newClear(x.size);
    x.do{ // will run the function on each element of x and collect the return result as an array
	arg raw_index, which_iter;
	var new_index;
	b.getToFloatArray(raw_index - 199, 200, action: {|context| // get the 199 samples before the detected slicing point
		var crossings = Array.newClear(199);
		context.reverse;//flip the order so we are looking from the present to the past
		context.doAdjacentPairs { |a,b,i| // compare consecutive elements
				crossings[i] = (a > 0) != (b > 0); // if they have different parity (aka zero crossing)
		};
			new_index = crossings.detectIndex{|a| a}; // find the first item that is True aka diff polarity aka zero crossing
			if (new_index.isNil, {new_index = raw_index}, {new_index = raw_index - new_index}); //if none were found pass back the original index, otherwise offset it by how far back we had to go to find a zero crossing
			y[which_iter] = new_index; // write the new index in the array of index
		cond.unhang; //then we can do the next pass
	});
	cond.hang; //wait for the iteration to be done
	}
}
)
	
y.postln

I didn’t dare to ping @jamesbradbury and @weefuzzy who also have better SC chops than me - but they might see where my code bails - it is working for the first item, so I must be messing up the condition position… initially I did y = x.collect but I was getting strange results… all of this points are a racing condition and the wonderful world of batch processing in SC with all the sync issues…

Here’s my humble effort. It runs and does stuff, but I haven’t really checked if it’s the right stuff

(
b = Buffer.read(s,FluidFilesPath("Nicol-LoopE-M.wav"));
c = Buffer.new(s);

{  
    var raw, adjusted; 
    var cond = Condition.new(); 
    
    //takes a slice point and loads (up to) the 199 preceding samples from the audio buffer leading up to that point and looks for a zero crossing. If one (or more) is found, replace the slice point in the `adjusted` array with the nearest zero cross to the original point 
    var zerox = { |p,i| 
        b.loadToFloatArray((p-199).max(0),199.min(p),action:{|a|
            
            // find zero crossings by looking for changes of sign (ignoring the very first as a false positive) 
            var zeroxes = a.sign.differentiate.drop(1).selectIndices{|x,j| x != 0};
    
            if(zeroxes.isEmpty.not){adjusted[i] = p - (199 - zeroxes.last)}; 
            
            if(i == (c.numFrames - 1)) {cond.unhang};
    })
    };
    
    //after slicing, retreve the slice buffer as an array and immediately trigger the zerox function above for each slice point    
    FluidBufOnsetSlice.process(s,b, indices: c, threshold:0.5, action:{ |sliceBuffer|
        sliceBuffer.loadToFloatArray(action: {|a|
            adjusted = a.copy; //copy originals, to overwrite later in zerox
            raw = a;
            a.do{|x,i|
                    zerox.value(x,i)      
            }
        })
     });
    
    cond.hang; 
    ("Raw" + raw).postln; 
    ("Adjusted" + adjusted).postln; 
}.fork(TempoClock.default);
)
1 Like

so elegant :slight_smile:

The 199.min(p) should be, iiuc, 199.min(b.numFrames-p) right?

Otherwise I love the elegance of the zerox line.

for people not on the nightlies, the loading needs to happen the old way:

b = Buffer.read(s,File.realpath(FluidBufOnsetSlice.class.filenameSymbol).dirname.withTrailingSlash ++ "../AudioFiles/Nicol-LoopE-M.wav");

playing with the code, if someone wanted to find ascending only zero crossings, that would be possible with the line

a.sign.differentiate.drop(1).selectIndices{|x,j| x > 0};

thanks again @weefuzzy !

Another approach might be to produce your own zero crossing segmentation and then lookup nearest slices either from novelty → zerox or zerox → novelty using FluidKDTree as a more interactive way of working out which segmentation you like. Example coming in the evening, or possibly a surrender to the fact that the idea may be too hard to realise :slight_smile:

Tried it in SC but I don’t have the chops :joy:

Here is a Max version if you care to delve into the visual side of programming :wink:

zero-crossing-lookup.maxpat (29.6 KB)

Wow, thanks for all the suggestions and ideas! i haven’t yet had the chance to try them all, but will on first occasion and a quiet moment! Really great to see all these different approaches and thoughts on the matter!:slight_smile: I do believe that an option with zero crossing could potentially be interesting as a fixed implementation on slicing generally, but thats rather a dream of the future;)

1 Like