You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
12 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

TITLE:: FluidBufStats
SUMMARY:: Computing Statistics on Buffers as Series.
CATEGORIES:: Libraries>FluidDecomposition, UGens>Buffer
RELATED:: Guides/FluCoMa, Guides/FluidDecomposition, Guides/FluidBufMultiThreading
DESCRIPTION::
This class implements non-real-time statistical analysis on buffer channels. Typically, a buffer would hold various time series (i.e. descriptors over time), and link::Classes/FluidBufStats:: allows this series to be described statistically. It is part of the LINK:: Guides/FluidDecomposition:: of LINK:: Guides/FluCoMa::. For more explanations, learning material, and discussions on its musicianly uses, visit http://www.flucoma.org/
The process returns a buffer where each channel of the STRONG::source:: buffer has been reduced to 7 statistics: mean, standard deviation, skewness, kurtosis, followed by 3 percentiles, by default the minimum value, the median, and the maximum value. Moreover, it is possible to request the same 7 stats to be applied to derivative of the input. These are useful to describe statistically the rate of change of the time series. The STRONG::stats:: buffer will grow accordingly, yielding the seven same statistical description of the n requested derivatives. Therefore, the STRONG::stats:: buffer will have as many channel as the input buffer, and as many frames as 7 times the requested STRONG::numDerivs::.
STRONG::Threading::
By default, this UGen spawns a new thread to avoid blocking the server command queue, so it is free to go about with its business. For a more detailed discussion of the available threading and monitoring options, including the two undocumented Class Methods below (.processBlocking and .kr) please read the guide LINK::Guides/FluidBufMultiThreading::.
CLASSMETHODS::
METHOD:: process
This is the method that calls for the slicing to be calculated on a given source buffer.
ARGUMENT:: server
The server on which the buffers to be processed are allocated.
ARGUMENT:: source
The index of the buffer to use as the source material to be processed. The different channels of multichannel buffers will be considered independently as time series.
ARGUMENT:: startFrame
The starting point (in samples) from which to copy in the source buffer.
ARGUMENT:: numFrames
The duration (in samples) to copy from the source buffer. The default (-1) copies the full lenght of the buffer.
ARGUMENT:: startChan
The first channel from which to copy in the source buffer.
ARGUMENT:: numChans
The number of channels from which to copy in the source buffer. This parameter will wrap around the number of channels in the source buffer. The default (-1) copies all of the buffer's channel.
ARGUMENT:: stats
The index of the buffer to write the statistics to. Each channel is the fruit of the statistical computations on the same channel number of the source buffer.
ARGUMENT:: numDerivs
The number of derivatives of the original time series for the statistic to be computed on. By default, none are computed. This will influence the number of frames the stats buffer will have.
ARGUMENT:: low
The rank requested for the first percentile value. By default, it is percentile 0.0, which is the minimum of the given channel of the source buffer.
ARGUMENT:: middle
The rank requested for the second percentile value. By default, it is percentile 50.0, which is the median of the given channel of the source buffer.
ARGUMENT:: high
The rank requested for the third percentile value. By default, it is percentile 100.0, which is the maximum of the given channel of the source buffer.
ARGUMENT:: outliersCutoff
A ratio of the inter quantile range (IQR) that defines a range outside of which data will be rejected. It is run on each channel independently and a single channel being flagged as outlier removes the whole frame (on all channels). The default (-1) bypasses this function, keeping all frames in the statistical measurements. For more information on this statistical process, please refer to the concept of IQR and how the whiskers of a box plot are computed here (https://en.wikipedia.org/wiki/Box_plot)
ARGUMENT:: weights
A buffer to provide relative weighting of the source material. Not providing one will not apply weighting and consider all frames equally. The provided buffer has to satisfy all of the following conditions: LIST::
## a single-channel, that will be applied to all channels of source
## exactly the same amount of frames as source
## weights must be positive (anything lower than 0 will be rejected)
::
ARGUMENT:: action
A Function to be evaluated once the offline process has finished and indices instance variables have been updated on the client side. The function will be passed stats as an argument.
RETURNS::
Nothing, as the destination buffer is declared in the function call.
EXAMPLES::
STRONG::A didactic example::
CODE::
// make a buffer of known lenght
b = Buffer.alloc(s,101);
// add known values - here, a ramp up
b.setn(0, Array.fill(101,{|i|i / 100}));
// create a new buffer as destinations
c = Buffer.new(s);
//run the process on them
(
Routine{
t = Main.elapsedTime;
FluidBufStats.process(s, b, stats:c, numDerivs:1);
(Main.elapsedTime - t).postln;
}.play
)
// list the statistics. The first seven are for the source buffer values themselves, the last seven for the first derivative of the source buffer.
c.getn(0,c.numFrames,{|item|item.postln;})
// replace the source values by a ramp down
b.setn(0, Array.fill(101,{|i| 1 - (i / 100)}));
// run the process and read the values
FluidBufStats.process(s, b, stats:c, numDerivs:1, action:{c.getn(0,c.numFrames,{|item|item.postln;})});
// replace the source values by halfsine
b.setn(0, Array.fill(101,{|i| (i * pi/ 100).sin}));
b.plot
// run the process and read the values
FluidBufStats.process(s, b, stats:c, numDerivs:1, action:{c.getn(0,c.numFrames,{|item|item.postln;})});
// replace the source values by partial halfsine
b.setn(0, Array.fill(101,{|i| (i * pi/ 50).sin.max(0)}));
b.plot
// run the process and read the values
FluidBufStats.process(s, b, stats:c, numDerivs:1, action:{c.getn(0,c.numFrames,{|item|item.postln;})});
// replace the source values by positive white noise
b.setn(0, Array.fill(101,{1.0.rand}));
b.plot
// run the process and read the values
FluidBufStats.process(s, b, stats:c, numDerivs:1, action:{c.getn(0,c.numFrames,{|item|item.postln;})});
::
STRONG::A musical example::
CODE::
// create some buffers
(
b = Buffer.read(s,File.realpath(FluidBufStats.class.filenameSymbol).dirname.withTrailingSlash ++ "../AudioFiles/Tremblay-ASWINE-ScratchySynth-M.wav");
c = Buffer.new(s);
d = Buffer.new(s);
)
//split in various chunks, collecting the indices in an array
FluidBufOnsetSlice.process(s,b, minSliceLength: 10, metric: 9, threshold: 0.4, filterSize: 7, indices: c, action:{c.loadToFloatArray(action: {|array| e = array.add(b.numFrames).addFirst(0);e.postln;})});
//describe the whole input too, here using pitch, and collecting the values in an array, dismissing the (interleaved) confidence.
FluidBufPitch.process(s,b,features:c, action:{c.loadToFloatArray(action: {|array| f = array.unlace(2)[0]; f.postln;})});
// iterate through each slice, taking the median of the first derivative of the pitch of each
(
g= Array.new;
Routine({
e.doAdjacentPairs({
arg start,end;
FluidBufStats.processBlocking(s,c,(start/512).asInteger,((end-start)/512).max(2).asInteger,0,1,d,1, action: {d.loadToFloatArray(action: {
arg array;
g = g.add(array[12]);
"% % %\n".postf((start/512).asInteger,((end-start)/512).max(2).asInteger, array[12]);
})});
});
"Done".postln;
}).play;
)
//obtain the order of indices that would sort the stats
h = g.order;
//play in loop the slice in order of pitch direction (the median of the slice's pitch variation)
(
var which = h[5];
{BufRd.ar(1, b, Phasor.ar(0,1,e[which],e[which+1],e[which]))}.play;
)
::
STRONG::Stereo Input Behaviour::
CODE::
// make a buffer of known lenght
b = Buffer.alloc(s,101,2);
// add known values - here, a ramp up on the left and negative random values on the right
b.setn(0, Array.fill(101,{|i|[i / 100,-1.0.rand]}).flat);
// plot to confirm
b.plot.plotMode_(\points)
// create a new buffer as destinations
c = Buffer.new(s);
// run the stats and send back the values
FluidBufStats.process(s, b, stats:c, numDerivs:1, action:{c.getn(0,c.numFrames * c.numChannels,{|item|d = item; d.postln})});
//looking at the result is not easy to grasp, since it is interleaved: first number is mean of L, second is mean of R, third is stddev of L, fourth is stddev or R
//this will make it tidier - the first value of each line is Left, the second is Right
d.reshape(14,2).do({|x,i|["mean\t\t","stddev\t\t","skew\t\t\t", "kurtosis\t", "min\t\t\t", "median\t\t", "max\t\t\t","d-mean\t","d-stddev\t","d-skew\t\t", "d-kurtosis", "d-min\t\t", "d-median\t", "d-max\t\t"].at(i).post;x.round(0.01).postln});"".postln;
::
STRONG::Outliers and Weights::
CODE::
// example 1a
// make a buffer of known qualities
b = Buffer.loadCollection(s,[1, 8, 9, 10, 11, 12, 99]);
// plot to confirm
b.plot.plotMode = \points;
// create a new buffer as destinations
c = Buffer.new(s);
// run the stats and send back the values
FluidBufStats.process(s, b, stats:c, numDerivs:1, action:{c.getn(0,c.numFrames,{|item|item.postln})});
// run the same array with outliers rejected if outside of 1.5 times the IQR - observe the new minimum and maximum to see
FluidBufStats.process(s, b, stats:c, numDerivs:1, outliersCutoff: 1.5, action:{c.getn(0,c.numFrames,{|item| item.postln})});
// example 1b (run the stats above, and change the value of some elements in the array too)
b = Buffer.loadCollection(s,[1, 8, 9, 10, 11, 12, 16, 99].scramble);
// example 1c (multichannel in behaviour is greedy)
// This mean that an outlier in any channel will dismiss the whole frame.
// For instance here the outlier is 99 (frame 8) in channel 0, and 1001 in channel 1 (frame 0)
// The final stats therefore has minima of [2,10002] and maxima of [8,10008]
e = [(1..8)++99, [1001] ++ 10002.series(10003,10009)].flop.scramble.flat
b = Buffer.loadCollection(s,e,2);
FluidBufStats.process(s, b, stats:c, numDerivs:1, outliersCutoff: 1.5, action:{c.getn(0,c.numFrames * c.numChannels,{|item| f =item.postln})});
//More readable format
f.reshape(14,2).do({|x,i|["mean\t\t","stddev\t\t","skew\t\t\t", "kurtosis\t", "min\t\t\t", "median\t\t", "max\t\t\t","d-mean\t","d-stddev\t","d-skew\t\t", "d-kurtosis", "d-min\t\t", "d-median\t", "d-max\t\t"].at(i).post;x.round(0.01).postln});"".postln;
//////////////
// example 2a
// make an array of 9 values, with known weigths. Scramble them pairwise for good measure (that should not change any stats)
e = [(1..9), 1.0.series(0.9,0.2)].flop.scramble.flop;
b = Buffer.loadCollection(s,e[0]);
c = Buffer.loadCollection(s,e[1]);
d = Buffer.new(s);
// run the stats and send back the values
FluidBufStats.process(s, b, stats:d, numDerivs:1, action:{d.getn(0,d.numFrames,{|item|item.postln})});
// run the same array with the weights
FluidBufStats.process(s, b, stats:d, numDerivs:1, weights: c, action:{d.getn(0,d.numFrames * d.numChannels,{|item|item.postln})});
// example 2b
e = [(1..9), 0.series(-10,-80)].flop.scramble.flop;
b = Buffer.loadCollection(s,e[0]);
c = Buffer.loadCollection(s,e[1]);
FluidBufStats.process(s, b, stats:d, numDerivs:1, weights: c, action:{d.getn(0,d.numFrames * d.numChannels,{|item|item.postln})});
// this has only negative weights, so it bails out, outputing all 0s. It also publishes a warning if the server options on verbosity are on.
// but if we scale them up
g = Buffer(s)
FluidBufScale.process(s,c,destination: g,inputLow: -100,inputHigh: 0)
// look at the new values - because 0->1 and -100->0 we get the same weights as example 2a
g.getn(0,9,{|x|x.postln})
// run the stats - same results as example 2a
FluidBufStats.process(s, b, stats:d, numDerivs:1, weights: g, action:{d.getn(0,d.numFrames * d.numChannels,{|item|item.postln})});
//example 2c (stereo input but mono weigths - works like a charm)
e = [(1..9), (101..109), 1.0.series(0.9,0.2)].flop.scramble.flop;
b = Buffer.loadCollection(s,e[0..1].flop.flat,2);
b.plot(separately: true).plotMode = \points;
c = Buffer.loadCollection(s,e[2]);
FluidBufStats.process(s, b, stats:d, numDerivs:1, weights: c, action:{d.getn(0,d.numFrames * d.numChannels,{|item|f = item.postln})});
//More readable format
f.reshape(14,2).do({|x,i|["mean\t\t","stddev\t\t","skew\t\t\t", "kurtosis\t", "min\t\t\t", "median\t\t", "max\t\t\t","d-mean\t","d-stddev\t","d-skew\t\t", "d-kurtosis", "d-min\t\t", "d-median\t", "d-max\t\t"].at(i).post;x.round(0.01).postln});"".postln;
//see the example folder for 2 musical comparisons: 1) weighted MFCCs providing different nearest neighbours, and 2) pitch manipulations
::