blockfree_chan
Multithreaded data sharing and audio processing are a painful mix on mainstream desktop operating systems. Unfortunately, audio processing is always handled with multiple threads on said operating systems. I've been using Rust for several years, and one motivitations was a multithreaded audio project I was working on. In C++, subtle multithreading bugs are the norm, and soRust's goal to prevent improper data access when using multiple threads was very appealing. While it does this quite well, it does not offer many of the guarantees needed for realtime safe audio processing.
For example, while Rust makes it easy to safely use data on multiple threads, none of its commonly used abstractions for sharing data across threads are suitable for sharing large amount of data that is used by more than one thread, in a way where the writer is given realtime safe gurantees. If you want to send data from the audio thread to a different thread, this is a problem. Whilst atomics ensure (b)lock free data sharing (when supported by the underlying hardware), they are only implemented for smaller data, like individual numbers. Locks are not suitable if trying to get the lock causes the thread to block, or if data must be written to ensure correct operation (which would require taking the lock, which can block). This is only a concern when using safe Rust, which forces the use of locks on data that is accessible on multiple threads. Other systems languages, for example C or C++, would not, although without some synchronization method this is technically incorrect.
The last major kind of synchronization method in Rust are channels. Channels work by sending data from one thread to another, making data acessible to one thread at a time and thus removing the need for any synchronization on the programmer's end. Whilst the interface is great and the ownership model matches up with the needs of sending data from the audio thread to readers, it is unsuitable for realtime audio. There are different reasons for this depending on the channel- unbounded channels will allocate when their internal buffer is full. This happens when data is sent but not read. In the same scenario, a bounded channel will block the sender if its internal buffer is full. Neither of these are acceptable when sending data from the audio thread. If receiving data on the audio thread then channels are fine. Reads can be non-blocking, and only non-audio threads can be blocked when sending.
{% Title "A Practical Application" %} Unfortantely, when developing an fft, I found myself needing to send data from the audio thread to the UI. The output of an fft is a vector of floats, and I wanted to send every output vector to the rendering thread, without ever blocking or allocating on the audio thread. Critically, with the fft and several other audio visualizers I've made, I've always wanted to render only the most recent data. Anything older than that does not need to be visualized.
With those requirements in mind, I decided to make a triple buffering system for sending data. One buffer will always be reserved for reading. Reading and writing cannot happen on the same buffer, otherwise the reader will not read the correct data- it will be a random combination of the data at the start of the read and the data being written into the buffer in question. The other two can be swapped between for writing. When there are no reads happening, the writer cycles through all three buffers, with the reader one buffer behind. When there is reading, the writer cycles between the remaining two buffers, never blocked. There are some concerns with how current the reader is using this method- especially if there are multiple readers. However, in my situation there is only one reader- the worst case scenairo is that the reader reads data that is 2 buffers old. While this is not perfect, it's also not the end of the world, assuming an app running at a constant 60fps.
One more complication is specific to the way I like to design ffts and meters. I'm a fan of the dorrough meter, which is a digital meter for measuring audio signals. It shows the maximum sample value received between each meter update, which gives the operator a clear picture of the actual peak of the signal over time. In that way, I wanted each bin to show its maximum (as opposed to most recent) value since the last read. That requires updating the bins on every read to take the maximum value of the existing and new bins. More difficult is resetting the bins- once the maximum has been seen, I want to see the maximum for the next period. Making a mechanism that supported this this while reads and writes are in flight proved to be the most difficult part of the project.