|
|
TITLE:: FluidBuf* Multithreading Behaviour
|
|
|
SUMMARY:: A tutorial on the multithreading behaviour of offline processes of the FluCoMa toolbox for signal decomposition
|
|
|
CATEGORIES:: Libraries>FluidDecomposition
|
|
|
RELATED:: Guides/FluCoMa, Guides/FluidDecomposition
|
|
|
|
|
|
DESCRIPTION::
|
|
|
The Fluid Decomposition toolbox footnote::This toolbox was made possible thanks to the FluCoMa project ( http://www.flucoma.org/ ) funded by the European Research Council ( https://erc.europa.eu/ ) under the European Union’s Horizon 2020 research and innovation programme (grant agreement No 725899).:: provides an open-ended, loosely coupled set of objects to break up and analyse sound in terms of slices (segments in time), layers (superositions in time and frequency) and objects (configurable or discoverable patterns in sound). Almost all objects have audio-rate and buffer-based versions.
|
|
|
|
|
|
These latter buffer-based processes can be very CPU intensive, and therefore require a careful consideration of the underlying architecture. Luckily, the FluidBuf* have different entry points, from transparent usage to more advanced control, to allow the creative coder to care as much as they need too. The overarching principle is to send the CPU intensive tasks to their own background thread, to avoid blocking the Server and its Non-Real Time thread, whilst providing ways to cancel the tasks and monitor their progress.
|
|
|
|
|
|
subsection:: Basic Usage
|
|
|
|
|
|
In SuperCollider, the server will delegate to a second, non-real-time thread, tasks that are potentially too long for the real-time server, for instance, loading a soundfile to a buffer. This process is explained HERE and HERE, and for the inquisitive mind, in chapter XX of the SuperCollider book.
|
|
|
|
|
|
The problem with the FluidBuf* tasks is that they are much longer than any of these native tasks, so we have to send them in their own thread in order to leave both real-time and non-real-time native server threads alone and responsive.
|
|
|
|
|
|
The first approach, the simplest, is therefore to call the 'process' method on the FluidBuf* objects. For this tutorial, we will use a dummy class, LINK::Classes/FluidBufThreadDemo::, which in effects does nothing but to wait on that new thread before sending back one value in a buffer.
|
|
|
|
|
|
CODE::
|
|
|
// define a destination buffer
|
|
|
b=Buffer.alloc(s,1);
|
|
|
|
|
|
// a simple call, where we query the destination buffer upon completion with the action message.
|
|
|
FluidBufThreadDemo.process(s, b, 1000, {|x|x.get(0,{|y|y.postln});});
|
|
|
::
|
|
|
|
|
|
This will print 1000 in the Post window. But actually, this is what is happening:
|
|
|
NUMBEREDLIST::
|
|
|
|
|
|
## The class will check the arguments' validity
|
|
|
## It will send the job to a new thread (in this case, doing nothing but waiting for 1000 ms, then writing that number to index [0] of a destination buffer)
|
|
|
## It will received an acknoledgment of the job being done
|
|
|
## It will call the user-defined function with the destination buffer as argument. In this case, we send it to a function get which prints the value of index 0.
|
|
|
::
|
|
|
Actually, what is really happening is going to be discussed below, but this should be enough for most use cases.
|
|
|
|
|
|
subsection:: Cancelling
|
|
|
|
|
|
The 'process' method returns an instance of FluidNRTProcess, which is a pointer to a Synth running in the background. This allows us to cancel the job.
|
|
|
|
|
|
CODE::
|
|
|
//start a long process, capturing the instance of the process
|
|
|
c = FluidBufThreadDemo.process(s, b, 100000, {|x|x.get(0,{|y|y.postln});});
|
|
|
|
|
|
//cancel the job. Look at the Post Window
|
|
|
c.cancel;
|
|
|
|
|
|
//////////////////////////////
|
|
|
////// FOR GERARD AND OWEN: we are still getting a call to the done function, which would be good to avoid. We can comment on this here when it is sorted.
|
|
|
//////////////////////////////
|
|
|
::
|
|
|
|
|
|
subsection:: KR Usage
|
|
|
|
|
|
If we look at the class definition, we will see that the 'process' method is actually calling a temporary Synth which spawns a thread with the job defined by the specific UGen. We can also call this specify UGen directly, in order to get a feedback on how the process is going.
|
|
|
|
|
|
CODE::
|
|
|
// if a simple call to the UGen is used, the progress can be monitored
|
|
|
{FluidBufThreadDemo.kr(b,10000, Done.freeSelf);}.scope;
|
|
|
|
|
|
//or polled within a synth
|
|
|
{FluidBufThreadDemo.kr(b,3000).poll}.play;
|
|
|
|
|
|
//or its value used to control other processes, here changing the pitch, whilst being polled to the window twice per second
|
|
|
{SinOsc.ar(Poll.kr(Impulse.kr(2),FluidBufThreadDemo.kr(b,3000)).exprange(110,220),0,0.1)}.play;
|
|
|
::
|
|
|
|
|
|
To cancel the job that way, we just free the synth, and the background thread will be killed.
|
|
|
|
|
|
CODE::
|
|
|
// load a buffer, declare a destination, and make a control bus to monitor the work
|
|
|
(
|
|
|
b = Buffer.read(s,File.realpath(FluidBufNMF.class.filenameSymbol).dirname.withTrailingSlash ++ "../AudioFiles/Tremblay-AaS-SynthTwoVoices-M.wav");
|
|
|
c = Buffer.new(s);
|
|
|
d = Bus.control(s,1);
|
|
|
)
|
|
|
|
|
|
// start a very long job
|
|
|
e = {Out.kr(d,FluidBufNMF.kr(b, destination:c, components:50, iterations:1000, windowSize:8192, hopSize:256))}.play
|
|
|
|
|
|
// make a dummy synth to look at the progress
|
|
|
f = {In.kr(d).poll}.play
|
|
|
|
|
|
// stop the monitoring
|
|
|
f.free
|
|
|
|
|
|
//make a slighly more useful use of the progress
|
|
|
f = {SinOsc.ar(In.kr(d).poll.exprange(110,880),0,0.1)}.play
|
|
|
|
|
|
//kill the process
|
|
|
e.free
|
|
|
|
|
|
//kill the synth
|
|
|
f.free
|
|
|
|
|
|
//to appreciate the multithreading, use your favourite CPU monitoring application. scsynth will be very, very high, despite the peakCPU and avgCPU being very low.
|
|
|
::
|
|
|
|
|
|
|
|
|
subsection: Further Reading
|
|
|
|
|
|
a few tutorials on how messed up it is, includign the thread sync and NRT thread in SC. |