Hello,
I want to implement a component with the following functionality in a separate audio callback from the main plugin/audio app one:
- Load an existing audio file
- Record a new file (and eventually save with a user-defined filename)
- Play the file (with transport controls)
- Display the waveform (with zoom, scolling)
I know there are JUCE demos which do all of this, I started by adapting them so that I could have each one running in my project. That worked fine with each one individually, but I need it all happening cleanly at once. I've been trying to mash together the code from the Recording and Playback demo projects, but couldn't figure out how to put them together properly. Now I'm trying to make a new class, AudioRecorderPlayer.
I'm successfully recording audio (using code based on the AudioRecorder class in the Recording demo), but the thumbnail isn't displaying despite the fact I'm calling thumbnail.addBlock and my component class is listening for change events (it's set to repaint when it receives such a message).
On the playback side, I'm able to load a file (at least, I can create a file object and pass it into setSource), but the waveform does not display and when the audio callback tries to receive samples from the file, I get an exception in ResamplingAudioSource::getNextAudioBlock because 'buffer' is empty.
Here is the current state of my classes. WaveformGUI is the UI component, AudioRecorderPlayer is the backend (it's a mess, sorry).
class WaveformGUI :
public Component,
public ChangeListener,
public ChangeBroadcaster,
private Slider::Listener,
private ScrollBar::Listener,
private Timer
{
public:
enum Mode {
IDLE = 0,
RECORD,
PLAY,
NUM_MODES
};
explicit WaveformGUI (AudioDeviceManager& sharedAudioDeviceManager)
:
deviceManager(sharedAudioDeviceManager),
formatManager(),
thumbnailCache (10),
thumbnail (512, formatManager, thumbnailCache),
currentFilePath("Audio Recording.wav"),
io(thumbnail),
// recorder(thumbnail, thread),
// player(),
zoomSlider (new Slider()),
scrollBar (new ScrollBar(false)),
displayFullThumb (false)
{
formatManager.registerBasicFormats();
deviceManager.addAudioCallback(&io);
thumbnail.setSource(nullptr);
thumbnail.addChangeListener(this);
addAndMakeVisible(scrollBar);
scrollBar->addListener (this);
scrollBar->setRangeLimits (visibleRange);
scrollBar->setAutoHide (false);
currentPositionMarker.setFill (Colours::white.withAlpha (0.85f));
addAndMakeVisible (currentPositionMarker);
}
~WaveformGUI()
{
deviceManager.removeAudioCallback(&io);
zoomSlider->removeListener (this);
scrollBar->removeListener (this);
thumbnail.removeChangeListener (this);
io.setSource(nullptr, 0, nullptr, 0, 0);
thumbnail.setSource (nullptr);
scrollBar = nullptr;
zoomSlider = nullptr;
currentAudioFileSource = nullptr;
}
AudioThumbnail& getAudioThumbnail() { return thumbnail; }
void import()
{
FileChooser fc ("Choose a file to open...",
File::getCurrentWorkingDirectory(),
"*.wav",
true);
fc.browseForMultipleFilesToOpen();
const File& file = fc.getResult();
if (! file.isDirectory())
{
currentFilePath = file.getFullPathName();
io.loadFile (file, formatManager);
updateRange();
zoomSlider->setValue (0, dontSendNotification);
}
}
void follow(const bool shouldFollow)
{
io.setFollowsTransport(shouldFollow);
}
void startRecording()
{
File newFile (File::getSpecialLocation (File::userDocumentsDirectory)
.getNonexistentChildFile ("Audio Recording", ".wav"));
currentFilePath = newFile.getFullPathName();
io.startRecording (newFile);
setDisplayFullThumbnail (false);
}
void stopRecording()
{
io.stopRecording();
io.loadFile(File::getSpecialLocation (File::userDocumentsDirectory)
.getChildFile (currentFilePath), formatManager);
updateRange();
setDisplayFullThumbnail (true);
}
void record()
{
if(io.isRecording())
{
stopRecording();
// setMode(IDLE);
}
else{
if (io.isPlaying())
{
io.stopPlaying();
}
// setMode(RECORD);
startRecording();
}
}
void play()
{
if (io.isPlaying())
{
io.stopPlaying();
// setMode(IDLE);
}
else
{
// setMode(PLAY);
io.setPosition (0);
io.startPlaying();
}
}
void stop()
{
if(io.isRecording())
{
stopRecording();
}
if (io.isPlaying())
{
io.stopPlaying();
}
// setMode(IDLE);
}
void setDisplayFullThumbnail (bool displayFull)
{
displayFullThumb = displayFull;
repaint();
}
//
File getCurrentFilePath() const noexcept { return currentFilePath; }
void setZoomFactor (double amount)
{
if (thumbnail.getTotalLength() > 0)
{
const double newScale = jmax (0.001, thumbnail.getTotalLength() * (1.0 - jlimit (0.0, 0.99, amount)));
const double timeAtCentre = xToTime (getWidth() / 2.0f);
setRange (Range<double> (timeAtCentre - newScale * 0.5, timeAtCentre + newScale * 0.5));
}
}
void setRange (Range<double> newRange)
{
visibleRange = newRange;
scrollBar->setCurrentRange (visibleRange);
updateCursorPosition();
repaint();
}
void paint (Graphics& g) override
{
g.fillAll(juce::Colour::fromRGBA(179, 179, 179, 255));
g.setColour(juce::Colour::fromRGBA(51, 51, 51, 255));
if (thumbnail.getTotalLength() > 0.0)
{
Rectangle<int> thumbArea (getLocalBounds());
thumbArea.removeFromBottom (scrollBar->getHeight() + 4);
thumbnail.drawChannels (g, thumbArea.reduced (2),
visibleRange.getStart(), visibleRange.getEnd(), 1.0f);
}
else
{
g.setFont (14.0f);
g.drawFittedText ("(No audio data)", getLocalBounds(), Justification::centred, 2);
}
}
void resized() override
{
scrollBar->setBounds (getLocalBounds().removeFromBottom (14).reduced (2));
}
void changeListenerCallback (ChangeBroadcaster* source) override
{
if (source == &thumbnail)
{
repaint();
}
}
//
void sliderValueChanged (Slider* slider)
{
if (slider == zoomSlider)
{
setZoomFactor (zoomSlider->getValue());
}
}
void mouseDown (const MouseEvent& e) override
{
mouseDrag (e);
}
void mouseDrag (const MouseEvent& e) override
{
if (io.canMoveTransport())
{
io.setPosition (jmax (0.0, xToTime ((float) e.x)));
}
}
void mouseUp (const MouseEvent&) override
{
if(io.isPlaying())
{
io.stopPlaying();
}
else
{
io.startPlaying();
}
}
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
{
if (thumbnail.getTotalLength() > 0.0)
{
double newStart = visibleRange.getStart() - wheel.deltaX * (visibleRange.getLength()) / 10.0;
newStart = jlimit (0.0, jmax (0.0, thumbnail.getTotalLength() - (visibleRange.getLength())), newStart);
if (io.canMoveTransport())
setRange (Range<double> (newStart, newStart + visibleRange.getLength()));
if (wheel.deltaY != 0.0f)
zoomSlider->setValue (zoomSlider->getValue() - wheel.deltaY);
repaint();
}
}
private:
// TimeSliceThread thread;
AudioDeviceManager& deviceManager;
AudioFormatManager formatManager;
AudioThumbnailCache thumbnailCache;
AudioThumbnail thumbnail;
String currentFilePath;
ScopedPointer<AudioFormatReaderSource> currentAudioFileSource;
AudioRecorderPlayer io;
// AudioRecorder recorder;
// AudioTransportSource player;
double sampleRate;
ScopedPointer<Slider> zoomSlider;
ScopedPointer<ScrollBar> scrollBar;
Range<double> visibleRange;
DrawableRectangle currentPositionMarker;
bool displayFullThumb;
Mode mode;
void updateRange ()
{
const Range<double> newRange (0.0, thumbnail.getTotalLength());
scrollBar->setRangeLimits (newRange);
setRange (newRange);
// startTimerHz (40);
}
float timeToX (const double time) const
{
return getWidth() * (float) ((time - visibleRange.getStart()) / (visibleRange.getLength()));
}
double xToTime (const float x) const
{
return (x / getWidth()) * (visibleRange.getLength()) + visibleRange.getStart();
}
void scrollBarMoved (ScrollBar* scrollBarThatHasMoved, double newRangeStart) override
{
if (scrollBarThatHasMoved == scrollBar)
if (io.canMoveTransport())
setRange (visibleRange.movedToStartAt (newRangeStart));
}
void timerCallback() override
{
if (io.canMoveTransport())
{
updateCursorPosition();
}
else
{
setRange (visibleRange.movedToStartAt (io.getCurrentPosition() - (visibleRange.getLength() / 2.0)));
}
}
void updateCursorPosition()
{
currentPositionMarker.setVisible (io.isPlaying() || isMouseButtonDown());
currentPositionMarker.setRectangle (Rectangle<float> (timeToX (io.getCurrentPosition()) - 0.75f, 0,
1.5f, (float) (getHeight() - scrollBar->getHeight())));
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WaveformGUI)
};
class AudioRecorderPlayer : public AudioIODeviceCallback, public AudioTransportSource
{
public:
AudioRecorderPlayer (AudioThumbnail& thumbnailToUpdate)
:
sampleRate (0),
sourceSampleRate (0.0),
thumbnail (thumbnailToUpdate),
backgroundThread ("Audio Recorder Player Thread"),
nextSampleNum (0), activeWriter (nullptr),
bufferSize (0),
lastGain (1.0f),
gain (1.0f),
playing (false),
stopped (true),
source (nullptr),
resamplerSource (nullptr),
bufferingSource (nullptr),
positionableSource (nullptr),
masterSource (nullptr),
blockSize (128),
readAheadBufferSize (0),
isPrepared (false),
inputStreamEOF (false),
isFollowingTransport(false)
{
backgroundThread.startThread(3);
}
~AudioRecorderPlayer()
{
//Transport
if(isRecording())
{
stopRecording();
}
if(isPlaying())
{
stopPlaying();
}
releaseMasterResources();
backgroundThread.stopThread(3);
}
TimeSliceThread * getBackgroundThread(){
return &backgroundThread;
}
// Player
void setSource (PositionableAudioSource* const newSource,
int readAheadSize, TimeSliceThread* readAheadThread,
double sourceSampleRateToCorrectFor, int maxNumChannels){
if (source != newSource)
{
AudioSource* const oldSource = source;
if (newSource != nullptr && bufferSize > 0 && sampleRate > 0){
newSource->prepareToPlay (bufferSize, sampleRate);
}
{
const ScopedLock sl (callbackLock);
source = newSource;
}
if (oldSource != nullptr){
oldSource->releaseResources();
}
}
if (source == newSource)
{
if (source == nullptr)
return;
setSource (nullptr, 0, nullptr, 0, 0); // deselect and reselect to avoid releasing resources wrongly
}
readAheadBufferSize = readAheadSize;
sourceSampleRate = sourceSampleRateToCorrectFor;
ResamplingAudioSource* newResamplerSource = nullptr;
BufferingAudioSource* newBufferingSource = nullptr;
PositionableAudioSource* newPositionableSource = nullptr;
AudioSource* newMasterSource = nullptr;
ScopedPointer<ResamplingAudioSource> oldResamplerSource (resamplerSource);
ScopedPointer<BufferingAudioSource> oldBufferingSource (bufferingSource);
AudioSource* oldMasterSource = masterSource;
if (newSource != nullptr)
{
newPositionableSource = newSource;
if (readAheadSize > 0)
{
// If you want to use a read-ahead buffer, you must also provide a TimeSliceThread
// for it to use!
jassert (readAheadThread != nullptr);
newPositionableSource = newBufferingSource
= new BufferingAudioSource (newPositionableSource, *readAheadThread,
false, readAheadSize, maxNumChannels);
}
newPositionableSource->setNextReadPosition (0);
if (sourceSampleRateToCorrectFor > 0)
newMasterSource = newResamplerSource
= new ResamplingAudioSource (newPositionableSource, false, maxNumChannels);
else
newMasterSource = newPositionableSource;
if (isPrepared)
{
if (newResamplerSource != nullptr && sourceSampleRate > 0 && sampleRate > 0)
newResamplerSource->setResamplingRatio (sourceSampleRate / sampleRate);
newMasterSource->prepareToPlay (blockSize, sampleRate);
}
}
{
const ScopedLock sl (callbackLock);
source = newSource;
resamplerSource = newResamplerSource;
bufferingSource = newBufferingSource;
masterSource = newMasterSource;
positionableSource = newPositionableSource;
inputStreamEOF = false;
playing = false;
}
if (oldMasterSource != nullptr)
{
oldMasterSource->releaseResources();
}
}
void startPlaying()
{
if ((! playing) && masterSource != nullptr)
{
{
const ScopedLock sl (callbackLock);
playing = true;
stopped = false;
inputStreamEOF = false;
}
sendChangeMessage();
}
}
void stopPlaying()
{
if (playing)
{
{
const ScopedLock sl (callbackLock);
playing = false;
}
int n = 500;
while (--n >= 0 && ! stopped)
Thread::sleep (2);
sendChangeMessage();
}
}
void setFollowsTransport (bool shouldFollow)
{
isFollowingTransport = shouldFollow;
}
bool getFollowsTransport() const
{
return isFollowingTransport;
}
bool canMoveTransport() const noexcept
{
return ! (isFollowingTransport && isPlaying());
}
void setPosition (double newPosition)
{
if (sampleRate > 0.0)
setNextReadPosition ((int64) (newPosition * sampleRate));
}
double getCurrentPosition() const
{
if (sampleRate > 0.0)
return getNextReadPosition() / sampleRate;
return 0.0;
}
double getLengthInSeconds() const
{
if (sampleRate > 0.0)
return getTotalLength() / sampleRate;
return 0.0;
}
void setNextReadPosition (int64 newPosition)
{
if (positionableSource != nullptr)
{
if (sampleRate > 0 && sourceSampleRate > 0)
newPosition = (int64) (newPosition * sourceSampleRate / sampleRate);
positionableSource->setNextReadPosition (newPosition);
if (resamplerSource != nullptr)
resamplerSource->flushBuffers();
inputStreamEOF = false;
}
}
int64 getNextReadPosition() const
{
if (positionableSource != nullptr)
{
const double ratio = (sampleRate > 0 && sourceSampleRate > 0) ? sampleRate / sourceSampleRate : 1.0;
return (int64) (positionableSource->getNextReadPosition() * ratio);
}
return 0;
}
int64 getTotalLength() const
{
const ScopedLock sl (callbackLock);
if (positionableSource != nullptr)
{
const double ratio = (sampleRate > 0 && sourceSampleRate > 0) ? sampleRate / sourceSampleRate : 1.0;
return (int64) (positionableSource->getTotalLength() * ratio);
}
return 0;
}
bool isLooping() const
{
const ScopedLock sl (callbackLock);
return positionableSource != nullptr && positionableSource->isLooping();
}
/** Returns the source that's playing.
May return nullptr if there's no source.
*/
AudioSource* getCurrentSource() const noexcept { return source; }
void setGain (float newGain) noexcept
{
gain = newGain;
}
float getGain() const noexcept { return gain; }
void prepareToPlay (int samplesPerBlockExpected, double newSampleRate)
{
const ScopedLock sl (callbackLock);
sampleRate = newSampleRate;
blockSize = samplesPerBlockExpected;
if (masterSource != nullptr)
masterSource->prepareToPlay (samplesPerBlockExpected, sampleRate);
if (resamplerSource != nullptr && sourceSampleRate > 0)
resamplerSource->setResamplingRatio (sourceSampleRate / sampleRate);
inputStreamEOF = false;
isPrepared = true;
}
void releaseResources()
{
releaseMasterResources();
}
void getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const ScopedLock sl (callbackLock);
if (masterSource != nullptr && ! stopped)
{
masterSource->getNextAudioBlock (info);
if (! playing)
{
// just stopped playing, so fade out the last block..
for (int i = info.buffer->getNumChannels(); --i >= 0;)
info.buffer->applyGainRamp (i, info.startSample, jmin (256, info.numSamples), 1.0f, 0.0f);
if (info.numSamples > 256)
info.buffer->clear (info.startSample + 256, info.numSamples - 256);
}
if (positionableSource->getNextReadPosition() > positionableSource->getTotalLength() + 1
&& ! positionableSource->isLooping())
{
playing = false;
inputStreamEOF = true;
sendChangeMessage();
}
stopped = ! playing;
for (int i = info.buffer->getNumChannels(); --i >= 0;)
info.buffer->applyGainRamp (i, info.startSample, info.numSamples, lastGain, gain);
}
else
{
info.clearActiveBufferRegion();
stopped = true;
}
lastGain = gain;
}
void audioDeviceIOCallback (const float** inputChannelData,
int totalNumInputChannels,
float** outputChannelData,
int totalNumOutputChannels,
int numSamples) override
{
// these should have been prepared by audioDeviceAboutToStart()...
jassert (sampleRate > 0 && bufferSize > 0);
// Player
if(isPlaying())
{
const ScopedLock sl (callbackLock);
if (source != 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);
getNextAudioBlock (info);
//apply gain
for (int i = info.buffer->getNumChannels(); --i >= 0;){
buffer.applyGainRamp (i, info.startSample, info.numSamples, lastGain, gain);
lastGain = gain;
}
}
else
{
// 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);
}
}
}
}
// 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 audioDeviceAboutToStart (AudioIODevice* device) override
{
sampleRate = device->getCurrentSampleRate();
prepareToPlay (sampleRate, device->getCurrentBufferSizeSamples());
}
/** Implementation of the AudioIODeviceCallback method. */
void audioDeviceStopped() override
{
if (source != nullptr){
source->releaseResources();
}
sampleRate = 0.0;
bufferSize = 0;
tempBuffer.setSize (2, 8);
}
/** An alternative method for initialising the source without an AudioIODevice. */
void prepareToPlay(double newSampleRate, int newBufferSize)
{
sampleRate = newSampleRate;
bufferSize = newBufferSize;
zeromem (channels, sizeof (channels));
if (source != nullptr){
source->prepareToPlay (bufferSize, sampleRate);
}
}
//Recorder
void startRecording (const File& file)
{
if (sampleRate > 0)
{
// 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 stopRecording()
{
// 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;
}
bool isRecording() const
{
return activeWriter != nullptr;
}
bool isPlaying() const
{
return playing;
}
void loadFile(const File& file, AudioFormatManager& formatManager)
{
// unload the previous file source and delete it..
stopRecording();
stopPlaying();
setSource (nullptr, 0, nullptr, 0, 0);
AudioFormatReader* reader = formatManager.createReaderFor (file);
if (reader != nullptr)
{
// ..and plug it into our transport source
setSource (new AudioFormatReaderSource (reader, false),
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
thumbnail.setSource (new FileInputSource (file));
}
}
private:
//==============================================================================
// Shared
double sampleRate, sourceSampleRate;
CriticalSection callbackLock;
AudioThumbnail& thumbnail;
// Recorder
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;
// CriticalSection writeLock;
AudioFormatWriter::ThreadedWriter* volatile activeWriter;
// Player
// CriticalSection readLock;
int bufferSize;
float* channels [128];
float* outputChans [128];
const float* inputChans [128];
AudioSampleBuffer tempBuffer;
float volatile lastGain, gain;
bool volatile playing, stopped;
PositionableAudioSource* source;
ResamplingAudioSource* resamplerSource;
BufferingAudioSource* bufferingSource;
PositionableAudioSource* positionableSource;
AudioSource* masterSource;
int blockSize, readAheadBufferSize;
bool volatile isPrepared, inputStreamEOF;
bool isFollowingTransport;
void releaseMasterResources()
{
const ScopedLock sl (callbackLock);
if (masterSource != nullptr)
masterSource->releaseResources();
isPrepared = false;
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioRecorderPlayer)
};
I think at least part of the problem is that I'm not setting the source for the transport properly. I've been stuck on this for a few days now so thanks in advance to anyone who can help me. Even just knowing the correct classes to inhereit from would be a good start. I figured I need
AudioIODeviceCallback for the recording part, and AudioTransportSource to play and load files.
