A cross platform C++ audio driver with low latency.
- 📱 iOS 9.0+
- 💻 OS X 10.11+
- 🐧 Ubuntu Bionic 18.04+ (clang 3.9 or gcc 7.5)
- 🤖 Android NDK r17b+
- 🖥️ Microsoft Windows Store 10
Developed at Spotify 2019-2022, Discontinued and handed over to new maintainers January 2023
During the development of innovative new audio experiences, we required a driver that would reliably work on a number of different platforms for our experimentation purposes. We noticed that at the time of development no such open source software existed (that managed to support all the platforms we were looking for), so we decided to create a new one. Given that the common language we could use across our experimentation platforms was C++ we decided on that as the language of choice for our interface. It is also worth noting that this wasn't just designed for front end use cases, and as such has the ability to write out WAV files to disk at above real time speeds to support backend rendering use cases.
NFDriver
is designed as a common C++ interface to write information to different systems sound drivers in a low latency. The API simply allows you to create a driver that will then call the callbacks fed into it every time a new block of audio data is requested. It uses very basic C functions in order to reduce the amount of latency when interfacing to it, and to prevent unwanted locks in some implementations of the C++ 11 STL. It always has a fixed block size of 1024 samples it will ask for at any one time. It also has the ability to report errors, stutters, and give callbacks before and after the rendering of a block.
You may notice it has a fixed samplerate and number of channels. This was done due to this being the standard configuration in music output, so in order to lower the complexity of the API and the way each wrapper acts with the system we decided to hardcode these values.
It is designed with 2 major layers:
- The Normalisation layer, which takes input and resamples it to whatever channel format and samplerate the driver expects.
- The System Layer, which interfaces to the operating systems audio drivers.
Our support table looks like so:
OS | Underlying Framework | Status |
---|---|---|
iOS | Core Audio | Stable |
OSX | Core Audio | Stable |
Linux | ALSA | Stable |
Android | OpenSL ES | Beta |
Windows | Media Foundation | Alpha |
In terms of bouncing to files, our support table looks like so:
Format | Options | Comments | Support |
---|---|---|---|
WAV | wavsize : int | Writes a WAV file to the output destination. | iOS, OSX, Linux, Android, Windows |
MP3 | bitrate : int | Writes an MP3 file to the output destination. | iOS, OSX, Linux, Android |
AAC | bitrate : int | Writes an AAC file to the output destination. | iOS, OSX |
Note that using MP3 will require you to define the environment variable LAME_DYLIB
. Make sure you allow your users the option to replace this library to comply with its LGPL License. We do not statically link against LAME so we do not take on its LGPL status and retain our MIT license.
NFDriver
is a cmake project, while you can feel free to download the prebuilt static libraries it is recommended to use cmake to install this project into your wider project. In order to add this into a wider Cmake project, simply add the following line to your CMakeLists.txt
file:
add_subdirectory(NFDriver)
Generate an Xcode project from the Cmake project like so:
$ mkdir build
$ cd build
$ cmake .. -GXcode
Generate a Ninja project from the Cmake project like so:
$ mkdir build
$ cd build
$ cmake .. -GNinja
Use gradle to include the NFDriver project like so:
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.spotify.nfdrivertest_android"
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_APP=1 -DANDROID=1"
}
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/cpp']
}
}
externalNativeBuild {
cmake {
path "../CMakeLists.txt"
}
}
}
Generate a Visual Studio project from the Cmake project like so:
$ mkdir build
$ cd build
$ cmake .. -G "Visual Studio 12 2013 Win64"
For examples of this in use, see the demo program src/cli/NFDriverCLI.cpp
. The API is rather small, it basically has a create function, and a stop/start interface on the created class. You feed the create function with the necessary callbacks used for inputting audio data and then press play.
NF_STUTTER_CALLBACK stutter_callback = [](void *clientdata) {
printf("Driver Stuttered...\n");
};
NF_RENDER_CALLBACK render_callback = [](void *clientdata, float *frames, int numberOfFrames) {
const float samplerate = 2000.0f;
const float multiplier = (2.0f * float(M_PI) * samplerate) / float(NF_DRIVER_SAMPLERATE);
static unsigned int sinewave = 0;
for (int n = 0; n < numberOfFrames; n++) {
float audio = sinf(multiplier * sinewave++);
for (int i = 0; i < NF_DRIVER_CHANNELS; ++i) {
*frames++ = audio;
}
}
return numberOfFrames;
};
NF_ERROR_CALLBACK error_callback = [](void *clientdata, const char *errorMessage, int errorCode) {
printf("Driver Error (%d): %s\n", errorCode, errorMessage);
};
NF_WILL_RENDER_CALLBACK will_render_callback = [](void *clientdata) {};
NF_DID_RENDER_CALLBACK did_render_callback = [](void *clientdata) {};
NFDriver *driver = nativeformat::driver::createDriver(nullptr /* Anything client specific to use in the callbacks */,
stutter_callback /* called on stutter */,
render_callback /* called when we have samples to output */,
error_callback /* called upon driver error */,
will_render_callback /* Called before render_callback */,
did_render_callback /* Called after render callback */,
nativeformat::driver::OutputTypeSoundCard);
driver->setPlaying(true);
The above will output a sine wave at 2kHz on the audio card.
Contributions are welcomed, have a look at the CONTRIBUTING.md document for more information.
The project is available under the Apache 2.0 license.
- Icon in readme banner is “Audio” by Becris from the Noun Project.