AudioTransportSource causes MemoryMappedAudioFormatReader to read beyond its range?

Hello, I want to use MemoryMappedAudioFormatReader with AudioTransportSource for a multi-track audio playback application. (In my experiments, using AudioFormatManager::createReaderFor(file) on multiple tracks may randomly lag and is unacceptable.)

The following code is adapted from the Projucer demo AudioPlaybackDemo.h, for playing one audio file. Here I stripped out the UI and replaced the reader with a MemoryMappedAudioFormatReader, which will map the entire file.

class AudioPlayerComponent : public juce::AudioAppComponent
{
public:
    AudioPlayerComponent()
    {
        formatManager.registerBasicFormats();
        setAudioChannels(0, 2);
    }

    ~AudioPlayerComponent() override
    {
        shutdownAudio();
    }

    void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override
    {
        transportSource.prepareToPlay(samplesPerBlockExpected, sampleRate);
    }

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

        transportSource.getNextAudioBlock(bufferToFill);
    }

    void releaseResources() override
    {
        transportSource.releaseResources();
    }

    void resized() override
    {
    }

    void play()
    {
        transportSource.start();
    }

    void stop()
    {
        transportSource.stop();
    }

    void setPosition(double position)
    {
        transportSource.setPosition(position);
    }

    void openFile(juce::String& path)
    {
        juce::File file(path);

        if (file != juce::File{})
        {
            // auto* reader = formatManager.createReaderFor(file); // Don't use this AudioFormatReader

            juce::AudioFormat* audioFormat = formatManager.findFormatForFileExtension(file.getFileExtension());
            if (audioFormat == nullptr) return;

            auto* reader = audioFormat->createMemoryMappedReader(file);

            if (!reader->mapEntireFile())
            {
                throw "mapEntireFile() failed";
            }

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

private:
    juce::AudioFormatManager formatManager;
    std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
    juce::AudioTransportSource transportSource;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPlayerComponent)
};

I ran it as a console app to play a WAV file. I noticed that when the playhead reaches the end of the file, I would get the following assertion failure:

JUCE Assertion failure in juce_WavAudioFormat.cpp:1868

        if (map == nullptr || ! mappedSection.contains (Range<int64> (startSampleInFile, startSampleInFile + numSamples)))
        {
            jassertfalse; // you must make sure that the window contains all the samples you're going to attempt to read.

I added print statements and checked at that point, the requested mappedSection has range( 2930654, 2930173), an invalid range where start > end, whereas the reader created above has the valid range(0, 2930173).

What am I doing wrong? Please advise. Thank you for your time and consideration.

I found a solution after some trial and error. It seems I need to explicitly check range boundaries in getNextAudioBlock, making sure I don’t read beyond the memory-mapped range end. The following solution works (no more error), but I don’t think it is right way as I’m skipping the final block entirely instead of processing its partially valid range.

void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
{
    // If the next block to read (reader's next position + buffer size) will exceed the memory-mapped range end,
    // just abandon this last block entirely :(
    if (readerSource->getNextReadPosition() + bufferToFill.numSamples >= reader->getMappedSection().getEnd())
    {
        bufferToFill.clearActiveBufferRegion();
        return;
    }

    transportSource.getNextAudioBlock(bufferToFill);
}

Also, this seems like a bug in the implementation of AudioFormatReaderSource::getNextAudioBlock if it really is necessary to do this explicit range checking… Please correct me if there is a better way. Otherwise, I’m considering to make my own subclass from AudioFormatReaderSource where I override its getNextAudioBlock.

Thanks for reporting this issue. I’ve updated the AudioFormatReaderSource so that it will respect the length in samples of the AudioFormatReader that it wraps: