C++17 library for creating macOS Audio Server plugins.

Overview

libASPL Build Doxygen License

Synopsis

libASPL (Audio Server PLugin library) is a C++17 library helping to create macOS CoreAudio Audio Server Plug-In (a.k.a User-Space CoreAudio Driver) with your custom virtual device.

The library acts as a thin shim between Audio Server and your code and takes care of the boilerplate part:

  • Instead of implementing dynamic property dispatch and handling dozens of properties, you inherit classes provided by this library and override statically typed getters and setters that you're interested in.

  • Instead of coping with Core Foundation types like CFString, you mostly work with convenient C++ types like std::string and std::vector.

  • All properties have reasonable default implementation, so typically you need to override only a small subset which is specific to your driver.

  • The library does not hide any Audio Server functionality from you. If necessary, you can customize every aspect of the plugin by overriding correspoding virtual methods.

  • The library also does not introduce any new abstractions. Audio Server properties and callbacks are mapped almost one-to-one to C++ methods.

  • As a bonus, the library performs verbose tracing of everything that happens with your driver. The output and the format of the trace can be customized.

Instructions

Install recent CMake:

brew install cmake

Clone project:

git clone https://github.com/gavv/libASPL.git
cd libASPL

Build and install into /usr/local (headers, static library, and cmake package):

make
sudo make install

You can do more precise configuration by using CMake directly:

mkdir -p build/Release
cd build/Release
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/my/install/path ../..
make -j4
make install

Versioning

The library uses semantic versioning.

Only source-level compatibility is maintained. There is no binary compatibility even between minor releases. In other words, if you update the library, you should recompile your code with the new version.

API reference

Doxygen-generated documentation is available here.

Example driver

A complete standalone example driver with comments can be find in example directory.

You can build it using:

make example [CODESIGN_ID=...]

You can then (un)install driver into the system with:

sudo ./example/install.sh [-u]

The device should appear in the device list:

$ system_profiler SPAudioDataType
Audio:

    Devices:

        ...

        Example Device:

          Manufacturer: libASPL
          Output Channels: 2
          Current SampleRate: 44100
          Transport: Virtual
          Output Source: Default

You can also gather driver logs:

log stream --predicate 'sender == "ASPL_Example"'

Or, for more compact output:

log stream --predicate 'sender == "ASPL_Example"' | sed -e 's,.*\[aspl\],,'

The example driver sends sound written to it via UDP to 127.0.0.1:4444. The following command receives 1M samples, decodes them, and converts to a WAV file:

nc -u -l 127.0.0.1 4444 | head -c 1000000 | sox -t raw -r 44100 -e signed -b 16 -c 2 - test.wav

Quick start

Add to CMake project using ExternalProject

ExternalProject_Add(libASPL
  URL "https://github.com/gavv/libASPL.git"
  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libASPL-src
  BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/libASPL-build
  INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/libASPL-prefix
  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
  )

target_include_directories(YourDriver
    PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/libASPL-prefix/include
  )
target_link_libraries(YourDriver
    PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/libASPL-prefix/lib/libASPL.a
  )

Add to CMake project using FindPackage

# if libASPL is pre-installed into the system:
find_package(libASPL REQUIRED)

# if libASPL is pre-installed into a directory:
find_package(libASPL REQUIRED
  PATHS /your/install/directory
  NO_DEFAULT_PATH
  )

target_include_directories(YourDriver PRIVATE aspl::libASPL)
target_link_libraries(YourDriver PRIVATE aspl::libASPL)

Minimal driver with no-op device

std::shared_ptr<aspl::Driver> CreateDriver()
{
    auto context = std::make_shared<aspl::Context>();

    auto device = std::make_shared<aspl::Device>(context);
    device->AddStreamWithControlsAsync(aspl::Direction::Output);

    auto plugin = std::make_shared<aspl::Plugin>(context);
    plugin->AddDevice(device);

    return std::make_shared<aspl::Driver>(context, plugin);
}

extern "C" void* EntryPoint(CFAllocatorRef allocator, CFUUIDRef typeUUID)
{
    if (!CFEqual(typeUUID, kAudioServerPlugInTypeUUID)) {
        return nullptr;
    }

    static std::shared_ptr<aspl::Driver> driver = CreateDriver();

    return driver->GetReference();
}

Handler for control and I/O requests

class MyHandler : public aspl::ControlRequestHandler, public aspl::IORequestHandler
{
public:
    // Invoked on control thread before first I/O request.
    OSStatus OnStartIO() override
    {
        // prepare to start I/O
        return kAudioHardwareNoError;
    }

    // Invoked on control thread after last I/O request.
    void OnStopIO() override
    {
        // finish I/O
    }

    // Invoked on realtime I/O thread to read data from device to client.
    virtual void OnReadClientInput(const std::shared_ptr<Client>& client,
        const std::shared_ptr<Stream>& stream,
        Float64 zeroTimestamp,
        Float64 timestamp,
        void* buff,
        UInt32 buffBytesSize)
    {
        // fill data for client
    }

    // Invoked on realtime I/O thread to write mixed data from clients to device.
    void OnWriteMixedOutput(const std::shared_ptr<aspl::Stream>& stream,
        Float64 zeroTimestamp,
        Float64 timestamp,
        const void* buff,
        UInt32 buffBytesSize) override
    {
        // handle data from clients
    }
};

device->AddStreamWithControlsAsync(aspl::Direction::Input);
device->AddStreamWithControlsAsync(aspl::Direction::Output);

auto handler = std::make_shared<MyHandler>();

device->SetControlHandler(handler);
device->SetIOHandler(handler);

Streams and controls

If you want to configure streams and controls more precisely, then instead of:

device->AddStreamWithControlsAsync(aspl::Direction::Output);

you can write:

auto stream = device->AddStreamAsync(aspl::Direction::Output);

auto volumeControl = device->AddVolumeControlAsync(kAudioObjectPropertyScopeOutput);
auto muteControl = device->AddMuteControlAsync(kAudioObjectPropertyScopeOutput);

stream->AttachVolumeControl(volumeControl);
stream->AttachMuteControl(muteControl);

Furthermore, all AddXXX() methods have overloads that allows you to specify custom parameters or provide manually created object. The latter is useful if you want to use your own subclass instead of default implementation.

Tracing

Disable tracing to syslog:

auto tracer = std::make_shared<aspl::Tracer>(aspl::Tracer::Mode::Noop);
auto context = std::make_shared<aspl::Context>(tracer);

// pass context to all objects

Provide custom tracer:

class MyTracer : public aspl::Tracer
{
protected:
    void Print(const char* message) override
    {
        // ...
    }
};

auto tracer = std::make_shared<MyTracer>();
auto context = std::make_shared<aspl::Context>(tracer);

Object model

Typical AudioServer Plug-In consists of the following components:

  • Factory function, which name is defined in Info.plist, and which should return the function table of the driver.
  • Driver, which consists of the audio object tree and the function pointers table with operations on objects.
  • Audio object tree, consisting of objects such as plugin, device, stream, etc., organized in a tree with the plugin object in the root.

The Audio Server invokes the factory function to obtain the function pointer table of the driver and then issues operations on driver's audio objects.

Each audio object has the following characteristics:

  • it has a numeric identifier, unique within a driver
  • it belongs to one of the predefined audio object classes, also identified by numeric identifiers
  • it has a list of owned audio objects (e.g. plugin owns devices, device owns streams, etc.)
  • it has a set of properties that can be read and written
  • in case of some objects, it has some additional operations (e.g. device has I/O operations)

This diagram shows driver and audio object tree in libASPL:

Audio object classes are organized in a hierarchy as well, with the AudioObject class in the root of the hierarchy. For example, AudioVolumeControl class inherits more genric AudioLevelControl, which inherits even more generic AudioControl, which finally inherits AudioObject.

Audio objects "classes" are more like "interfaces": they define which properties and operations should be supported by an object. Inheritance means supporting of all properties and operations of the parent class too.

In libASPL, there are C++ classes corresponding to some of the leaf audio object classes. All of them inherit from aspl::Object which corresponds to AudioObject and provide basic services liker identification, ownership, property dispatch, etc.

This diagram shows audio object classes and and corresponding libASPL classes.

Note that the classes in the middle of the tree may not have corresponding C++ classes, e.g. there is aspl::VolumeControl for AudioVolumeControl, but there are no C++ classes for AudioLevelControl and AudioControl. Instead, aspl::VolumeControl implements everything needed.

All objects inherited from aspl::Object are automatically registered in aspl::Dispatcher. Driver uses it to find object by identifier when it redirects Audio Server operations to C++ objects.

Types of setters

libASPL objects provide setters of two types:

  • Synchronous setters, named SetXXX().

    Such setters are used for properties which are allowed to be changed on fly at any point. They typically do two things: call protected virtual method SetXXXImpl() to actually change the value (you can override it), and then notify HAL that the property was changed.

  • Asynchronous setters, named SetXXXAsync().

    Such setters are used for properties that can't be changed at arbitrary point of time, but instead the change should be negotiated with HAL. They typically request HAL to schedule configuration change. When the time comes (e.g. after I/O cycle end), the HAL invokes the scheduled code, and the change is actually applied by invoking protected virtual method SetXXXImpl() (which you can override).

Note 1: if you invoke asynchronous setter before you've published plugin to HAL by returning driver from the entry point, the setter applies the change immediately without invloving HAL, because it's safe to change anything at this point.

Note 2: if you invoke an asynchronous setter while you're already applying some asynchronous change, i.e. from some SetXXXImpl() method, again the setter applies the change immediately without scheduling it, because we're already at the point where it's safe to apply such changes.

Customization

The library allows several ways of customization.

Builtin properties:

  1. Each object (Plugin, Device, Stream, etc.) can be provided with the custom config at construction time (PluginParamateres, DeviceParameters, etc), which defines initial values of the properties.

  2. Each object also provides setters for the properties that can be changed on fly.

  3. If desired, you can subclass any object type (Plugin, Device, etc.) and override statically typed getters and setters. Dynamic property dispatch will automatically use overridden versions.

  4. For even more precise control, you can override dynamic dispatch methods themselves (HasProperty, GetPropertyData, etc).

Custom properties:

  1. Each object allows to register custom properties, i.e. properties not known by HAL but available for apps. Such properties support only limited number of types.

  2. Again, for more precise control of custom properties, you can override dynamic dispatch methods (same as for builtin properties).

Control and I/O requests:

  1. You can provide custom implementations of ControlRequestHandler and IORequestHandler. Device will invoke their methods when serving requests from HAL.

  2. You can also provide custom implementation of Client if you want to associate some state with every client connected to device. See ControlRequestHandler for details.

  3. Finally, for more precise control of request handling, you can subclass Device and override its control and I/O methods directly (StartIO, StopIO, WillDoIOOperation, etc.).

Thread and realtime safety

An operation is thread-safe if it can be called from concurrent threads without a danger of a data race.

An operation is realtime-safe if it can be called from realtime threads without a danger of blocking on locks held by non-realtime threads. This problem is known as priority inversion.

In CoreAudio, I/O is performed on realtime threads, and various control operations are performed in non-realtime threads. As a plugin implementer, you should ensure that all code is thread-safe and I/O operations are also realtime-safe. Every non-realtime safe call from an I/O handler would increase probability of audio glitches.

To help with this, libASPL follows the following simple rules:

  • All operations are thread-safe.

  • All operations that don't modify object state, e.g. all GetXXX() and ApplyProcessing() methods, are also non-blocking and realtime-safe. You can call them from any thread. This, however, excludes getters that return STL strings and containers.

  • All operations that modify object state, e.g. all SetXXX(), AddXXX(), and RegisterXXX() methods, are allowed to block and should be called only from non-realtime threads. So avoid calling them during I/O.

  • All operations that are invoked on realtime threads are groupped into a single class IORequestHandler. So typically you need to be careful only when overriding methods of that class.

When overriding libASPL methods, you can either follow the same rules for simplicity, or revise each method you override and make sure it's realtime-safe if there are paths when it's called on realtime threads.

Note that various standard library functions, which implicitly use global locks shared among threads, are typically not realtime-safe. Some examples are: everything that allocates or deallocates memory, including copy constructors of STL containers; stdio functions; atomic overloads for shared_ptr; etc. Basically, you need to carefully check each function you call.

Internally, realtime safety is achieved by using atomics and double buffering combined with a couple of simple lock-free algorithms. There is a helper class aspl::DoubleBuffer, which implements a container with blocking setter and non-blocking lock-free getter. You can use it to implement the described approach in your own code.

Sandboxing

AudioServer plugin operates in its own sandboxed process separate from the system daemon.

These things are no specific to libASPL, but worth mentioning:

  • Filesystem access is restricted. You can access only your own bundle, plus system libraries and frameworks.

  • IPC is allowed, including semaphores and shared memory. The plugin should list the mach services to be accessed in Info.plist.

  • Networking is allowed, including sockets and syslog. The plug-in should declare this in Info.plist as well.

  • IOKit access is allowed too.

Missing features

Below you can find the list of things that are NOT supported out of the box because so far authors had no need for any of them.

Element-related properties are currently not supported (ElementName, ElementCategoryName, ElementNumberName).

Currently unsupported device-related object types:

  • AudioBox
  • AudioClockDevice
  • AudioTransportManager
  • AudioEndPoint
  • AudioEndPointDevice

Currently unsupported control object types:

  • AudioClipLightControl
  • AudioClockSourceControl
  • AudioDataDestinationControl
  • AudioDataSourceControl
  • AudioHighPassFilterControl
  • AudioJackControl
  • AudioLFEMuteControl
  • AudioLFEVolumeControl
  • AudioLineLevelControl
  • AudioListenbackControl
  • AudioPhantomPowerControl
  • AudioPhaseInvertControl
  • AudioSelectorControl
  • AudioSoloControl
  • AudioTalkbackControl

If you need one of these, you can either implement it on your side, or submit a patch.

To implement a new object type on your side, you need to derive Object class and manually implement dynamic dispatch methods (HasProperty, IsPropertySettable, etc.).

To prepare a patch that adds support for a new object type, you also need to derive Object class, but instead of implementing dynamic dispatch by hand, provide a JSON file with properties description. The internal code generation framework (see below) will do the rest of the job.

Apple documentation

Reading documentation for underlying Apple interfaces can help when working with libASPL. The following links can be useful.

Official examples:

Headers with comments:

Code generation

To solve the issue with lots of the boilerplate code needed for a plugin, libASPL extensively uses code generation.

There are three code generators:

  • script/generate-accessors.py - reads JSON description of object's properties and generates C++ code for dispatching dynamic HAL requests to statically typed getters and setters
  • script/generate-bridge.py - reads JSON description of C plugin interface and generates C++ code for dispatching HAL calls to corresponding C++ objects calls
  • script/generate-strings.py - reads CoreAudio header files and generates C++ code to convert various identifiers to their string names

All of the generators are created using excelent Jinja Python module.

See CMake scripts for details on how the generators are invoked. The generated files are added to the repo. Unless you modify the sources, CMake wont regenerate them and there is no need to install Jinja.

Hacking

Install tools for code generation (needed if you modify sources):

brew install python3
pip3 install jinja2

Run release or debug build:

make [release_build|debug_build]

Build and run tests:

make test

Remove build results:

make clean

Also remove generated files:

make clobber

Format code:

make fmt

Contributions are welcome!

Authors

See here.

License

The library is licensed under MIT.

This library is mostly written from scratch, but is inspired by and borrows some pieces from "SimpleAudio" and "NullAudio" plugins from newer and older Apple documentation.

Apple examples are licensed under MIT and Apple MIT licenses.

You might also like...
a library for audio and music analysis
a library for audio and music analysis

aubio is a library to label music and sounds. It listens to audio signals and attempts to detect events. For instance, when a drum is hit, at which frequency is a note, or at what tempo is a rhythmic melody.

A discord audio playback library in c++ with node bindings

Tokio This project is still WIP. C++ Discord library with node bindings focused on audio playback. The Core parts as networking with discord/audio dec

VINE Audio Processing Library Standard Edition

audiolib VINE Audio Processing Library Standard Edition Standard 버전에서는 AGC (Automaic Gain Control)기능을 제공합니다. AGC는 Mic와 화자 간 거리에 따라 자동으로 송신음량을 최적화하는 기능

Audio File Library

Audio File Library

C++ library for audio and music analysis, description and synthesis, including Python bindings

Essentia Essentia is an open-source C++ library for audio analysis and audio-based music information retrieval released under the Affero GPL license.

Single file C library for decoding MPEG1 Video and MP2 Audio

PL_MPEG - MPEG1 Video decoder, MP2 Audio decoder, MPEG-PS demuxer Single-file MIT licensed library for C/C++ See pl_mpeg.h for the documentation. Why?

A simple and easy-to-use audio library based on miniaudio

raudio A simple and easy-to-use audio library based on miniaudio raudio forks from raylib.audio module to become an standalone library. Actually, it w

Oboe is a C++ library that makes it easy to build high-performance audio apps on Android.
Oboe is a C++ library that makes it easy to build high-performance audio apps on Android.

Oboe Oboe is a C++ library which makes it easy to build high-performance audio apps on Android. It was created primarily to allow developers to target

Cross platform C++11 library for decoding audio (mp3, wav, ogg, opus, flac, etc)

Libnyquist is a small C++11 library for reading sampled audio data from disk or memory. It is intended to be used an audio loading frontend for games, audio sequencers, music players, and more.

Comments
  • Device shows no formats available / not listed in Logic Pro

    Device shows no formats available / not listed in Logic Pro

    Hey @gavv

    Thanks so much for the work here. This has been so helpful for my project.

    Right now, though, I'm really focused on getting the example working.

    I can build it. I can install it. It shows up in system settings. It shows up in OBS.

    However it does not show up in Logic Pro, and in Audio MIDI Setup, it lists "No valid formats available".

    What am I doing wrong here?

    Screen Shot 2021-12-18 at 11 49 44 PM
    opened by mattahorton 14
  • Help Creating a Loopback Device

    Help Creating a Loopback Device

    Hi @gavv (and other authors),

    First, congratulations on the fantastic job! This library is awesome! I’m getting my feet wet in C++ and lower-level programming (I’m mostly used to web development, programming-language theory, and so forth), and I was getting a bit frustrated with the amount of complexity in https://developer.apple.com/documentation/coreaudio/creating_an_audio_server_driver_plug-in, but then I found libASPL and managed to get close to a working prototype in a couple hours 👏

    I’d love if you could give me a couple pointers to continue.

    I’m building a loopback device. Similar to https://github.com/ExistentialAudio/BlackHole, https://github.com/mattingalls/Soundflower, https://rogueamoeba.com/loopback/, and so forth. Here’s as far as I’ve managed to go:

    Code
    #include <aspl/Driver.hpp>
    
    namespace {
    class Loopback : public aspl::IORequestHandler
    {
    public:
        void OnWriteMixedOutput(const std::shared_ptr<aspl::Stream>& stream,
            Float64 zeroTimestamp,
            Float64 timestamp,
            const void* bytes,
            UInt32 bytesCount) override
        {
            for (auto bytesIndex = 0u; bytesIndex < bytesCount; bytesIndex++) {
                circularBuffer[circularBufferWriteIndex] =
                    reinterpret_cast<const UInt8*>(bytes)[bytesIndex];
                circularBufferWriteIndex++;
                if (circularBufferWriteIndex == circularBufferSize)
                    circularBufferWriteIndex = 0;
            }
        }
    
        void OnReadClientInput(const std::shared_ptr<aspl::Client>& client,
            const std::shared_ptr<aspl::Stream>& stream,
            Float64 zeroTimestamp,
            Float64 timestamp,
            void* bytes,
            UInt32 bytesCount) override
        {
            for (auto bytesIndex = 0u; bytesIndex < bytesCount; bytesIndex++) {
                reinterpret_cast<UInt8*>(bytes)[bytesIndex] =
                    circularBuffer[circularBufferReadIndex];
                circularBufferReadIndex++;
                if (circularBufferReadIndex == circularBufferSize)
                    circularBufferReadIndex = 0;
            }
        }
    
    private:
        UInt8 circularBuffer[48000 * 100];
        UInt32 circularBufferSize = 48000 * 100;
        UInt32 circularBufferWriteIndex = 0;
        UInt32 circularBufferReadIndex = 0;
    };
    } // namespace
    
    extern "C" void* EntryPoint(CFAllocatorRef allocator, CFUUIDRef typeUUID)
    {
        if (!CFEqual(typeUUID, kAudioServerPlugInTypeUUID))
            return nullptr;
    
        auto context = std::make_shared<aspl::Context>();
    
        aspl::DeviceParameters deviceParams;
        deviceParams.Name = "Loopback";
        deviceParams.SampleRate = 48000;
        auto device = std::make_shared<aspl::Device>(context, deviceParams);
        device->AddStreamAsync(aspl::Direction::Input);
        device->AddStreamAsync(aspl::Direction::Output);
        device->SetIOHandler(std::make_shared<Loopback>());
    
        auto plugin = std::make_shared<aspl::Plugin>(context);
        plugin->AddDevice(device);
    
        static auto driver = std::make_shared<aspl::Driver>(context, plugin);
    
        return driver->GetReference();
    }
    

    To my surprise, audio is getting in and coming back out! 😁

    But there are plenty of things I don’t understand yet:

    1. Am I right in thinking that OnWriteMixedOutput() is called once per device, and that OnReadClientInput() is called once per client?

    2. If so, should I have one circularBufferReadIndex per client?

    3. Right now there’s a huge time gap between audio going in and coming back up. I suppose that’s because circularBufferWriteIndex and circularBufferReadIndex are out of alignment. So perhaps I shouldn’t have circularBufferReadIndexs at all? But then how would I keep track of where each client should be in the circularBuffer?

    4. Doing all this circular buffer management by hand seems silly. For one thing, I suppose it’s far from being thread-safe. What data structure implementation do you recommend? Is this what libASPL’s DoubleBuffer is for?

    5. My plan is to have a way for the user to create devices dynamically. I suppose it’s okay to create devices at any time and just AddDevice() and RemoveDevice() them as needed, right?

    6. From what I read in libASPL’s README & CoreAudio/AudioServerPlugIn.h the plugin runs in a sandbox. What’s the best way to communicate with the plugin to add/remove devices? I was thinking of spinning up an HTTP server listening on a socket in the temporary directory right from the plugin process. Is this even viable? Is there a better idea?

    7. Still related to the sandbox: How do I store the devices that should be created so that configuration is persisted across runs of Core Audio? Should I use the so-called “Storage Operations” in CoreAudio/AudioServerPlugIn.h? Is there an abstraction for it in libASPL that I couldn’t find?

    8. How do I control bit depth? DeviceParameters has a way of controlling the sample rate, but not the bit depth…

    9. Can libASPL synchronize the clock with other devices or will I run into issues similar to BlackHole?

    Thank you very much in advance.

    opened by leafac 1
Releases(v2.0.0)
Owner
Victor Gaydov
Victor Gaydov
Tenacity is an easy-to-use, cross-platform multi-track audio editor/recorder for Windows, MacOS, GNU/Linux

Tenacity is an easy-to-use, cross-platform multi-track audio editor/recorder for Windows, MacOS, GNU/Linux and other operating systems and is developed by a group of volunteers as open source software.

null 59 Jan 1, 2023
An Audio-For-VATSIM ATC Client for macOs and Linux

An Audio-For-VATSIM ATC Client for macOs and Linux (audio only)

Pierre Ferran 27 Dec 27, 2022
PortAudio is a portable audio I/O library designed for cross-platform support of audio

PortAudio is a cross-platform, open-source C language library for real-time audio input and output.

PortAudio 786 Jan 1, 2023
This is a library for creating a MIDI controller using an Arduino or Teensy board.

MIDI controller This is a library for creating a MIDI controller using an Arduino board. It enables you to easily create MIDI controllers or instrumen

Pieter P 361 Dec 27, 2022
A simple C++ library for reading and writing audio files.

AudioFile A simple header-only C++ library for reading and writing audio files. Current supported formats: WAV AIFF Author AudioFile is written and ma

Adam Stark 683 Jan 4, 2023
A C library for reading and writing sound files containing sampled audio data.

libsndfile libsndfile is a C library for reading and writing files containing sampled audio data. Authors The libsndfile project was originally develo

null 1.1k Jan 2, 2023
C library for cross-platform real-time audio input and output

libsoundio C library providing cross-platform audio input and output. The API is suitable for real-time software such as digital audio workstations as

Andrew Kelley 1.6k Jan 6, 2023
C++ Audio and Music DSP Library

_____ _____ ___ __ _ _____ __ __ __ ____ ____ / \\_ \\ \/ / |/ \| | | | \_ \/ \ | Y Y \/ /_ \> <| | Y Y \ | |_|

Mick Grierson 1.4k Jan 7, 2023
Single file audio playback and capture library written in C.

A single file library for audio playback and capture. Example - Documentation - Supported Platforms - Backends - Major Features - Building - Unofficia

David Reid 2.6k Jan 8, 2023