Gaps of zeros in BufferingAudioSource's buffer


#1

If a BufferingAudioSource is reading from an audio source that cannot deliver enough data, e.g. because it gets data from from a slow network connection, parts of the buffer will be filled with silence, resulting in stuttering and fragmented audio when the buffer is played back.

The problem originates from the method ::readBufferSection, which is regularly called on a background thread to fill the buffer. It does not take into account the number of samples that are available in it’s source, which in turn causes BufferingAudioSource to contain half filled blocks of audio.

In juce_bufferingAudioSource.cpp:

void BufferingAudioSource::readBufferSection (const int64 start, const int length, const int bufferOffset)
{
    if (source->getNextReadPosition() != start)
        source->setNextReadPosition (start);

    AudioSourceChannelInfo info (&buffer, bufferOffset, length);
    source->getNextAudioBlock (info);
}

If source, or a source later in the chain, cannot fill the entire buffer given by info with audio, the rest of the samples will be set to zero. This may originate all the way from the beginning of the chain of sources, which in my setup is a WebInputStream that is read by a CoreAudioReader:

WebInputStream -> CoreAudioReader -> AudioFormatReader -> AudioFormatReaderSource -> BufferingAudioSource

If the WebInputStream does not deliver enough samples because of a halting http connection, then CoreAudioReader will clear the rest.

In juce_coreAudioFormat.cpp:

bool readSamples (int** destSamples, int numDestChannels, int startOffsetInDestBuffer,
                  int64 startSampleInFile, int numSamples) override
{
    clearSamplesBeyondAvailableLength (destSamples, numDestChannels, startOffsetInDestBuffer,
                                       startSampleInFile, numSamples, lengthInSamples);

Later in the same method, bytes are read from the input stream, by:

        UInt32 numFramesToRead = (UInt32) numThisTime;
        OSStatus status = ExtAudioFileRead (audioFileRef, &numFramesToRead, bufferList);

Here, ExtAudioFileRead upon return sets the variable numFramesToRead to the number of bytes that were available, but this information is not utilized at a later stage.

To provide enough samples for the audio device output, clearing may be necessary in the top level source, but for intermediate sources it causes a problem. The number of samples that are actually read should propagate back through the chain.

What would you recommend as best approach to overcome the gaps in the resulting buffer?


#2

I don’t understand the issue… The entire point of BufferingAudioStream is to decouple a realtime player from its possibly slow input sources.

If you want the entire reader to block until it definitely delivers all the samples, then just don’t use a BufferingAudioStream! Obviously that’s no use if you’re sending it to a realtime output, but you can’t have it both ways.


#3

Yes, but buffering is still very useful even if it appends chunks of variable size containing received audio from its source to the end of the buffer. A status value could be returned from getNextAudioBlock telling whether the read position has reached the buffered amount. This way, real time audio could seamlessly be played without gaps by using a long buffer, even if there are hiccups in the http connection. This is an ideal use of a buffer in my opinion.

As it is now, there is also a risk that the position from getNextReadPosition could go beyond getTotalLength, which could cause some inconsistency.