-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Realtime conventions
Give hints on
- how to write your new code real-time safe
- what we need to change in current code to make LMMS realt-time safe
Realtime safety is required to guarantee no lags whenever using LMMS for live performance. There's an article summarizing this very well.
LMMS is not real time safe in 2019 (as opposed to zynaddsubfx or Ardour). Some users don't consider LMMS professional because of this, so it's important to attract developers.
- audio thread - thread with high priority, which does the audio computations and must be finished fast
- UI thread - thread with normal priority
... are calls that give back control to the kernel. Many C and C++ standard library calls are syscalls. Most of them can not be used in audio threads, e.g. malloc
, new
, printf
, pthread_mutex_lock
, sleep
... There is also a whitelist, e.g. strcmp
, atoi
, sinf
are safe.
Don't use Qt data structures that use copy-on-write (e.g. QVector
, QString
and many others). E.g., if you have
QVector<int> v2 = v1;
in an UI thread, and then start with something like
v1[0] = 42;
in the realtime thread, this can cause an allocation (calling the forbidden malloc
). Even if you try your best to enforce the copy-on-write in the UI threads, this can not be checked automatically by stoat. So just don't use those data structures, use std::vector
etc.
Mutexes can not be used (see "Syscalls" above), and the audio thread should also never block in general (avoid spinlocks).
This requires more advanced communication mechanisms...
... are the only good way to exchange data between threads.
Example: If an audio thread needs a resizable buffer, it can not resize it on it's own (malloc
and new
are syscalls). If a UI thread does it, that UI thread should first does the allocation, not interacting in any way with the audio thread. If the new memory is finally returned to the UI thread, it does an atomic exchange of the buffer pointer. The audio thread is not blocked at any time.
As there's often more communication than just 1 pointer between UI and audio threads, take care of atomically writing things that must be read together. Example: You exchange one buffer and a float that normalize it. If a UI thread first exchanges the buffer pointer, then the normalizing float, the audio thread could still have an old buffer pointer and then read the new float. This could lead to playing the buffer with wrong normalizing, in the worst case, playing it too loud. This almost always leeds to...
Ringbuffers can be used to exchange different information through one FIFO channel. The read and write indexes are atomics. For example, this allows a UI thread to first write an audio buffer and the normalizing float into the ringbuffer, and then increasing the ringbuffer pointer, letting the audio thread either see "nothing new" or "both at once".
Increasing the atomical ringbuffer pointer is just like exchanging a buffer pointer. So a single atomic variable, like described above, is just a special case of a "size 1 ringbuffer".
If you want to send different things through one ringbuffer, identifiers before the data are often required: <next is buffer><buffer content><next ist string><string content>
. Those identifiers describing the next content can be simple integers. In zyn, we wanted to make those identifiers usable by external UIs, so we used OSC strings instead of integers, like /adsynth/voice0/volume\0<some float value>
.
Note: It can be better to copy small buffers into ringbuffers, instead of using pointers. That way, a UI thread can delete its pointer, knowing it's not in use by the audio thread.
A real time safe ringbuffer implementation using atomics can be found in the ringbuffer repo, which LMMS uses as a submodule.
This is often a decision between readability/paradigm (messages) and runtime (direct). Messages can be good when access patterns are sparse or there is non-deterinism in regards to what's available. Direct access can make it more difficult to understand which thread owns which variable and in what order the communications work.
Calls of bad functions can be checked using stoat:
- Prepare stoat once:
-
git clone git@github.com:JohannesLorenz/stoat.git -b johannes-fixes
(note, you can probably now take the official fundamtenal/stoat, since the required patches have been merged there. it must be tested though) mkdir build && cd build && cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..
-
make
and thenmake test
.
-
- Mark any function from LMMS that must meet realtime requirements with
__attribute__((annotate("realtime")))
(right before the semicolon of the declaration). Good candidates are e.g.-
EffectName::processAudioBuffer()
to check a single effect -
AudioJack::staticProcessCallback()
to check the whole core (run with Jack)
-
-
mkdir build-stoat && cd build-stoat
(you may have one for gcc or clang already, but the next one will be incompatible) -
cmake -DCMAKE_C_COMPILER=/path/to/your/stoat/repo/stoat-compile -DCMAKE_CXX_COMPILER=/path/to/your/stoat/repo/stoat-compile++ ..
(if you only want to check one plugin, combine this with-DPLUGIN_LIST=<name of dir in plugin/ folder>
, e.g.-DPLUGIN_LIST=SpectrumAnalyzer
). Note: If you entered the wrong compiler, you must remove the build directory and start over. make -j <cores>
touch whitelist2.txt
-
/path/to/your/stoat/repo/stoat -l /path/to/your/stoat/repo/build/libstoat.so -w whitelist2.txt -r ..
. The-l
just gives stoat its libary. The-w
tells stoat that all functions inwhitelist2.txt
are assumed realtime safe and shall not produce warnings. The-r .
just tells it to scan your whole build dir. - Now for every error, you have different choices:
- If the function marked as "unsafe" should be safe (for Qt classe, check e.g. the Qt sources), add it to your whitelist
- If the function marked as "unsafe" can be unsafe, rewrite the code.
- If you are finished with 0 errors, your code should be realtime safe in the sense of not calling any bad functions (only assuming you don't use non-direct-connected signals/slots). Keep your whitelist file, it will be needed later. Maybe it is worth to add some of the files to the stoat project, or at least to LMMS.
- Use mlockall once in LMMS to keep all memory resident
- Alsa + Jack: Give real time priority to all audio worker threads (currently, only 1 thread has it)