AudioTransportSource::stop() causes a long pause

When trying to stop playing a sample in AudioTransportSource using stop(), the plugin pauses for a second and the audio artifact in the form of a click or something similar after the plugin continues working. I have seen many forum posts about this issue and none of the answers helped me. The problem disappears if you delete the lines in the AudioTransportSource::stop() method (juce_AudioTransportSource.cpp):

while (--n >= 0 && ! stopped)
            Thread::sleep (2);

Why is the thread blocking? Am I calling stop() from the wrong place?

Initializing everything here:

void NewProjectAudioProcessor::Init()
{
    formatManager.registerBasicFormats();

    transportSource.addChangeListener(this);
    audioReader = formatManager.createReaderFor(juce::File(juce::String("Samples/test1.wav"))); //in DAW .exe folder or plugin .vst3 folder

    if (audioReader != nullptr)
    {
        juce::AudioFormatReaderSource* newSource = new juce::AudioFormatReaderSource(audioReader, true);
        transportSource.setSource(newSource, 0, nullptr, audioReader->sampleRate);
        transportSource.setGain(1.0f);
    }
}

The rest is here:


void NewProjectAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    transportSource.prepareToPlay(samplesPerBlock, sampleRate);
}

void NewProjectAudioProcessor::releaseResources()
{
    transportSource.releaseResources();
}

//=====

void NewProjectAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    transportSource.getNextAudioBlock(juce::AudioSourceChannelInfo(buffer));
	
    for (const auto metadata : midiMessages)
    {
        auto message = metadata.getMessage();

        if (message.isNoteOn())
        {
            PlaySound(&transportSource);
        }
        else if(transportSource.isPlaying())
        {
            StopSound(&transportSource);
        }
    }
	
}

void NewProjectAudioProcessor::PlaySound(AudioTransportSource* ts)
{
    ts->setPosition(0.0);
    ts->start();
}

void NewProjectAudioProcessor::StopSound(AudioTransportSource* ts)
{
    ts->stop();
}

Full code:


#pragma once

#include <JuceHeader.h>

using namespace juce;

//==============================================================================
/**
*/
class NewProjectAudioProcessor  : public AudioProcessor,
                                    public juce::ChangeListener
{
public:
    //==============================================================================
    NewProjectAudioProcessor();
    ~NewProjectAudioProcessor() override;

    //==============================================================================
    void prepareToPlay (double sampleRate, int samplesPerBlock) override;
    void releaseResources() override;

   #ifndef JucePlugin_PreferredChannelConfigurations
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
   #endif

    void processBlock (AudioBuffer<float>&, MidiBuffer&) override;

    //==============================================================================
    AudioProcessorEditor* createEditor() override;
    bool hasEditor() const override;

    //==============================================================================
    const String getName() const override;

    bool acceptsMidi() const override;
    bool producesMidi() const override;
    bool isMidiEffect() const override;
    double getTailLengthSeconds() const override;

    //==============================================================================
    int getNumPrograms() override;
    int getCurrentProgram() override;
    void setCurrentProgram (int index) override;
    const String getProgramName (int index) override;
    void changeProgramName (int index, const String& newName) override;

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override;
    void setStateInformation (const void* data, int sizeInBytes) override;

    void Init();
    void LoadSample(int note, String samplePath);
    void changeListenerCallback(juce::ChangeBroadcaster* source) override;

    AudioFormatReader* audioReader;
    AudioFormatManager formatManager;
    AudioTransportSource transportSource;

    void PlaySound(AudioTransportSource* ts);
    void StopSound(AudioTransportSource* ts);

    std::unique_ptr<FileChooser> chooser;
    void openButtonClicked();


private:
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NewProjectAudioProcessor)
};


#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
NewProjectAudioProcessor::NewProjectAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", AudioChannelSet::stereo(), true)
                     #endif
                       )
#endif
{
    Init();
}

void NewProjectAudioProcessor::Init()
{
    formatManager.registerBasicFormats();

    transportSource.addChangeListener(this);
    audioReader = formatManager.createReaderFor(juce::File(juce::String("Samples/test1.wav"))); //in DAW .exe folder or plugin .vst3 folder

    if (audioReader != nullptr)
    {
        juce::AudioFormatReaderSource* newSource = new juce::AudioFormatReaderSource(audioReader, true);
        transportSource.setSource(newSource, 0, nullptr, audioReader->sampleRate);
        transportSource.setGain(1.0f);
    }
}

void NewProjectAudioProcessor::openButtonClicked()
{
    DBG("openButtonClicked");
    chooser = std::make_unique<FileChooser>("Select a .txt file to load...", File{}, "*.txt");
    auto chooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles;

    chooser->launchAsync(chooserFlags, [this](const FileChooser& fc)
        {
            auto file = fc.getResult();

            if (file != File{} && file.existsAsFile())
            {
                FileInputStream inputStream(file);
                String noteSection("note = ");

                if (inputStream.openedOk())
                {
                    while (!inputStream.isExhausted())
                    {
                        String line = inputStream.readNextLine();

                        if (line.startsWith(noteSection))
                        {
                            DBG(line);
                            String currentNote = line.removeCharacters(noteSection);
                            DBG(currentNote);
                        }
                        else
                        {
                        }
                    }
                }
                //String text = file.loadFileAsString();
                //DBG(text);
            }
    });
}

void NewProjectAudioProcessor::LoadSample(int note, String samplePath)
{

}

NewProjectAudioProcessor::~NewProjectAudioProcessor()
{
}

//==============================================================================
const String NewProjectAudioProcessor::getName() const
{
    return JucePlugin_Name;
}

bool NewProjectAudioProcessor::acceptsMidi() const
{
   #if JucePlugin_WantsMidiInput
    return true;
   #else
    return false;
   #endif
}

bool NewProjectAudioProcessor::producesMidi() const
{
   #if JucePlugin_ProducesMidiOutput
    return true;
   #else
    return false;
   #endif
}

bool NewProjectAudioProcessor::isMidiEffect() const
{
   #if JucePlugin_IsMidiEffect
    return true;
   #else
    return false;
   #endif
}

double NewProjectAudioProcessor::getTailLengthSeconds() const
{
    return 0.0;
}

int NewProjectAudioProcessor::getNumPrograms()
{
    return 1;
}

int NewProjectAudioProcessor::getCurrentProgram()
{
    return 0;
}

void NewProjectAudioProcessor::setCurrentProgram (int index)
{
}

const String NewProjectAudioProcessor::getProgramName (int index)
{
    return {};
}

void NewProjectAudioProcessor::changeProgramName (int index, const String& newName)
{
}

//==============================================================================
void NewProjectAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    transportSource.prepareToPlay(samplesPerBlock, sampleRate);
}

void NewProjectAudioProcessor::releaseResources()
{
    transportSource.releaseResources();
}

#ifndef JucePlugin_PreferredChannelConfigurations
bool NewProjectAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
  #if JucePlugin_IsMidiEffect
    juce::ignoreUnused (layouts);
    return true;
  #else
    if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
     && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
        return false;

   #if ! JucePlugin_IsSynth
    if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
        return false;
   #endif

    return true;
  #endif
}
#endif

void NewProjectAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    for (const auto metadata : midiMessages)
    {
        auto message = metadata.getMessage();

        if (message.isNoteOn())
        {
            PlaySound(&transportSource);
        }
        else if(transportSource.isPlaying())
        {
            StopSound(&transportSource);
        }
    }
    transportSource.getNextAudioBlock(juce::AudioSourceChannelInfo(buffer));
}

void NewProjectAudioProcessor::PlaySound(AudioTransportSource* ts)
{
    ts->setPosition(0.0);
    ts->start();
}

void NewProjectAudioProcessor::StopSound(AudioTransportSource* ts)
{
    ts->stop();
}

//==============================================================================
bool NewProjectAudioProcessor::hasEditor() const
{
    return true;
}

AudioProcessorEditor* NewProjectAudioProcessor::createEditor()
{
    return new NewProjectAudioProcessorEditor (*this);
}

//==============================================================================
void NewProjectAudioProcessor::getStateInformation (MemoryBlock& destData)
{
}

void NewProjectAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
}

void NewProjectAudioProcessor::changeListenerCallback(juce::ChangeBroadcaster* source)
{
}
//==============================================================================
// This creates new instances of the plugin..
AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
    return new NewProjectAudioProcessor();
}

I’ve encountered this before. It is working as designed, although I think the 1-sec pause is a bit arbitrary and excessive.

My guess is the thread sleep is to let the currently playing audio complete and the getNextAudioBlock to apply the buffer->applyGainRamp to avoid a click before bubbling the message up

I fixed this by assuming the minimum amount of sleep needed to come to a graceful stop was 2 blocks, so we just made the number of sleep iterations the length of a block in milliseconds (rounded up)

int n = (int)((1000/samplerate*blocksize)+1);     // iterate with a roundup of the blocksize in ms
       while (--n >= 0 && ! stopped)
           Thread::sleep (2);

I keep repeating myself: An AudioTransportSource is working asynchronous and has no place in a plugin. A plugin needs to work independently from any other timing, e.g. to render in hyper speed in offline bounce.

That means you have to expect it to be not working synchronous to the audio thread.