The issue is that we want to pass data from the audio thread to the UI thread and we’re using a shared array for this, fftData. We don’t want the audio thread to write into this array while the UI thread is reading from it. So we need to protect it somehow.
The audio thread does the following:
void pushNextSampleIntoFifo (float sample) noexcept
{
if (fifoIndex == fftSize)
{
if (!nextFFTBlockReady)
{
/* copy the data into the fftData shared array */
nextFFTBlockReady = true;
}
fifoIndex = 0;
}
fifo[fifoIndex++] = sample;
}
Suppose fftSize is 1024. So once every 1024 samples this tries to copy the data from fifo into the fftData array.
The only time the audio thread will change the fftData array is when nextFFTBlockReady is false. Initially this will be the case. Once the data is copied into fftData, we set nextFFTBlockReady to true.
Let’s say the UI thread does not read from fftData for a while. In the mean time, we process another 1024 samples and now if (!nextFFTBlockReady) will be false, since nextFFTBlockReady is still true from last time. So the audio thread throws away these most recent 1024 samples and will start collecting the next set of 1024 samples from scratch.
This is why this method isn’t optimal: if the UI thread doesn’t read fast enough, the audio thread cannot update fftData with the latest values. In other words, we may end up displaying stale data once the UI gets around to redrawing itself.
You can think of it as follows:
- when
nextFFTBlockReady is false, the audio thread is in charge of fftData.
- when it is true, the UI thread is in charge of
fftData.
The UI thread runs a timer that fires every X times per second and does:
if (nextFFTBlockReady)
{
drawNextFrameOfSpectrum();
nextFFTBlockReady = false;
repaint();
}
Initially this doesn’t do anything because nextFFTBlockReady is false, meaning that only the audio thread can access fftData.
However, after the audio thread has written into fftData, nextFFTBlockReady will be true. That means the UI thread is allowed to use fftData (and only the UI thread).
In drawNextFrameOfSpectrum() it copies whatever it needs from fftData into its own variables. Then it sets nextFFTBlockReady to false again, to “pass the baton” back to the audio thread. From now on, the UI thread is no longer allowed to access fftData.
And so on it goes, back-and-forth.
To summarize, this is thread-safe because the value of nextFFTBlockReady determines which thread is allowed to access fftData.
Now, there is an obvious problem. The compiler is allowed to re-order statements if they do not change the meaning of the code, so the UI thread might end up doing the following:
if (nextFFTBlockReady)
{
nextFFTBlockReady = false;
drawNextFrameOfSpectrum();
repaint();
}
This is wrong! It sets nextFFTBlockReady to false, passing ownership back to the audio thread, before reading fftData.
In the code from the original tutorial, this could theoretically happen (depending on the compiler settings etc), and now the whole thing is no longer thread-safe.
By making nextFFTBlockReady a std::atomic, the compiler will insert memory barriers to prevent this kind of reordering from going on. So the logic as discussed above works fine, and the compiler won’t do any funny stuff that breaks this logic when you’re not looking.