Hey guys,
background
I recently found a problem in a recorder application that I swear I had tested and tested earlier, but now somehow cant seem to get around. I have various “recorder” classes (Audio, Sensor data…etc) which you can dynamically create from the gui, and record mono .wav files of the input (audio input for the AudioRecorder, serial or OSC data for the sensor recorders). The crucial part for my application was that all files were completely synchronous (so they are currently driven by the audioDeviceIOCallback), and also properly stop writing to disk at the same time (thanks @Jules for cluing me on the audio device managers audioCallbackLock!).
issue
In preparation for some research did some testing today recording a metronome out of Ableton on two Audio recorders, both listening to the same input channel from my internal mic. What I noticed was that they were perfectly in sync with one another, but randomly seem to be dropping samples (on playback I noticed the metronome gently speeding up and slowing down). I can’t for the life of me figure out whats happening. I am using the CircularBuffer class from the older JUCE Demo, which I noticed is no longer used-- instead its using AudioFormatWriter::ThreadedWriter which is some sort of FIFO buffer used to write to disk?
The only thing I changed in my version of the CircularBuffer class is that it only pays attention to the channel we are interested in recording instead of all channels in the AudioSampleBuffer passed in. Any ideas of where to start poking around? Should I ditch using the CircularBuffer and do something simialr to the new JUCE Demo? The only thing with that is I don’t see a way to only write a specific channel in AudioFormatWriter::ThreadedWriter. If I stick with the circular buffer, does it seem like the CB is the culprit or does it look like its somewhere in the writing to disk in the threads AudioRecorders threads run()?
Any help would be super appreciated… will keep poking around for now! Thanks guys
Heres a copy of the CircularBuffer class (which is almost directly from the old JUCE Demo)
#ifndef _CIRCULARAUDIOBUFFER_H_
#define _CIRCULARAUDIOBUFFER_H_
class CircularAudioBuffer
{
public:
CircularAudioBuffer (const int numChannels, const int numSamples)
: buffer (numChannels, numSamples)
{
clear();
selectedChannel = 0;
}
~CircularAudioBuffer()
{
}
void clear()
{
buffer.clear();
const ScopedLock sl (bufferLock);
bufferValidStart = bufferValidEnd = 0;
}
void addSamplesToBuffer (const AudioSampleBuffer& sourceBuffer, int numSamples, int selectedChan)
{
const int bufferSize = buffer.getNumSamples();
bufferLock.enter();
int newDataStart = bufferValidEnd;
int newDataEnd = newDataStart + numSamples;
const int actualNewDataEnd = newDataEnd;
bufferValidStart = jmax (bufferValidStart, newDataEnd - bufferSize);
bufferLock.exit();
newDataStart %= bufferSize;
newDataEnd %= bufferSize;
if (newDataEnd < newDataStart)
{
//for (int i = jmin (buffer.getNumChannels(), sourceBuffer.getNumChannels()); --i >= 0;)
//{
//copyFrom (int destChannel, int destStartSample, const AudioSampleBuffer &source, int sourceChannel, int sourceStartSample, int numSamples)
buffer.copyFrom (0, newDataStart, sourceBuffer, selectedChan, 0, bufferSize - newDataStart);
buffer.copyFrom (0, 0, sourceBuffer, selectedChan, bufferSize - newDataStart, newDataEnd);
//}
}
else
{
//for (int i = jmin (buffer.getNumChannels(), sourceBuffer.getNumChannels()); --i >= 0;)
buffer.copyFrom (0, newDataStart, sourceBuffer, selectedChan, 0, newDataEnd - newDataStart);
}
const ScopedLock sl (bufferLock);
bufferValidEnd = actualNewDataEnd;
}
int readSamplesFromBuffer (AudioSampleBuffer& destBuffer, int numSamples)
{
const int bufferSize = buffer.getNumSamples();
bufferLock.enter();
int availableDataStart = bufferValidStart;
const int numSamplesDone = jmin (numSamples, bufferValidEnd - availableDataStart);
int availableDataEnd = availableDataStart + numSamplesDone;
bufferValidStart = availableDataEnd;
bufferLock.exit();
availableDataStart %= bufferSize;
availableDataEnd %= bufferSize;
if (availableDataEnd < availableDataStart)
{
//for (int i = jmin (buffer.getNumChannels(), destBuffer.getNumChannels()); --i >= 0;)
//{
destBuffer.copyFrom (0, 0, buffer, 0, availableDataStart, bufferSize - availableDataStart);
destBuffer.copyFrom (0, bufferSize - availableDataStart, buffer, 0, 0, availableDataEnd);
//}
}
else
{
//for (int i = jmin (buffer.getNumChannels(), destBuffer.getNumChannels()); --i >= 0;)
destBuffer.copyFrom (0, 0, buffer, 0, availableDataStart, numSamplesDone);
}
return numSamplesDone;
}
void setChan(int _chan){
selectedChannel = _chan;
}
private:
CriticalSection bufferLock;
AudioSampleBuffer buffer;
int bufferValidStart, bufferValidEnd;
int selectedChannel;
};
#endif
And my recorder class (stripped of most gui stuff, and other things which shouldn’t have an effect on the problem…
#include "AudioRecorder.h"
#include <iostream>
AudioRecorder::AudioRecorder() : Thread ("audio recorder"), recording (false), sampleRate(0), liveAudioDisplayComp(0){
circularBuffer = new CircularAudioBuffer (1,44100);
channel = 0; bufferPos = 0; bufferSize = 512; numSamplesIn = 0;
startTimer (1000 / 50); // repaint every 1/50 of a second
fileName = inputChannelSelector->getItemText(inputChannelSelector->getSelectedId()); // initialize fileName for output recording .wav
savePath = "default"; //initialize savePath to NULL so that if no save folder is set, files will be saved in a "default" directory (currently user/documents folder)
}
AudioRecorder::~AudioRecorder(){
stop();
}
void AudioRecorder::paint (Graphics& g){
g.fillAll (Colours::lightgrey);
g.setColour(Colours::darkgrey);
g.drawRect (getWidth()/1.9, getHeight()/1.9, getHeight()/4.4, getHeight()/4.6,1);
}
void AudioRecorder::resized(){
//..removed for sake of posting space...
}
void AudioRecorder::timerCallback(){
repaint();
}
void AudioRecorder::startRecording()
{
//create file for recording and name it according to set name
if (channel != -1){
if (sampleRate > 0)
{
if (savePath != "default"){
fileToRecord = File(savePath).getNonexistentChildFile (fileName, ".wav");
}else{
fileToRecord = File(File::getSpecialLocation (File::userDocumentsDirectory) .getNonexistentChildFile (fileName, ".wav"));
}
startThread(); // start our thread
circularBuffer->clear(); //make sure our circular buffer is cleared
recording = true; //set recording flag to True
}
}
}
void AudioRecorder::stop(){
recording = false;
stopThread (5000);
}
bool AudioRecorder::isRecording() const{
return isThreadRunning() && recording;
}
void AudioRecorder::audioDeviceAboutToStart (AudioIODevice* device){
sampleRate = device->getCurrentSampleRate(); //set sample rate
}
void AudioRecorder::audioDeviceStopped(){
sampleRate = 0;
liveAudioDisplayComp->audioDeviceStopped();
}
void AudioRecorder::audioDeviceIOCallback (const float** inputChannelData, int numInputChannels,
float** outputChannelData, int numOutputChannels,
int numSamples)
{
//this is a global flag shared between all recorders called from outside to have them stop adding samples to the circular buffer to prepare for final writing...
if (*finishWriting == false){
numSamplesIn = numSamples;
if (recording)
{
const AudioSampleBuffer incomingData ((float**)inputChannelData, numInputChannels, numSamples);
circularBuffer->addSamplesToBuffer (incomingData, numSamples, channel);
}
}
// We need to clear the output buffers, in case they're full of junk..
for (int i = 0; i < numOutputChannels; ++i)
if (outputChannelData[i] != 0)
zeromem (outputChannelData[i], sizeof (float) * numSamples);
}
void AudioRecorder::run(){
fileToRecord.deleteFile();
OutputStream* outStream = fileToRecord.createOutputStream();
if (outStream == 0)
return;
WavAudioFormat wavFormat;
AudioFormatWriter* writer = wavFormat.createWriterFor (outStream, sampleRate, 1, 16, StringPairArray(), 0);
if (writer == 0)
{
delete outStream;
return;
}
AudioSampleBuffer tempBuffer (1, numSamplesIn );
while (! threadShouldExit())
{
int numSamplesReady = circularBuffer->readSamplesFromBuffer (tempBuffer, tempBuffer.getNumSamples());
//std::cout << "audio:" << numSamplesReady << " finishWriting: " << *finishWriting << std::endl;
if (numSamplesReady > 0)
tempBuffer.writeToAudioWriter (writer, 0, numSamplesReady);
Thread::sleep (1);
}
delete writer;
}
void AudioRecorder:: setShouldFinishWriting(bool finishUp){
finishWriting = &finishUp; //set out local bool* to point to our flag set in MainComponent
}
//-----------------------------------Setters and Getters--------------------------------//
void AudioRecorder::setChannel(int _channel){
channel = _channel;
liveAudioDisplayComp->setChan(_channel);
circularBuffer->setChan(_channel);
}
int AudioRecorder::getChannel(){
return channel;
}
//sets the fileName to append to the savePath
void AudioRecorder::setFilename(String _fileName){
fileName = _fileName;
}
//sets the savePath used for writing the files to disk
void AudioRecorder::setSavePath(String _pathName){
savePath = _pathName;
}
//method access to the (private String) filename set for the recorder from outside
String AudioRecorder::getFilename(){
return fileName;
}
//==============================================================================