Writing About Cross platform audio with Portaudio and libsndfile
In my recent efforts to make a game I’d reached a point where I had a rudimental 2d platform prototype which included a player character with animation via a sprite sheet, collision detection with the ground and platforms as well as parallax scrolling backgrounds. To make the prototype feel a bit more whole, I decided to add sound effects for the player and the menu, as well as background music.
In my initial investigation I’d identified Portaudio and libsndfile as the candidate 3rd party libraries I would target to facilitate audio playback. I found this an incredibly difficult domain to get my head around, as well as the documentation for hooking these things together in a usable way being relatively sparse, so I’ve put together an example of how to achieve cross platform asynchronous audio playback in C++ using Portaudio and libsndfile.
The source code for this guide can be found here: https://github.com/andystanton/sound-example
Getting started
The aim of this guide is to build an application that is capable of being compiled on Windows, Linux and Mac OSX.
The cross platform support will come via GNU compilers and tied together using make which will build the application’s sources as well as those of libsndfile and Portaudio. The makefiles will be different for each platform, and the simplest way to generate cross-platform makefiles for C++ projects is with CMake so this will be the tool that drives the build configuration.
The starting point of the project then is the CMakeLists.txt
file in the project root:
To build Portaudio and libsndfile we first need to obtain their sources. There are a few ways to go about this. One way might be to write a script to download them. Another might be to use Biicode to manage dependencies and create community packages for them. In the past I’ve opted for git submodules to include the source in a project, but for this guide I’m going to use CMake’s ExternalProject
module.
The module is included in CMakeLists.txt
by adding:
Building libsndfile
Let’s first build libsndfile. To do this, we need to use the ExternalProject_Add
CMake command. This command is able to handle a variety of project source and structure formats, as well as offering the ability to introduce custom steps to tailor the behaviour as needed. The command is well documented here. The most basic flow of an external project is:
- Get or update project sources
- Configure project sources
- Build project sources
- Install project binaries
For this example application, we don’t want to install the libraries that are built, just link the static versions so that they are constrained to the project rather than installed on the system somewhere.
We’ll call ExternalProject_Add
, naming our external project project_libsndfile
and set the value of GIT_REPOSITORY
to the location of the libsndfile sources. We’ll also set a PREFIX
of lib/libsndfile
which is a directory that will be created to contain the sources and build output of the library.
The value of CONFIGURE_COMMAND
is the command to perform to configure the build. libsndfile is a make-based project so we set this to <SOURCE_DIR>/configure
which is interpolated for us by CMake with the value of <SOURCE_DIR>
. Ideally we would not build in source, opting instead for a different folder but this caused problems as the libsndfile install process was not able to differentiate between the source and build folders. Enabling BUILD_IN_SOURCE
resolves this issue which is not ideal, but is good enough for now.
Finally we want to skip the install process so we put a no-op to achieve this, giving us a call that looks like this:
There’s one catch: libsndfile has a complex build process that requires a pre-configuration configuration step! To add this custom behaviour, which calls a script called autogen.sh
, we introduce a new step with the CMake command ExternalProject_Add_Step
. We link it to the External Project we added before and give it a name of autogen
. The value of DEPENDEES
tell us which steps need to happen before this one, and DEPENDERS
indicates which steps must wait for this one to complete. The value of COMMAND
contains the location of the target script, and is interpolated for us:
The complexity of libsndfile’s build process is passed on to us in the form of extra tools, which unfortunately are now a dependency of this library. These are:
- autoconf
- autogen
- automake
- libtool
- pkg-config
- python
I was able to obtain all of these on my target operating systems using their most common package mangers:
With these tools installed, we can test that the CMakeLists.txt file generates a working makefile by running:
and then
This will take some time while the libsndfile dependencies are downloaded, configured and built, but when the process is finished, a shiny new static library is available relative to the project root at lib/libsndfile/src/project_libsndfile/src/.libs/libsndfile.a
.
Now we need to make a couple of changes so that the example application can make use of the library. First we’ll extract the BINARY_DIR
property of the libsndfile external project, from where we can derive both the path to static library the library’s post-build headers:
Then we’ll use the ADD_LIBRARY
command to name the library libsndfile
and indicate that it is both static and imported from an external source, setting the IMPORTED_LOCATION
property for this library with the location of the static library.
Lastly we’ll set a variable containing the path to the post-build header files so that we can include them later on as part of the example application build steps:
Somewhat confusingly interpolation here is achieved using the regular CMake variable style ${VARIABLE_NAME}
whereas the ExternalProject_*
commands made use of internal variables of the form <VARIABLE_NAME>
.
That’s just the way it is, kid.
Building Portaudio
Now we understand how to use ExternalProject_Add
, things are a lot simpler. Portaudio has a similar make-based build process, but it’s also simpler than libsndfile’s in that there is no pre-configuration configuration step.
The only other difference for our call to ExternalProject_Add
is that Portaudio’s source controll is Subversion based so we set the value of SVN_REPOSITORY
intentionally to the location of the repo’s trunk and also enable SVN_TRUST_CERT
noting that the Subversion URL is HTTPS:
We don’t need to introduce a custom step like we did to run the autogen.sh
command for libsndfile so we can jump straight to setting up the example project to make use of the Portaudio headers and library.
Portaudio doesn’t do any processing of header files for its build so the headers don’t end up in its binary dir like they do for libsndfile. This means we need to extract both the binary and source directory locations from the Portaudio external project so we can use them in the example project:
Now we have the library location, we can add it to the example project as a statically imported library as we did with libsndfile:
We’re almost there but if we’re building on Linux there’s one more thing we need to do before Portaudio will compile. Portaudio uses platform-specific libraries because interfacing with your computer’s sound card is different for each operating system. The necessary libraries come with Windows and OSX, but with Linux we need to download a couple the development files for two other libraries: Jack and ALSA. I’m using Ubuntu Desktop so was able to run:
We can re-run the build process and verify the libraries are available:
The CMakeLists.txt file now describes how to download libsndfile and Portaudio, build them from source and make them available to our project.
The second part of this guide will concentrate on how to write the example C++ application that makes use of these libraries to play audio back asynchronously.
The source code for this guide can be found here: https://github.com/andystanton/sound-example