Workers
Supported bindings: ossia
It is relatively common to require to perform some long-running work in a separate thread: processing samples, etc.
Avendish provides two APIs for doing this: a general one, and a simpler one for the specific case of pre-processing the data of a port (for instance when a file is loaded).
The general idea is that the processor code sends a request to the worker with some data. The host processes the request in a thread. This returns a function which is then called back onto the processor, to update its state with the result of the computations.
General worker API Usage
In the main class, define a worker
struct/instance ; the arguments can be anything.
Here we'll have a request that takes an int, std::string
set of arguments ; to have multiple
behaviours one can use a std::variant of potential requests.
struct MyObject
{
struct worker
{
// Called from DSP thread
std::function<void(int, std::string)> request;
// "work" is called from a worker thread
static std::function<void(MyObject&)> work(int, std::string);
// The std::function object returned by work is called from the DSP thread
} worker;
};
Here is how to use it:
// 1. Within the DSP thread:
void operator()(...) {
if(something_happened) {
this->worker.request(123, "foo");
}
}
// 2. Implement the worker::work function:
std::function<void(MyObject&)> MyObject::worker::work(int x, std::string foo)
{
// 3. Implement the "threaded" work part:
// This is executed in a separate worker thread, slow operations can be done here
// safely.
// Repeat the string x times:
std::string orig = foo;
while(x > 0) {
foo += orig;
}
// 4. Return a function which will update the internal state of MyObject:
return [str = std::move(foo)] (MyObject& obj) {
// Executed in the DSP thread again, so don't do long operations here!
obj.internal_string = str;
};
}
The astute reader will have noticed a fairly bad performance issue in the function above: we are copying the entire string in the DSP thread! This is very bad.
How can that be improved?
// Sadly does not do anything as the object inside the lambda is const by default,
// so this is still making a copy
return [str = std::move(foo)] (MyObject& obj) {
obj.internal_string = std::move(str);
};
// Adding mutable at least removes the copy... but there is still a performance issue!
return [str = std::move(foo)] (MyObject& obj) mutable {
obj.internal_string = std::move(str);
};
// Replacing the string even with std::move may call `free` on `obj.internal_string`
// which is not a real-time-safe operation.
// Instead, if the data is swapped with the string variable in the lambda,
// a well-written host will make sure that the lambda ends its lifetime outside of
// the DSP thread, ensuring perfectly safe real-time operation.
return [str = std::move(foo)] (MyObject& obj) mutable {
std::swap(obj.internal_string, str);
};
A complete example is available here: https://github.com/ossia/score-addon-threedim/blob/main/Threedim/StructureSynth.hpp
Simple threaded port-processing API usage
This API is a "simplified" version of the above one for the common case of wanting to pre-process some data which was loaded from the hard drive.
It is accessed by adding the following to a port:
struct : my_file_port {
static std::function<void(MyObject&)> process(file_type data)
{
// 1. Process the raw file data. This happens in a worker thread.
int N = long_operation(data);
// 2. Return a function that will apply the change to the object.
return [N] (MyObject& obj) {
// Executed in the processing thread.
obj.foo(N);
};
}
} my_port;
A complete example is available here: https://github.com/ossia/score-addon-threedim/blob/main/Threedim/ObjLoader.hpp