Audio IO Conundrum


#1

Hello!

A little background: I'm currently prototyping a fairly complex plugin, and I'm finding it easiest to test stuff out as a standalone app. However, the eventual goal is to build it as a plugin. 

I want to be able to do the following things with respect to audio IO, all from one app/plugin interface:

- Record a new audio file/load an audio file and play it back (separately from the DAW playback)
- Process recorded/loaded audio files offline (on a separate thread)
- Process live input

 

I used the JUCE examples to make a class which can handle the recording/loading/playback, so the current prototype can do the first two tasks just fine. The problem is that in order to accomplish that I had to add my audioRecorderPlayer class's audio callback via the AudioAppComponent's deviceManager. Doing so seems to kill the AudioAppComponent's normal audio callback (it only executes once), so I'm stuck being unable to accomplish the third IO task (processing live input). I'm trying to figure out if I'm doing something wrong wrt the deviceManager or if there's a fundamental problem with how I've structured my code.

 

Those IO tasks are tied to separate, mutually exclusive modes of operation for my app/plugin. My thought was that when I need to do the recording/playback I could just add the corresponding callback and then remove it when I don't need it. This doesn't really seem to work though -- I assume I'm misunderstanding something about how JUCE works in that regard. I would really appreciate any guidance on this!!

 

Here is my audioRecorderPlayer class:
 

audioRecorderPlayer.h

#ifndef AUDIORECORDERPLAYER_H_INCLUDED
#define AUDIORECORDERPLAYER_H_INCLUDED
#include "JuceHeader.h"
class AudioRecorderPlayer  : public AudioSourcePlayer, public ActionBroadcaster,
                             public ChangeBroadcaster, public ChangeListener
{
public:
    
    AudioRecorderPlayer (AudioFormatManager& audioFormatManager);
    
    ~AudioRecorderPlayer();
    
    TimeSliceThread* getBackgroundThread();
    
    void setSource(AudioSource* newSource);
    
    void setThumbnail(AudioThumbnail* newThumbnail);
    
    void startPlaying();
    
    void stopPlaying();
    
    void setFollowsTransport (bool shouldFollow);
    
    bool getFollowsTransport() const;
    
    bool canMoveTransport() const noexcept;
    
    void setPosition (double newPosition);// in seconds
    
    double getCurrentPosition() const;// in seconds
    double getSamplingRate();
    
    double getFileSamplingRate();
    
    bool getFileSaveState();
    
    const String& getCurrentFilePath() const noexcept;
    
    double getLengthInSeconds() const;
    
    void setNextReadPosition (int64 newPosition);// in samples
    
    int64 getNextReadPosition() const;// in samples
    
    int64 getLengthInSamples() const;//in samples
    
    bool isLooping() const;
    
    /** Returns the source that's playing.
     May return nullptr if there's no source.
     */
    AudioSource* getCurrentSource() const noexcept;
    
    void setGain (float newGain) noexcept;
    
    float getGain() const noexcept;
    
    void prepareToPlay (int newBufferSize, double newSampleRate);
    
    
    void audioDeviceIOCallback (const float** inputChannelData,
                                int totalNumInputChannels,
                                float** outputChannelData,
                                int totalNumOutputChannels,
                                int numSamples) override;
    
    void audioDeviceAboutToStart (AudioIODevice* device) override;
    void audioDeviceStopped() override;
    
    void startRecording();
    
    void stopRecording();
    
    bool isRecording() const;
    
    bool isPlaying() const;
    
    bool loadFile(const bool loadNewlyRecordedFile);
    
    bool saveFile();
    
    void reset();
    
private:
    //==============================================================================
    double sampleRate, fileSampleRate;
    CriticalSection callbackLock;
    AudioThumbnail* thumbnail;
    
    TimeSliceThread backgroundThread; // the thread that will write our audio data to disk
    ScopedPointer<AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data
    int64 nextSampleNum;
    
    const String tempFileName;
    String currentFilePath;
    bool currentFileHasBeenSaved;
    AudioFormatManager& formatManager;
    AudioFormatWriter::ThreadedWriter* volatile activeWriter;
    
    int bufferSize;
    float* channels [128];
    float* outputChans [128];
    const float* inputChans [128];
    AudioSampleBuffer tempBuffer;
    float volatile lastGain, gain;
    
    AudioSource* audioSource;
    AudioTransportSource transport;
    ScopedPointer<AudioFormatReaderSource> currentAudioFileSource;
    bool isFollowingTransport;
    
    void changeListenerCallback(ChangeBroadcaster* broadcastSource) override;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioRecorderPlayer)
};

#endif  // AUDIORECORDERPLAYER_H_INCLUDED

 

audioRecorderPlayer.cpp

#include "audioRecorderPlayer.h"
//=================================================================================
// AudioRecorderPlayer ============================================================
//=================================================================================
// Public =========================================================================
AudioRecorderPlayer::AudioRecorderPlayer(AudioFormatManager& audioFormatManager)
:
sampleRate(0),
fileSampleRate(0.0),
thumbnail(nullptr),
backgroundThread("AudioRecorderPlayer Thread"),
nextSampleNum(0),
tempFileName("Audio_Clip.wav"),
currentFilePath(""),
currentFileHasBeenSaved(false),
formatManager(audioFormatManager),
activeWriter(nullptr),
bufferSize(0),
lastGain(1.0f),
gain(1.0f),
audioSource(nullptr),
currentAudioFileSource(nullptr)
{
    File tempFile (File::getSpecialLocation (File::tempDirectory)
                   .createTempFile(tempFileName));
    transport.addChangeListener(this);
    reset();
    audioSource= &transport;
    backgroundThread.startThread(3);
}
AudioRecorderPlayer::~AudioRecorderPlayer()
{
    if(isRecording())
    {
        stopRecording();
    }
    if(isPlaying())
    {
        stopPlaying();
    }
    reset();
    backgroundThread.stopThread(3);
    transport.removeChangeListener(this);
    
    threadedWriter = nullptr;
    currentAudioFileSource = nullptr;
    
    File tempFile (File::getSpecialLocation (File::tempDirectory)
                   .getChildFile(tempFileName));
    tempFile.deleteFile();
}
TimeSliceThread* AudioRecorderPlayer::getBackgroundThread()
{
    return &backgroundThread;
}
void AudioRecorderPlayer::setSource(AudioSource* newSource)
{
    if (audioSource!= newSource)
    {
        AudioSource* const oldSource = audioSource;
        
        if (newSource != nullptr && bufferSize > 0 && sampleRate > 0)
            newSource->prepareToPlay (bufferSize, sampleRate);
        
        {
            const ScopedLock sl (callbackLock);
            audioSource= newSource;
        }
        
        if (oldSource != nullptr)
            oldSource->releaseResources();
    }
}
void AudioRecorderPlayer::setThumbnail(AudioThumbnail* newThumbnail)
{
    thumbnail = newThumbnail;
}
void AudioRecorderPlayer::startPlaying()
{
    transport.start();
}
void AudioRecorderPlayer::stopPlaying()
{
    transport.stop();
}
void AudioRecorderPlayer::setFollowsTransport (bool shouldFollow)
{
    isFollowingTransport = shouldFollow;
}
bool AudioRecorderPlayer::getFollowsTransport() const
{
    return isFollowingTransport;
}
bool AudioRecorderPlayer::canMoveTransport() const noexcept
{
    return ! (isFollowingTransport && isPlaying());
}
void AudioRecorderPlayer::setPosition (double newPosition)// in seconds
{
    transport.setPosition(newPosition);
}
double AudioRecorderPlayer::getCurrentPosition() const// in seconds
{
    return transport.getCurrentPosition();
}
double AudioRecorderPlayer::getLengthInSeconds() const
{
    return transport.getLengthInSeconds();
}
double AudioRecorderPlayer::getSamplingRate()
{
    return sampleRate;
}
double AudioRecorderPlayer::getFileSamplingRate()
{
    return fileSampleRate;
}
bool AudioRecorderPlayer::getFileSaveState()
{
    return currentFileHasBeenSaved;
}
const String& AudioRecorderPlayer::getCurrentFilePath() const noexcept
{
    return currentFilePath;
}
void AudioRecorderPlayer::setNextReadPosition (int64 newPosition)// in samples
{
    transport.setPosition(newPosition);
}
int64 AudioRecorderPlayer::getNextReadPosition() const// in samples
{
    return transport.getNextReadPosition();
}
int64 AudioRecorderPlayer::getLengthInSamples() const//in samples
{
    return transport.getTotalLength();
}
bool AudioRecorderPlayer::isLooping() const
{
    return transport.isLooping();
}
/** Returns the source that's playing.
 May return nullptr if there's no source.
 */
AudioSource* AudioRecorderPlayer::getCurrentSource() const noexcept
{
    return currentAudioFileSource;
}
void AudioRecorderPlayer::setGain (float newGain) noexcept
{
    gain = newGain;
}
float AudioRecorderPlayer::getGain() const noexcept
{
    return gain;
}
void AudioRecorderPlayer::prepareToPlay (int newBufferSize, double newSampleRate)
{
    sampleRate = newSampleRate;
    bufferSize = newBufferSize;
    zeromem (channels, sizeof (channels));
    
    if(audioSource != nullptr)
    {
        audioSource->prepareToPlay(bufferSize, sampleRate);
    }
}

void AudioRecorderPlayer::audioDeviceIOCallback (const float** inputChannelData,
                            int totalNumInputChannels,
                            float** outputChannelData,
                            int totalNumOutputChannels,
                            int numSamples)
{
    // these should have been prepared by audioDeviceAboutToStart()...
    jassert (sampleRate > 0 && bufferSize > 0);
    
    
    // Player
    if(isPlaying())
    {
        const ScopedLock sl (callbackLock);
        
        if (audioSource!= nullptr)
        {
            int numActiveChans = 0, numInputs = 0, numOutputs = 0;
            
            // messy stuff needed to compact the channels down into an array
            // of non-zero pointers..
            for (int i = 0; i < totalNumInputChannels; ++i)
            {
                if (inputChannelData[i] != nullptr)
                {
                    inputChans [numInputs++] = inputChannelData[i];
                    if (numInputs >= numElementsInArray(inputChans)){
                        break;
                    }
                }
            }
            
            for (int i = 0; i < totalNumOutputChannels; ++i)
            {
                if (outputChannelData[i] != nullptr)
                {
                    outputChans [numOutputs++] = outputChannelData[i];
                    if (numOutputs >= numElementsInArray (outputChans)){
                        break;
                    }
                }
            }
            
            if (numInputs > numOutputs)
            {
                // if there aren't enough output channels for the number of
                // inputs, we need to create some temporary extra ones (can't
                // use the input data in case it gets written to)
                tempBuffer.setSize (numInputs - numOutputs, numSamples, false, false, true);
                
                for (int i = 0; i < numOutputs; ++i)
                {
                    channels[numActiveChans] = outputChans[i];
                    memcpy (channels[numActiveChans], inputChans[i], sizeof (float) * (size_t) numSamples);
                    ++numActiveChans;
                }
                
                for (int i = numOutputs; i < numInputs; ++i)
                {
                    channels[numActiveChans] = tempBuffer.getWritePointer (i - numOutputs);
                    memcpy (channels[numActiveChans], inputChans[i], sizeof (float) * (size_t) numSamples);
                    ++numActiveChans;
                }
            }
            else
            {
                for (int i = 0; i < numInputs; ++i)
                {
                    channels[numActiveChans] = outputChans[i];
                    memcpy (channels[numActiveChans], inputChans[i], sizeof (float) * (size_t) numSamples);
                    ++numActiveChans;
                }
                
                for (int i = numInputs; i < numOutputs; ++i)
                {
                    channels[numActiveChans] = outputChans[i];
                    zeromem (channels[numActiveChans], sizeof (float) * (size_t) numSamples);
                    ++numActiveChans;
                }
            }
            
            AudioSampleBuffer buffer (channels, numActiveChans, numSamples);
            
            AudioSourceChannelInfo info (&buffer, 0, numSamples);
            audioSource->getNextAudioBlock (info);
            
            //apply gain
            for (int i = info.buffer->getNumChannels(); --i >= 0;){
                buffer.applyGainRamp (i, info.startSample, info.numSamples, lastGain, gain);
            }
            lastGain = gain;
        }
        else
        {
            stopPlaying();
        }
        return;
    }
    // Recorder
    else if(isRecording())
    {
        const ScopedLock sl (callbackLock);
        
        if (activeWriter != nullptr)
        {
            activeWriter->write (inputChannelData, numSamples);
            
            // Create an AudioSampleBuffer to wrap our incomming data, note that this does no allocations or copies, it simply references our input data
            const AudioSampleBuffer buffer (const_cast<float**> (inputChannelData), thumbnail->getNumChannels(), numSamples);
            thumbnail->addBlock (nextSampleNum, buffer, 0, numSamples);
            nextSampleNum += numSamples;
        }
    }
    // We need to clear the output buffers, in case they're full of junk..
    for (int i = 0; i < totalNumOutputChannels; ++i)
    {
        if (outputChannelData[i] != nullptr)
        {
            FloatVectorOperations::clear (outputChannelData[i], numSamples);
        }
    }
}
void AudioRecorderPlayer::audioDeviceAboutToStart (AudioIODevice* device)
{
    prepareToPlay(device->getCurrentBufferSizeSamples(), device->getCurrentSampleRate());
}
void AudioRecorderPlayer::audioDeviceStopped()
{
    if (audioSource!= nullptr){
        audioSource->releaseResources();
    }
    sampleRate = 0.0;
    bufferSize = 0;
    
    tempBuffer.setSize (2, 8);
}
void AudioRecorderPlayer::startRecording()
{
    if (sampleRate > 0)
    {
        File file = File::getSpecialLocation(File::tempDirectory).getChildFile(tempFileName);
        currentFilePath = file.getFullPathName();
        std::cout << "Recording to " + currentFilePath << std::endl;
        // Create an OutputStream to write to our destination file...
        file.deleteFile();
        ScopedPointer<FileOutputStream> fileStream (file.createOutputStream());
        if (fileStream != nullptr)
        {
            // Now create a WAV writer object that writes to our output stream...
            WavAudioFormat wavFormat;
            AudioFormatWriter* writer = wavFormat.createWriterFor (fileStream, sampleRate, 1, 16, StringPairArray(), 0);
            
            if (writer != nullptr)
            {
                fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
                
                // Now we'll create one of these helper objects which will act as a FIFO buffer, and will
                // write the data to disk on our background thread.
                threadedWriter = new AudioFormatWriter::ThreadedWriter (writer, backgroundThread, 32768);
                
                // Reset our recording thumbnail
                thumbnail->reset (writer->getNumChannels(), writer->getSampleRate());
                nextSampleNum = 0;
                
                // And now, swap over our active writer pointer so that the audio callback will start using it..
                const ScopedLock sl (callbackLock);
                activeWriter = threadedWriter;
            }
        }
    }
}
void AudioRecorderPlayer::stopRecording()
{
    currentFileHasBeenSaved = false;
    
    // First, clear this pointer to stop the audio callback from using our writer object..
    {
        const ScopedLock sl (callbackLock);
        activeWriter = nullptr;
    }
    
    // Now we can delete the writer object. It's done in this order because the deletion could
    // take a little time while remaining data gets flushed to disk, so it's best to avoid blocking
    // the audio callback while this happens.
    threadedWriter = nullptr;
    loadFile(true);
    currentFileHasBeenSaved = false;
}
bool AudioRecorderPlayer::isRecording() const
{
    return activeWriter != nullptr;
}
bool AudioRecorderPlayer::isPlaying() const
{
    return transport.isPlaying();
}
bool AudioRecorderPlayer::loadFile(const bool loadNewlyRecordedFile = false)
{
    File file;
    if(loadNewlyRecordedFile) // We just stopped recording
    {
        // TODO This should maybe be user temp folder instead..
        file = File(currentFilePath);
    }
    else
    {
        FileChooser fc("Choose a file to open...",
                        File::getSpecialLocation(File::SpecialLocationType::userDocumentsDirectory),
                        "*.wav",
                        true);
        fc.browseForMultipleFilesToOpen();
        file = fc.getResult();
        currentFilePath = file.getFullPathName();
        // unload the previous file source and delete it..
    }
    if (!file.isDirectory())
    {
        AudioFormatReader * reader = formatManager.createReaderFor (file);
        
        if (reader != nullptr)
        {
            // ..and plug it into our transport source
            const ScopedLock sl(callbackLock);
            ScopedPointer<AudioFormatReaderSource> newAudioFileSource = new AudioFormatReaderSource (reader, true);
            transport.setSource (newAudioFileSource,
                                 32768,                   // tells it to buffer this many samples ahead
                                 &backgroundThread,                 // this is the background thread to use for reading-ahead
                                 reader->sampleRate, reader->numChannels);     // allows for sample rate correction
            fileSampleRate = reader->sampleRate;
            currentAudioFileSource = newAudioFileSource;
            thumbnail->setSource (new FileInputSource (file));
            newAudioFileSource = nullptr;
            
            currentFileHasBeenSaved = false;
            sendActionMessage("Audio Available");
            return true;
        }
    }
    return false;
}
bool AudioRecorderPlayer::saveFile()
{
    if(!currentFileHasBeenSaved)
    {
        stopRecording();
        stopPlaying();
        
        
        //TODO create preference for directory for saving files
        //TODO save labeling data alongside them
        FileChooser fc ("Choose a location to save file...",
                        File::getSpecialLocation(File::SpecialLocationType::userDocumentsDirectory));
        fc.browseForFileToSave(true);
        const File& file = fc.getResult();
        if(!file.isDirectory())
        {
            bool success = false;
            
            file.deleteFile();
            ScopedPointer<FileOutputStream> fileStream(file.createOutputStream());
            if(fileStream != nullptr)
            {
                // Now create a WAV writer object that writes to our output stream...
                WavAudioFormat wavFormat;
                ScopedPointer<AudioFormatWriter> writer = wavFormat.createWriterFor(fileStream, sampleRate, 1, 16, StringPairArray(), 0);
                if (writer != nullptr)
                {
                    fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
                    currentAudioFileSource->setNextReadPosition(0);
                    success = writer->writeFromAudioSource(*currentAudioFileSource,
                                                           currentAudioFileSource->getTotalLength(),
                                                           4096);
                    writer = nullptr;
                }
            }
            
            if(success)
            {
                currentFilePath = file.getFullPathName();
                currentFileHasBeenSaved = true;
            }
            // TODO check if this is necessary
            else
            {
                currentFileHasBeenSaved = false;
            }
        }
    }
    return currentFileHasBeenSaved;
}
void AudioRecorderPlayer::reset()
{
    transport.setSource(nullptr);
    assert(transport.getTotalLength() == 0);
    sendActionMessage("No Audio Available");
}
// Private ========================================================================
void AudioRecorderPlayer::changeListenerCallback(ChangeBroadcaster * broadcastSource)
{
    if(broadcastSource == &transport)
    {
        if(transport.hasStreamFinished()){
            sendActionMessage("Stop Playing");
        }
    }
}

 

Cheers