Dropping samples?

66 posts / 0 new
Last post
jordanh
Offline
Last seen: 4 months 23 hours ago
Joined: 14 Mar 2010 - 04:16
Dropping samples?

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;	
}


//==============================================================================



jules
Offline
Last seen: 4 hours 32 min ago
Joined: 29 Apr 2013 - 18:37
Re: Dropping samples?

I'd definitely recommend updating to the latest code before spending too long debugging. That old CircularBuffer class was just a quick hack that I wrote to get the demo running, it was never intended as a piece of rock-solid code.

jordanh
Offline
Last seen: 4 months 23 hours ago
Joined: 14 Mar 2010 - 04:16
Re: Dropping samples?

Thanks Jules, always appreciate your input. Only question I have is if I'm using AudioFormatWriter::TreadedWriter as per the new demo code, how can I specify only one channel to be written being that the write method takes in a float** array? Would it make sense for my to copy the channel im interested in into a new float** array, and then to zero the rest and pass that into write or is there something under my nose im missing?

Best,
J

e.g. pulled from the demo

void audioDeviceIOCallback (const float** inputChannelData, int /*numInputChannels*/,
                                float** outputChannelData, int numOutputChannels,
                                int numSamples)
    {
        const ScopedLock sl (writerLock);

        if (activeWriter != 0)
            activeWriter->write (inputChannelData, numSamples);

        // 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);
    }
jules
Offline
Last seen: 4 hours 32 min ago
Joined: 29 Apr 2013 - 18:37
Re: Dropping samples?

Yes, I guess you'd just create a new float** containing only the channel pointer that you want it to use.

jordanh
Offline
Last seen: 4 months 23 hours ago
Joined: 14 Mar 2010 - 04:16
Re: Dropping samples?

hmm, I've been trying for the last 2 days before asking for help but I can't seem to get stable results. I've changed my code to be identical to the juce demo (and have also been testing inside the juce demo to make sure its not something else in my program), and in both cases it works fine 100% of the time when as per the demo, but as soon as I try using a new float** (have tried creating them a couple different ways) it sometimes works, and sometimes doesn't.

Could it be because of the way I am creating my float** array and assigning it [only] the channel I want to record? Perhaps it is too slow, and the next callback happens before it finishes? Heres what I'm currently doing...

I've changed the demo code in audioIODeviceCallback from

const ScopedLock sl (writerLock);
	
	if (activeWriter != 0)
		activeWriter->write (inputChannelData, numSamples);
	
	// 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);

to:

const ScopedLock sl (writerLock);
	
	AudioSampleBuffer monoChannelBuffer(numInputChannels, numSamples);
	monoChannelBuffer.copyFrom(0, 0, inputChannelData[channel], numSamples);
	
	if (activeWriter != 0)
		activeWriter->write((const float**)monoChannelBuffer.getArrayOfChannels(), numSamples);
	
	// 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);

Any help would be greatly appreciated. I tried doing it without an AudioSampleBuffer but perhaps I was still going a roundabout way of assigning the new array which was too slow. Or perhaps its something else completely? Thanks guys!

jordanh
Offline
Last seen: 4 months 23 hours ago
Joined: 14 Mar 2010 - 04:16
Re: Dropping samples?

Instead of using an AudioSampleBuffer I tried just with float* arrays as such, but still only works some of the time... Perhaps it has nothing to do with the arrays but the writer? Any ideas? Thanks!

void RecOut::audioDeviceIOCallback (const float** inputChannelData, int numInputChannels, float** outputChannelData, int numOutputChannels,int numSamples)
{
	const ScopedLock sl (writerLock);
	const float* monoChannelArray[numInputChannels];
	monoChannelArray[0] = inputChannelData[channel];
	
	if (activeWriter != 0)
		activeWriter->write(monoChannelArray, numSamples);
	
	// We need to clear the output buffers, in case they're full of junk..
}
jules
Offline
Last seen: 4 hours 32 min ago
Joined: 29 Apr 2013 - 18:37
Re: Dropping samples?

When you created your AudioFormatWriter, did you make sure that you requested only one channel?

jordanh
Offline
Last seen: 4 months 23 hours ago
Joined: 14 Mar 2010 - 04:16
Re: Dropping samples?

jules wrote:
When you created your AudioFormatWriter, did you make sure that you requested only one channel?

Yep...
AudioFormatWriter* writer = wavFormat.createWriterFor (fileStream, sampleRate, 1, 16, StringPairArray(), 0);
Could it be that I need to increase/decrease the # of samples the ThreadedWriter buffers?
threadedWriter = new AudioFormatWriter::ThreadedWriter (writer, backgroundThread, 32768);
jules
Offline
Last seen: 4 hours 32 min ago
Joined: 29 Apr 2013 - 18:37
Re: Dropping samples?

Quote:
Could it be that I need to increase/decrease the # of samples the ThreadedWriter buffers?

Nope.

I can't see any obvious mistakes - if it was me, the next thing I'd try would be to step into the writer and see what's actually going on in there. It might be obvious what's going wrong.

jordanh
Offline
Last seen: 4 months 23 hours ago
Joined: 14 Mar 2010 - 04:16
Re: Dropping samples?

Thanks Jules, I'll take a dive and see if anything stands out. Will report back...

As I try to code things as efficient as possible, can I ask you which method for preparing the single-channel buffer you find more appropriate/efficient? Using an AudioSampleBuffer and copying the channel in and then calling getArrayOfChannels() in the writer (earlier post) or the last example where I just pack a float* array and pass it into the writer (const float* monoChannelArray[numInputChannels]; monoChannelArray[0] = inputChannelData[channel];...)? Thanks

jules
Offline
Last seen: 4 hours 32 min ago
Joined: 29 Apr 2013 - 18:37
Re: Dropping samples?

I think it's safe to say that creating a float[] and putting a pointer into it is faster than allocating a massive chunk of memory, copying another massive chunk of memory into it, and then throwing it away when you're done!

Pages