Bug: MemoryAudioSource does not properly support looping

This works for audio files because as we can see in AudioFormatReaderSource::getNextReadPosition() it properly wraps the next position if the source is set to loop:

int64 AudioFormatReaderSource::getNextReadPosition() const
{
    return looping ? nextPlayPos % reader->lengthInSamples
                   : nextPlayPos;
}

But MemoryAudioSource::getNextReadPosition() only returns the current position:

int64 MemoryAudioSource::getNextReadPosition() const
{
    return position;
}

Updating the source for MemoryAudioSource::getNextReadPosition() to the following fixes the issue:

int64 MemoryAudioSource::getNextReadPosition() const
{
    return isCurrentlyLooping ? position % getTotalLength() : position;
}

You can verify this by using the project Tutorial: Draw audio waveforms.

Modify the code in the open click handler to support looping:

auto* reader = formatManager.createReaderFor (file);

if (reader != nullptr)
{
    auto newSource = std::make_unique<juce::AudioFormatReaderSource> (reader, true);
    transportSource.setSource (newSource.get(), 0, nullptr, reader->sampleRate);
    playButton.setEnabled (true);
    thumbnailComp.setFile (file);
    readerSource.reset (newSource.release());

    // Add these lines to enable looping
    readerSource->setLooping(true);
    transportSource.setLooping(true);
}

If you build at this point and open an audio file, you will be able to both hear it loop as well as see the playhead properly wrap around in the SimplePositionOverlay component, which gets its position this way:

auto audioPosition = (float) transportSource.getCurrentPosition();

However, if you switch to an MemoryAudioSource as the AudioTransportSource’s source instead of a file reader, and set it all to looping, you will hear the audio loop, but the position will continue to increment and will never properly wrap, which can be seen by the playhead that never returns to the start.

Add these two members to the MainContentComponent class so we can read the file into memory first:

juce::AudioSampleBuffer memorySampleBuffer;
std::unique_ptr<juce::MemoryAudioSource> memoryAudioSource = nullptr;

Next, replace the file open handler slightly:

auto* reader = formatManager.createReaderFor (file);

if (reader != nullptr)
{
    auto newSource = std::make_unique<juce::AudioFormatReaderSource> (reader, true);
    playButton.setEnabled (true);
    thumbnailComp.setFile (file);
                    
    // Read the entire file into memory
    memorySampleBuffer.setSize(2, reader->lengthInSamples);
    reader->read(&memorySampleBuffer, 0, reader->lengthInSamples, 0, true, true);
    memoryAudioSource = std::make_unique<juce::MemoryAudioSource>(memorySampleBuffer, true, true);
    memoryAudioSource->setLooping(true);
    transportSource.setLooping(true);
    transportSource.setSource(memoryAudioSource.get(), 0, nullptr, reader->sampleRate);
}

Finally, update the MainContentComponent::getNextAudioBlock() method to look for a valid memoryAudioSource pointer instead of readerSource:

void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
    if (memoryAudioSource.get() == nullptr)
        bufferToFill.clearActiveBufferRegion();
    else
        transportSource.getNextAudioBlock (bufferToFill);
}

If you load a file now and play, you will hear it loop, but you will see the playhead go off screen. And you can verify this by debugging the position returned.

Once you’ve applied the patch above to MemoryAudioSource::getNextReadPosition() you will see the position correctly wrap, and the AudioTransportSource’s position will respect looping properly from a MemoryAudioSource.