Programming with PortAudio in Pure C (How to Start?)


Playing sounds and music in your application or game is a must have. One way to do this is the PortAudio library. But how do you start?

PortAudio is a cross-platform open source audio library that is entirely written in C. It uses streams and is based on a callback paradigm. It also needs a wave loader in order to play wave files. This can be achieved in a Pure C application.

Wave Mixing Console
Image from Bokskapet at Pixabay

Learn more about how to use PortAudio in conjunction with a wave loader through a small and simple example in the following paragraphs.

Programming with PortAudio in Pure C

PortAudio is a cross-platform open source library which is originally developed by Ross Bencina. The current version of PortAudio is available on github.Opens in a new tab. It is based on a callback paradigm and is licensed under the MIT licenseOpens in a new tab.. It is written entirely in C and therefore is perfect for a Pure C application.

One thing that PortAudio cannot do is loading a wave file. For this task you either have to write your own waveloader or use one available online. For this example I will use sndfile (see link below in the section where I use it).

Example: How to Start Programming PortAudio in Pure C

We write a C Program that loads a WAVE File from a specified path and plays it through the primary sound device with the usage of PortAudio. We will use the aforementioned WAVE Loader in order to get the sound from the hard disk drive.

In order to keep this example short and simple I refrained from error handling throughout. I may check for errors here and there but don’t do anything about them. This is, of course, not an approach that you should take in real life!

Used Libraries, Defines and Structs

Besides <string.h> for memset we only need the waveloader library sndfile and the portaudio library itself. We also define the number of input and output channels for convenience.

The most interesting part here is the struct. It has a sound file buffer and the corresponding info structure as members and declares a new datatype. We will need this structure later to play our wave file.

/* INCLUDES */
#include "../Lib//libsndfile/include/sndfile.h"
#include "../Lib/portaudio/include/portaudio.h"
#include <string.h>

/* DEFINES */
#define INPUT_CHANNELS 0
#define OUTPUT_CHANNELS 2

/* STRUCTS */
typedef struct
{
	SNDFILE* file;
	SF_INFO	info;
} pa_callback_data_t;

Function Declarations and Global Variables

At first we declare all functions that we will need. You could also define them here but I like to have the main function as first defined function in my programs. It also makes it easier to factor it out into an interface later if you want to.

I separated one function from the others because this one will not be called directly. It will instead be called by PortAudio as a so called callback function. Again, there is no need for separation but it makes things clearer to other programmers (including yourself in the future).

Here we declare our variable that will contain the audio data with the struct we created above. We will also need a global Port Audio stream to manage the playing process.

/* FUNCTIONS */
static int initialize_audio_data();
static PaError initialize_port_audio();
static void play_audio();
static PaError clean_up();

/* CALLBACKS */	
static int pa_stream_callback(const void*, void*, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*);

/* VARIABLES */
static pa_callback_data_t _audioData;
static PaStream* _stream = NULL;

Initializing Audio Data

As I mentioned above we cannot load the wave file with the PortAudio lib. A popular wave loader that is used together with PortAudio is sndfile which you can download hereOpens in a new tab. (The Audacity DAW uses, or at least used, this combination of libraries).

Our job is to load the wave data into the file part and write the corresponding info into the info part of our struct. Afterwards we check for errors and return the result of this error check.

...
static int initialize_audio_data();
...
static int initialize_audio_data()
{
	_audioData.file = sf_open("../Asset/piano.wav", SFM_READ, &_audioData.info);
	return sf_error(_audioData.file);
}

Initializing PortAudio

The first thing to do is to initialize PortAudio. If this fails everything else is meaningless in this program. Then We open a stream with the default output interface.

Note the parameters: We fill our stream variable, tell the number of input and output channels, take the samplerate from our info member of the sndfile struct and also set the callback function to the one we declared above. We also hand over our callback struct variable which will the be available within the callback function.

...
static PaError initialize_port_audio();
...
static PaError initialize_port_audio()
{
	PaError error = SF_ERR_NO_ERROR;

	error = Pa_Initialize();
	if (error) { return error; }

	error = Pa_OpenDefaultStream(&_stream, INPUT_CHANNELS, OUTPUT_CHANNELS, paFloat32, _audioData.info.samplerate,
		paFramesPerBufferUnspecified, pa_stream_callback, &_audioData);
	if (error) { return error; }

	return paNoError;
}
...

Playing the Audio

This is the easiest and the trickiest part of the application in action together. We simply start the stream and wait as long as the stream is active before we stop and close it.

...
static void play_audio();
...
static void play_audio()
{
	Pa_StartStream(_stream);
	while (Pa_IsStreamActive(_stream)) {
		Pa_Sleep(100);
	}
	Pa_StopStream(_stream);

	Pa_CloseStream(_stream);
}
...

The real magic happens in the callback function which we applied to the stream at initialization and which is now called by PortAudio.

We clear the output buffer to make sure that we don’t play anything we don’t want to, for example bits of files played earlier on. Then we read the wave file directly into the output buffer and count the frames we have read.

When we read less frames than the frame count we know that there is nothing left to play. We then return paComplete which indicates that the stream is no longer active.

You can name the callback function anything you want but you have to maintain the order and types of the parameters!

...
static int pa_stream_callback(const void*, void*, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*);
...
static int pa_stream_callback(const void* input, void* output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo,
	PaStreamCallbackFlags statusFlags, void* userData)
{
	pa_callback_data_t* data = (pa_callback_data_t*)userData;
	sf_count_t framesRead = 0;
	sf_count_t frames = frameCount * (sf_count_t)data->info.channels;

	memset(output, 0, sizeof(float) * frameCount * data->info.channels); 	/* clear output buffer */

	framesRead = sf_readf_float(data->file, output, frameCount);

	if (framesRead < frameCount) {
		return paComplete;
	}

	return paContinue;
}

Cleaning Up

We close the audio file that we loaded with the sndfile library. If this fails we only report this to the user because we always want the next step to happen. The function returns whether the termination of PortAudio was successful.

...
static PaError clean_up();
...
static PaError clean_up()
{
	if (sf_close(_audioData.file) != 0) {
		fprintf(stdout, "Error: Could not close Audio File!");
	}

	return Pa_Terminate();
}

Putting It All Together

Everything is ready to run. We need two different error indicator variables if we want to take advantage of the PortAudio error enum and don’t confuse it with the sndfile errors.

We first need to load our wave file into the sndfile structure because we will need it in the next step which is the initialization of PortAudio. After initialization we play the audio and finally clean up everything.

...
int main()
{
	PaError error = SF_ERR_NO_ERROR;	
	int sndfileError = 0;
	
	sndfileError = initialize_audio_data();
	if (sndfileError) { return sndfileError; }

	error = initialize_port_audio();
	if (error) { return error; }

	play_audio();

	error = clean_up();
	if (error) { return error; }

	return 0;
}
...

PortAudio (and sndfile in this case) is a good way to play wave files in your application. You can now try to use it in your applications and games.

Marco Lieblang

Professional Programmer since 2003, passionate Programmer since the mid 90's. Developing in many languages from C/C++ to Java, C#, Python and some more. And I also may know a bit about Assembly Languages and Retro Systems.

Recent Posts