Concurrency: circular FIFO (good?)


#1

Hi,

I’m still learning stuff about lock free/atomic programming and trying to wrap my head around it. I tried to implement a circular FIFO in JUCE.

I would like to use this for a network thread that recieves packets, tries to decode them and then pushes them to the realtime threads (audio, midi…). In this use case I only need to use the newest packets (and maybe a few before that), and this as fast as possible (so I don’t need to know each and every packet recieved). One other thing is I need to get this packet as fast as possible to the realtime threads.

Is this the right way to do it or are there more efficient ways to do this? Is my code legit or what could/should be better/safer/quicker.

Thank you!


#2

Hi alfaleader,

you must not simply use the last packeges, because at the point where you append buffers that don’t belong after each other, you create jumps in the signal, which will result in ugly noise and high frequency artefacts. If you really want to allow dropouts of buffers, you must crossfade between the buffers.

Try to use bigger buffers per packet, so you get at least some coherent signal between the jumps. But better buffer a little more.

To your code: what did you want to use as template type, single samples? In this case you have 32 samples, so the lowest frequency you can render is 48000 samples/sec / 32 samples = 1500 Hz. Doesn’t sound very usable.

There is already a fifo class: AbstractFifo
So I would rather use that one. You only need to add the code for filling and reading. Try to use AudioBuffer::copyFrom() as often as possible, that will reduce CPU load compared to iterating.

Good luck


#3

Hi,

Maybe some more explenation: There is only one single thread that adds objects to these buffers. But there are multiple reader threads that get their information from this FIFO. The network thread (so the writer) recieves BPM beat taps (with some extra information, some strings, some ints, some floats…) and these get added in this buffer. With this bpm information I calculate Midi clock and SMPTE. It is perfectly possible that there is no coherent signal between the following incoming packets (there might be a beat “jump”), the midi clock sender takes care of this.

I also add a timestamp in the packets added to the FIFO, so even when we don’t recieve new BPM info over the network the midi clock can keep going.

I only use the last packet recieved EXCEPT when the bpm of the packet before the last packet has the same bpm, so I use them both to get my timestamps as accurate as possible (it’s possible that one packet has a bit higher delay on the network then the last).

(I used the abstractFIFO class as a base to build this class, but not sure I did everything right. Or maybe there are other ways in JUCE to do this more efficient :wink: )


#4

Sorry, I overread that you are sending midi in the packets. So only you can estimate if the synthesis works with lacking information.
But for audio you have to consider, a signal containing a frequncy of 200 Hz must be at least 48000 samples/sec / 200 1/sec = 240 samples. And musically if you cut off everything below 200 Hz is a high loss.

The AbstractFifo is not the codesample on the page but an actual class you can aggregate.
The main feature is to call prepareToRead and prepareToWrite, which calculates the proper indices where to write to and where to read from the buffer. And it’s abstract, because the actual memory is up to you.

To have multiple readers from the fifo will be tricky. You need to figure you, which parts run synchronous. Or if nothing else helps, you copy into several fifos one for each reader.

I copy a complete AudioBufferFifo class I use very often, maybe it’s usefull:

template<typename FloatType>
class AudioBufferFIFO
{
public:
    AudioBufferFIFO (int channels, int buffersize) :
        fifo (buffersize)
    {
        buffer.setSize (channels, buffersize);
    }

    void addToFifo (const FloatType** samples, int numSamples)
    {
        jassert (getFreeSpace() > numSamples);
        int start1, size1, start2, size2;
        fifo.prepareToWrite (numSamples, start1, size1, start2, size2);
        if (size1 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                buffer.copyFrom (channel, start1, samples[channel], size1);
        if (size2 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                buffer.copyFrom (channel, start2, samples[channel] + size1, size2);
        fifo.finishedWrite (size1 + size2);
    }

    void addToFifo (const juce::AudioBuffer<FloatType>& samples, int numSamples=-1)
    {
        const int addSamples = numSamples < 0 ? samples.getNumSamples() : numSamples;
        jassert (getFreeSpace() > addSamples);

        int start1, size1, start2, size2;
        fifo.prepareToWrite (addSamples, start1, size1, start2, size2);
        if (size1 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                buffer.copyFrom (channel, start1, samples.getReadPointer (channel), size1);
        if (size2 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                buffer.copyFrom (channel, start2, samples.getReadPointer (channel, size1), size2);
        fifo.finishedWrite (size1 + size2);

    }

    void readFromFifo (FloatType** samples, int numSamples)
    {
        jassert (getNumReady() > numSamples);
        int start1, size1, start2, size2;
        fifo.prepareToRead (numSamples, start1, size1, start2, size2);
        if (size1 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                juce::FloatVectorOperations::copy (samples [channel],
                                                   buffer.getReadPointer (channel, start1),
                                                   size1);
        if (size2 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                juce::FloatVectorOperations::copy (samples [channel] + size1,
                                                   buffer.getReadPointer (channel, start2),
                                                   size2);
        fifo.finishedRead (size1 + size2);
    }

    void readFromFifo (juce::AudioBuffer<FloatType>& samples, int numSamples=-1)
    {
        const int readSamples = numSamples > 0 ? numSamples : samples.getNumSamples();
        jassert (getNumReady() >= readSamples);

        int start1, size1, start2, size2;
        fifo.prepareToRead (readSamples, start1, size1, start2, size2);
        if (size1 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                samples.copyFrom (channel, 0, buffer.getReadPointer (channel, start1), size1);
        if (size2 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                samples.copyFrom (channel, size1, buffer.getReadPointer (channel, start2), size2);
        fifo.finishedRead (size1 + size2);
    }

    void readFromFifo (const juce::AudioSourceChannelInfo& info, int numSamples=-1)
    {
        const int readSamples = numSamples > 0 ? numSamples : info.numSamples;
        jassert (getNumReady() >= readSamples);

        int start1, size1, start2, size2;
        fifo.prepareToRead (readSamples, start1, size1, start2, size2);
        if (size1 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                info.buffer->copyFrom (channel, info.startSample, buffer.getReadPointer (channel, start1), size1);
        if (size2 > 0)
            for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
                info.buffer->copyFrom (channel, info.startSample + size1, buffer.getReadPointer (channel, start2), size2);
        fifo.finishedRead (size1 + size2);
    }

    int getNumChannels () const {
        return buffer.getNumChannels();
    }

private:
    AbstractFIFO fifo;

    /*< The actual audio buffer */
    juce::AudioBuffer<FloatType>    buffer;
};

#5

Thanks for the code, the internals should work quite similar but my version is more focused on my user case and slimmed down.

I don’t think multiple readers is a problem, as long as they only read it and don’t write (maybe putting const at some places might help to insure that). The only assumption I make is that a read won’t take longer then 32 writes (I assume this is pretty safe to say, or isn’t it?)

And btw maybe FIFO isn’t a good word for this kind of structure.


#6

IMHO the crucial point is, if the readers read at the same speed. Imagine one reader gets stuck reading and the writer wants to write. One reader will block the writing and the other readers keep waiting for data…


#7

If a reader catches up with the writer, wouldn’t the reader then just get faulty data? But the writer would still keep writing valid data?

And if a thread gets stuck for 32 “beats”/write operations (lets say 240bpm in an extreme case so 8s, doesn’t that mean there is something seriously wrong in this thread and a wrong read is the least of my problems?)


#8

My thought goes the other way round, you cannot write before ALL readers finished reading.
But again, my thoughts were about sample data, it might be completely different for your project where you buffer some completely different kind of data.
So I better stop speculating :wink:
Good luck


#9

Hi alfaleader & Daniel,

I am a beginner in JUCE and VST plugin development. Hence kindly please forgive me if my questions are naive and not coherent.

current task: Porting 3 party dsp modules to VST plugin which has dependency on block size parameter. Further since the VST hosts such as VstHost by Hermann allows samplesPerBlock that is restricted not to the power of 2^N, Its not wise to look into resampling as an option.

So, in order to make the Plugin Independent of the samplesPerBlock in prepareToPlay, I am intended to use 2 ring buffers at the beginning and at the end of processBlock to match the process rate to the I/O rate of the vst host.

currently I have added my own simple ring buffer class to audio thread in processBlock. But issue is that, I have artifacts in the output audio. As I have unit tested the “simple ring buffer” with the test sequences, I could assure its not due to contiguity mismatch in the ring Buffer.

Hence I would like to clarify if its due to dropping of samples from concurrency aspect. Is it worth looking towards, since I currently focused only on Audio thread. Please help me to orient towards right direction. Thanks a lot in advance,.

Regards
desbha


#10

I think you should never drop any samples, that has to lead to audible artefacts.

If I understand your problem correctly, the solution is to add an artificial latency of one block size (the block size your algorithm needs).
Imagine worst case, you get each block only one sample (unlikely, but makes it understandable), in that case you still have enough processed samples to return a continuous audio.

Lets say your algorithm needs 1024 samples per block:

  • have two FIFO buffers, one input FIFO for the unprocessed input signal, one output FIFO for processed samples
  • In prepareToPlay: feed 1024 samples silence to the input FIFO

Processing:

  • add the input samples to the input FIFO
  • while (samples in in input FIFO >= 1024) pull and process 1024 samples to output FIFO
  • return from output FIFO the amount of samples you got in

And override the AudioProcessor::getLatencySamples() to return the number (in this example 1024)

Actually you already mentioned that approach, so the only thing you might need to change is to add the latency by feeding silence before…

Hope that helps…


#11

yes Daniel,
Thank you very much for the same. This really helps to save a lot of debug time. Thank you very much for the quick response. I could try this immediately and get back in case.

BR…