Simple Audio Recording Question

Hey everyone,

New JUCE (and really new to C++ as well) user here, and had a quick question as I begin writing a simple audio/sensor recording application. I’m a little confused after reading other threads on the topic, and looking at the AudioDemo example w/Juce, so figured I’d ask and see if someone could help clarify a bit. Basically, I’m creating an application to record audio/sensor data as uncompressed .wav files. The first test is simply to stream in live audio (microphone/line, soundflower from another application…etc) and record it to disk.

I have created a class called AudioRecorder (see below) which should basically set up an input device, with a channel selector combobox, that would enable recording. So far, from pulling some stuff out of the juce AudioDemo, I’ve managed to make the basis of my record class, with an auto-updating channel selector, and a graphical waveform display (ala the one in the demo). All is good! Well…almost :slight_smile: I can’t seem to get it writing to disk or even playing out of speakers

What is the best approach? Do I need to make a mixer/transport? Any help would be greatly appreciated. Below is my current AudioRecorder class (there are a few things commented out which are still left from the various ways i’ve tried getting audio output/recording to disk as .wav’s)

Right now, I can instantiate as many AudioRecorder classes in my main (MainComponent) class as desired, and all seem to pull audio (at least in the wave-form display) from AudioDeviceManager nicely-- although, when I create more than one instance, I get a nasty sound out of my speakers… any info on that would also be helpful as I debug!

AudioRecorder.h

[code]/*

  • AudioRecorder.h
  • juce_application
  • Created by Jordan Hochenbaum on 11/19/09.

*/

#ifndef AUDIORECORDER_H
#define AUDIORECORDER_H

#include “includes.h”

class AudioRecorder : public Component,
public Timer,
//public AudioSource,
public AudioIODeviceCallback,
public ComboBoxListener

{

public:
AudioRecorder ();
~AudioRecorder();

WavAudioFormat wavFormat;
// this wraps the actual audio device

void paint (Graphics& g);
void timerCallback();
void addSample (const float sample);
void audioDeviceIOCallback (const float** inputChannelData,
                            int totalNumInputChannels,
                            float** outputChannelData,
                            int totalNumOutputChannels,
                            int numSamples);
void audioDeviceAboutToStart (AudioIODevice*);
void audioDeviceStopped();
void setChannel(int channel);
void createListOfInputChannels(AudioIODevice*);
void comboBoxChanged(ComboBox*);

/*void prepareToPlay( int samplesPerBlockExpected, double sampleRate); 
void releaseResources();
void getNextAudioBlock(const AudioSourceChannelInfo &bufferToFill);*/

private:
int channel;
int volatile bufferPos, bufferSize, numSamplesIn;
float* circularBuffer;
float currentInputLevel;
String _inputDeviceName;

ComboBox* inputChannelSelector;

//void recordButton (Button* recordButtonWasClicked);
//void audioRec();

};

#endif//AUDIORECORDER_H[/code]

AudioRecorder.cpp

[code]/*

  • AudioRecorder.cpp
  • juce_application
  • Created by Jordan Hochenbaum on 11/19/09.

*/

#include “AudioRecorder.h”
#include “jucedemo_headers.h”

AudioRecorder::AudioRecorder()
{
channel = 0;
bufferPos = 0;
bufferSize = 512;
circularBuffer = (float*) juce_calloc (sizeof (float) * bufferSize);
currentInputLevel = 0.0f;
numSamplesIn = 0;

setOpaque (true);
startTimer (1000 / 50);  // repaint every 1/50 of a second	

addAndMakeVisible (inputChannelSelector = new ComboBox (T("input")));
inputChannelSelector->setSelectedId (1);
inputChannelSelector->addListener(this);	
inputChannelSelector->setTextWhenNothingSelected("Select Input Channel");

}

AudioRecorder::~AudioRecorder()
{
juce_free (circularBuffer);
}

//this method draws the incoming audio waveform display
void AudioRecorder::paint (Graphics& g)
{
g.fillAll (Colours::white);
g.setColour (Colours::black);

inputChannelSelector->setBounds (getWidth()/1.6, (getHeight()/4)+15, 250, 24);	

const float halfHeight = getHeight() * 0.5f;

int bp = bufferPos;

for (int x = getWidth(); --x >= 0;)
{
	const int samplesAgo = getWidth() - x;
	const float level = circularBuffer [(bp + bufferSize - samplesAgo) % bufferSize];
	
	if (level > 0.01f)
		g.drawLine ((float) getWidth()-x, halfHeight - halfHeight * level,
					(float) getWidth()-x, halfHeight + halfHeight * level);
}

}

void AudioRecorder::timerCallback()
{
repaint();
}

void AudioRecorder::addSample (const float sample)
{
currentInputLevel += fabsf (sample);

const int samplesToAverage = 128;

if (++numSamplesIn > samplesToAverage)
{
	circularBuffer [bufferPos++ % bufferSize] = currentInputLevel / samplesToAverage;
	
	numSamplesIn = 0;
	currentInputLevel = 0.0f;
}

}

void AudioRecorder::audioDeviceIOCallback (const float** inputChannelData, int totalNumInputChannels, float** outputChannelData, int totalNumOutputChannels, int numSamples) {

for (int i = 0; i < totalNumInputChannels; ++i)
{
	if (inputChannelData [i] != 0)
	{
		for (int j = 0; j < numSamples; ++j){
			addSample (inputChannelData [channel][j]);
		}
		
		break;
	}
}

}

void AudioRecorder::audioDeviceAboutToStart (AudioIODevice* _audioIODevice)
{
zeromem (circularBuffer, sizeof (float) * bufferSize);

//test to see if input device has changed. If so, update list of input Channels.
if (_inputDeviceName != _audioIODevice->getName())
{
	channel = 0;
	createListOfInputChannels( _audioIODevice);
	_inputDeviceName ==_audioIODevice->getName();
}

}

void AudioRecorder::audioDeviceStopped()
{
zeromem (circularBuffer, sizeof (float) * bufferSize);
}

/*void AudioRecorder::prepareToPlay(int samplesPerBlockExpected, double sampleRate)
{
}

void AudioRecorder::releaseResources()
{
}

void AudioRecorder::getNextAudioBlock(const AudioSourceChannelInfo &bufferToFill)
{
bufferToFill.clearActiveBufferRegion();
}*/

void AudioRecorder::createListOfInputChannels(AudioIODevice* _audioIODevice)
{
//clear current contents IN comboBox’s
inputChannelSelector->clear();

//Add input channel names to all channel select comboBox's
for (int i = 0; i< _audioIODevice->getInputChannelNames().size(); i++)
{
	inputChannelSelector->addItem(_audioIODevice->getInputChannelNames()[i], i+1);
}	

}

void AudioRecorder::comboBoxChanged(ComboBox* comboBoxThatHasChanged)
{
//if the inputChannelSelector box has changed, update/set the AudioRecorder’s channel
setChannel(inputChannelSelector->getSelectedItemIndex());
}

void AudioRecorder::setChannel(int _channel) {
channel = _channel;
}
//==============================================================================
[/code]

You need to clear the output channels, or there might be garbage in them that get played… but didn’t you look at the audio code in the juce demo?? It does almost exactly what you’re trying to do…

thanks for the tip Jule’s, I’ll try to figure that out, is there a specific place/way to clear the output channels?.. I did indeed investigate the AudioDemo a lot, which looks like a fantastic example and also looks like it did almost exactly what I need, but I was a little confused if I needed to use transport and mixer (as in the example). In fact, at one point earlier yesterday I followed the example and tried to implement them as follows:

I created an instance of AudioSourcePlayer, AudioTransportSource, and MixerAudioSource. I then made my AudioRecorder class extend AudioSource, and tried to add each instance of my AudioRecorder class to the mixer, and then connect the mixer to the AudioSourcePlayer, then set the transport to play.

While it didn’t work, upon investigating the docs, it also didn’t seem like the right approach and so I came here to ask. Seems like I do not need transport as I am not playing back any file’s, and just want to route incoming audio to my output, and also record/save the incoming audio to disk.

Does the following look like the right approach?
[list]
[]Make my recorder class also extend AudioSource so I can attach it to a mixer and then connect the mixer to AudioSourcePlayer for output? Is this necessary or maybe there is a more direct way to output the incoming audio?[/]
[]Create some kind of buffer/stream (if necessary) at the same time and pass that into an AudioFormatWriter to save the incoming audio as a .wav/.aiff…etc?[/][/list]

Thanks again for your help!

J

From the demo code:

[code] void audioDeviceIOCallback (const float** inputChannelData, int numInputChannels,
float** outputChannelData, int numOutputChannels,
int numSamples)
{
if (recording)
{
const AudioSampleBuffer incomingData ((float**) inputChannelData, numInputChannels, numSamples);
circularBuffer.addSamplesToBuffer (incomingData, 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);
}[/code]

You could take either approach, using audio sources, or just handling the data directly - either would work, it’s up to you which would be best for your app, really.

hey Jules,
which demo code is that from? Can’t seem to find that in my demo (AudioDemo.cpp)… thanks!

It’s from the tip. I guess you’re still using an older version.

I was using the 1.50 version from the downloads page, but I just checked out the latest version from GIT and I see the new demos… Looks like I’ll be busy dissecting that tomorrow :slight_smile:
Thanks a lot!

@Jules: the example code for audio recording / circular buffer stuff has been really helpful, and has gotten me very close! I have however found myself stuck at a brick wall the last day or so, and was hoping you or others may have some more advice. It’s been a lot to digest, but have been learning a lot from your example code and think I finally (mostly) understand the circular buffer code and whatnot.

I am basically trying to create a multi-track recorder, for both Audio and sensor data (the latter coming over serial and have some step function / sample and holding at audio rate). For now, I am just trying to get the audio recording going. I have created an AudioRecorder class and have managed to get it writing to disk borrowing from your recording/writing samples. The problem I am currently having is being able to select which channel gets recorded.

Each AudioRecorder class has one of your liveAudioInputDisplays attached for drawing the waveform, and I was easily able to get it drawing only the selected channel (selected via a combobox in the recorder) by hard setting the channel in the liveAudioInput Displays audioIODeviceCallback as such (see bold)

[code]for (int chan = 0; chan < numInputChannels; ++chan)
{
if (inputChannelData[selectedChannel] != 0)
accumulator += fabsf (inputChannelData[selectedChannel][i]);

    }[/code]

I was hoping to do something similar in my actual AudioRecorder classes AudioIODeviceCallback however, no matter how I have tried to do it, it always crashes the app. I can get all the channels recording/rewriting the first channel, however, I cant get it to work by specifying the array from inputChannelData-- heres my AudioRecorders audioIODeviceCallback

[code]void AudioRecorder::audioDeviceIOCallback (const float** inputChannelData, int numInputChannels,
float** outputChannelData, int numOutputChannels,
int numSamples)
{
if (recording)
{
//create a buffer of our latest samples and pass it into our circularBuffer
//added [channel] which is the selected input channel & changed numInputChannels to '1’
const AudioSampleBuffer incomingData ((float**) inputChannelData[channel], 1, numSamples);
circularBuffer.addSamplesToBuffer (incomingData, 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);

}[/code]

Is the problem in circularBuffer.addSamplesToBuffer? Perhaps its in the circularBuffer’s readSamplesFromBuffer…both? Here is the latest version of those methods, which I think should be pretty similar if not identical to your example because I tried to reverse all the tinkering I did the last two days. Any help is much appreciated! Thanks a lot…

[code]void addSamplesToBuffer (const AudioSampleBuffer& sourceBuffer, int numSamples)
{
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 (i, newDataStart, sourceBuffer, i, 0, bufferSize - newDataStart);
			buffer.copyFrom (i, 0, sourceBuffer, i, bufferSize - newDataStart, newDataEnd);
			}
		}
		else
		{
			for (int i = jmin (buffer.getNumChannels(), sourceBuffer.getNumChannels()); --i >= 0;)
				buffer.copyFrom (i, newDataStart, sourceBuffer, i, 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 (i, 0, buffer, i, availableDataStart, bufferSize - availableDataStart);
				destBuffer.copyFrom (i, bufferSize - availableDataStart, buffer, i, 0, availableDataEnd);
			}
		}
		else
		{
			for (int i = jmin (buffer.getNumChannels(), destBuffer.getNumChannels()); --i >= 0;)
				destBuffer.copyFrom (i, 0, buffer, i, availableDataStart, numSamplesDone);
		}
		
		return numSamplesDone;
	}

[/code]


That's a pretty ropey-looking bit of code to me!

And it's a very good example of why c++ style casts are better than old c-style ones! If you'd written "const_cast <float**> (inputChannelData[channel])", it wouldn't have compiled.

That’s a pretty ropey-looking bit of code to me!

And it’s a very good example of why c++ style casts are better than old c-style ones! If you’d written “const_cast <float**> (inputChannelData[channel])”, it wouldn’t have compiled.

hmm, while trying to wrap my head around the example code, and learning the different classes JUCE makes available, it seemed like a potential solution; although, only seemed to get it working for the waveform display…Any suggestions on how to go about passing only a selected channel to the circular buffer?

There’s nothing wrong with what you’re trying to do, but obviously casting a float* to a float** isn’t a very bright idea… Just create your own float* array, put the appropriate pointers in it, and use that.

Thanks A LOT Jules! I tried getting it working as you said, but couldn’t quite get it-- I ended up doing it a little differently (see below), but seems to work pretty well. Does this look alright to you / or can you see a more efficient or better way? Thanks again for all your help.

[code]void AudioRecorder::audioDeviceIOCallback (const float** inputChannelData, int numInputChannels,
float** outputChannelData, int numOutputChannels,
int numSamples)
{
if (recording)
{
//create a buffer of our latest samples and pass it into our circularBuffer
AudioSampleBuffer incomingData (1, numSamples);
incomingData.copyFrom(0,0,inputChannelData[channel],numSamples);
circularBuffer.addSamplesToBuffer (incomingData, 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);

}[/code]

Well, that’s very inefficient, as you’re unnecessarily copying the data.

This circular buffer is your own code, right? So why not just change addSamplesToBuffer to take a channel number??

Ah, yes! Makes so much more sense now… Took all but a a minute to change the method, and make some minor changes in the circularbuffer class and it’s working great. Cheers!