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:

CMAKE_MINIMUM_REQUIRED(VERSION 3.2.2)
PROJECT(sound-example)

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:

INCLUDE(ExternalProject)

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:

  1. Get or update project sources
  2. Configure project sources
  3. Build project sources
  4. 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:

ExternalProject_Add(project_libsndfile
    GIT_REPOSITORY      https://github.com/erikd/libsndfile.git
    PREFIX              lib/libsndfile
    CONFIGURE_COMMAND   <SOURCE_DIR>/configure
    BUILD_COMMAND       make
    BUILD_IN_SOURCE     1
    INSTALL_COMMAND     echo Skipping install step for libsndfile
)

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:

ExternalProject_Add_Step(project_libsndfile autogen
   COMMAND              <SOURCE_DIR>/autogen.sh
   DEPENDEES            update
   DEPENDERS            configure
)

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:

  1. autoconf
  2. autogen
  3. automake
  4. libtool
  5. pkg-config
  6. 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:

$ cmake .

and then

$ make

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:

ExternalProject_Get_Property(project_libsndfile BINARY_DIR)
SET(libsndfile_lib_dir "${BINARY_DIR}/src/.libs")
SET(libsndfile_inc_dir "${BINARY_DIR}/src")

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.

ADD_LIBRARY(libsndfile STATIC IMPORTED)

SET_PROPERTY(TARGET libsndfile PROPERTY
             IMPORTED_LOCATION ${libsndfile_lib_dir}/libsndfile.a)

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:

SET(LIBSNDFILE_INCLUDE_PATH "${install_dir}/src/project_libsndfile-build/src/")

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:

ExternalProject_Add(project_portaudio
    SVN_REPOSITORY      https://subversion.assembla.com/svn/portaudio/portaudio/trunk
    SVN_TRUST_CERT      1
    PREFIX              lib/portaudio
    CONFIGURE_COMMAND   <SOURCE_DIR>/configure
    BUILD_IN_SOURCE     1
    BUILD_COMMAND       make
    INSTALL_COMMAND     echo Skipping install step for portaudio
)

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:

ExternalProject_Get_Property(project_portaudio BINARY_DIR)
ExternalProject_Get_Property(project_portaudio SOURCE_DIR)
SET(portaudio_lib_dir "${BINARY_DIR}/lib/.libs")
SET(portaudio_inc_dir "${SOURCE_DIR}/include")

Now we have the library location, we can add it to the example project as a statically imported library as we did with libsndfile:

ADD_LIBRARY(portaudio STATIC IMPORTED)
SET_PROPERTY(TARGET portaudio PROPERTY IMPORTED_LOCATION "${portaudio_lib_dir}/libportaudio.a")

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:

$ sudo apt-get update
$ sudo apt-get install libjack-dev
$ sudo apt-get install libasound2-dev

We can re-run the build process and verify the libraries are available:

$ cmake .
$ make
$ find -E . -regex '.*(libsndfile|portaudio)\.a'
./lib/libsndfile/src/project_libsndfile/src/.libs/libsndfile.a
./lib/portaudio/src/project_portaudio/lib/.libs/libportaudio.a

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