Introduction
I came into programming from a background in music production and audio engineering. I'd been interested in programming for a long time, but only in the last couple of years have I dug in seriously, working primarily in JavaScript and its libraries and frameworks. I wanted to go deeper and there are so many compelling paths. Of all of them, C++ emerged as a logical next step.
Coming from JavaScript, C++ felt more familiar than I expected. Both derive their basic syntax from C, so control structures like if-else and for loops look familiar. Both support object-oriented programming through classes and methods, and both support functional concepts like first-class functions and closures. But what really caught my attention was noticing the role C++ was playing in audio development. Once I started looking, it kept turning up, in the plugins I use and in the DAWs that host them.
So what makes C++ so widely used in this area? What problems arise in audio development that C++ solves, and what makes it well-suited to solve them?
C++, briefly
C++ was designed by Bjarne Stroustrup at AT&T Bell Labs and had its first commercial release in 1985, as an extension of C with object-oriented features added on top. It has since expanded to include functional features and facilities for low-level memory manipulation. It compiles ahead of time to machine code rather than being interpreted at runtime, and it is standardized internationally as ISO/IEC 14882, maintained by the ISO/IEC working group JTC1/SC22/WG21.
Audio processing and deadlines
Audio processing runs on a fixed deadline. In the browser, for example, the Web Audio API processes sound in blocks of 128 sample-frames, which works out to roughly 3ms of audio at a sample rate of 44.1kHz (128 samples ÷ 44,100 samples per second ≈ 2.9ms). If a block isn't finished before the next one is needed, the hardware is forced to play back stale data, silence, or interpolated samples, resulting in audible clicks, pops, or dropouts. Everything below follows from this constraint.
Garbage collection
Languages with a garbage collector, including JavaScript, clean up unused memory automatically. A background process decides when to run, and while it runs it can pause the whole program. The pause is brief, but its timing and length are unpredictable, and under a deadline of a few milliseconds, the risk of running into even a brief pause is unacceptable.
This is a major problem that C++ solves, as it has no garbage collector. A program allocates and frees memory as needed, happening exactly where the programmer writes them, rather than being left to the timing of a background process. The catch is that allocating can itself take an unpredictable amount of time, so real-time audio code goes a step further and moves every allocation out from under the deadline, doing it all before playback begins.
JUCE, a C++ framework for building audio plugins, builds its plugin structure around this. Its AudioProcessor class gives a plugin two functions with two different jobs. prepareToPlay() runs once, before playback starts, and the host tells it roughly how large the audio blocks are going to be. That's the moment the plugin sets aside all the memory it will need, mainly its buffers, the arrays that hold each block of samples while the plugin works on them. processBlock() then runs over and over during playback, once per block, under the deadline, doing the actual audio processing with memory that already exists. Since nothing in C++ allocates except what the code explicitly allocates, keeping every allocation in prepareToPlay() leaves processBlock() with no hidden step that could stall it, ensuring no calls to new, malloc, or vector reallocations occur during playback.
Avoiding locks on the audio thread
The garbage collection problem above is about one kind of unpredictable pause. Another kind comes from threads.
A thread is a single sequence of instructions that a CPU executes, one after another. A program can run more than one thread at once, each with its own place in the code, while sharing access to the same memory. This is how an audio plugin is built. One thread might run the user interface (drawing knobs, handling clicks), and a separate audio thread is called by the host repeatedly to fill each block of samples. Both run at the same time, possibly on different CPU cores, and both can see the same variables.
This situation is less common in standard JavaScript, where the main execution thread processes one statement at a time via an event loop. While Web Workers and AudioWorklets now allow multi-threading in the browser, the standard model for UI and logic remains single-threaded, whereas C++ audio plugins require parallel threads for UI and DSP from the start. An audio plugin's UI and audio threads run in parallel, and any data they share has to be coordinated.
Two threads reading and writing the same variable at the same time can corrupt it. One way to prevent this is through a mutex, short for mutual exclusion, which is a lock that guarantees only one thread can access a piece of shared data at a time. A thread has to acquire the lock before it can touch the shared data. If another thread already holds the lock, the second thread waits until the first releases it with an unlock operation.
Mutexes are a standard synchronization tool in most multithreaded software, and for good reason. However, for audio code, the wait behavior of a mutex creates a major problem because it has no time limit. When the audio thread needs a lock that the UI thread happens to be holding, and the UI thread gets paused by the OS scheduler or just takes longer than expected, the audio thread sits blocked for as long as that takes, and the deadline passes. This failure mode, where a high-priority thread ends up waiting on a low-priority one, is called priority inversion, and it's the main reason real-time audio code avoids mutexes.
For passing a single value between the two threads, like a plugin parameter, the common alternative to a mutex is an atomic variable. At the lowest level, std::atomic wraps a variable to ensure that read and write operations are indivisible. When you call load() or store() on an atomic variable (like std::atomic<float> as opposed to plain float), the compiler translates this into a single CPU instruction that cannot be interrupted. A plugin's gain parameter, for example, would be declared as a std::atomic<float>. When the user moves the gain knob, the UI thread writes a new value into it. Each time the audio thread runs processBlock() to fill a block of samples, it reads the current value and applies it to the audio.
To ensure this remains lock-free on all hardware and prevents instruction reordering, production code typically uses std::memory_order_release on the write and std::memory_order_acquire on the read.
No mutex is needed because of what "atomic" guarantees. An ordinary write to memory can take more than one CPU operation to complete, and a thread reading at the wrong moment can catch the write partway through, picking up a value that is part old number and part new, a number that was never written. That torn value is the corruption a mutex exists to prevent. An atomic read or write completes as a single, indivisible operation, so the audio thread always gets either the complete old value or the complete new one. The protection the lock provided is built into the operation itself, so there's nothing left to guard and no waiting to do. The UI thread writes whenever the user turns the knob, the audio thread reads whenever it needs to, and neither one ever blocks the other. This approach, coordinating threads without any locks, is called lock-free programming, and it's the standard way code running on the audio thread shares data with the rest of the program. To ensure the CPU doesn't reorder these operations and break the logic, production code typically pairs the write with std::memory_order_release and the read with std::memory_order_acquire.
However, while atomics guarantee thread safety, they do not prevent audio artifacts. If the parameter value changes instantly between samples, it creates a discontinuity known as a 'click.' Therefore, the atomic variable should strictly serve as the target value. Inside the audio loop, the processor must read this target and then smoothly interpolate (or 'slew') the actual processing parameter toward it over a few milliseconds, ensuring the audio signal remains continuous even when the control data changes abruptly.
Direct access to plugin SDKs and OS audio layers
Plugin formats are defined as C++ (or C-ABI-over-C++) interfaces. Steinberg distributes the VST3 SDK directly, built on COM-style interfaces implemented in C++. Avid’s AAX format, used by Pro Tools, expects its plugin developers to work in object-oriented C++. Native Instruments maintains ni-media, an open-source C++ library for reading and writing audio streams, and its NKS standard operates as a metadata specification that enriches standard VST3 and Audio Unit plugins with hardware integration, keeping them on the same C++ foundation.
Below the plugin formats sit the operating systems' own audio layers, which follow a similar hybrid model. On macOS, Core Audio handles the system's audio through pure C programming interfaces, though Apple provides an optional C++ SDK to wrap these APIs for easier development. On Windows, low-latency audio goes through ASIO, a driver protocol developed by Steinberg, with an SDK likewise written in C/C++. A C++ audio engine talks to the plugin host above it and the operating system below it primarily in its own language. However, to ensure stability across different compilers, these interactions cross ABI boundaries (such as COM on Windows or C-interfaces on macOS), acting as a standardized translation layer that allows C++ code to interoperate safely without binary incompatibility.
Layered architecture
Not everything in audio software is written this way, and the exceptions are informative. Cycling '74's Max/MSP is a visual, patch-based environment. Its SDK supplies the headers, source, and libraries for building external objects (plug-ins) in C or C++, so the underlying engine and its high-performance extensions are C/C++, while the patching layer end users work in is not.
Ableton's Max for Live integration adds another layer of abstraction. While the DSP inside a Max device may still rely on C++ (via the Max engine or gen~), controlling the host application itself happens through the Live Object Model. This is accessed via specific Max objects or a JavaScript interface, meaning the plugin author can script complex interactions with Ableton Live without writing a single line of C++.
This demonstrates a consistent industry pattern: C++ dominates the DSP and engine layers where nanosecond timing is critical, while higher-level scripting layers (JavaScript, Python, Lua) are deliberately used for control logic, UI, and arrangement, as these tasks are not bound by the real-time audio deadline.
Shortcomings
Manual memory management brings inherent risks such as forgetting to free memory, freeing it twice, or accessing it after release. These bugs can be subtle and difficult to track down, often requiring rigorous discipline or modern C++ idioms like RAII to mitigate.
C++ also lacks a standardized ABI (Application Binary Interface), meaning the memory layout of a compiled class or function call is not guaranteed to match across different compilers or versions. Plugin formats must design around this limitation. VST3, for instance, separates the audio processing component (IAudioProcessor) from the UI controller (IEditController) behind defined COM-style interfaces. This creates a stable binary contract—a "translation layer"—that allows a host and plugin to communicate safely without relying on raw C++ class layouts at the boundary.
Finally, the language is significantly more verbose than many modern alternatives. It demands a deep understanding of systems programming concepts, resulting in a steeper learning curve and longer development times compared to higher-level languages.
Open Problems
Audio development also faces challenges that the language itself cannot fix.
Format Fragmentation remains a major hurdle. VST3, AU, and AAX are separate, largely incompatible standards, forcing developers to build and test each plugin multiple times. CLAP, an open, MIT-licensed format launched in 2022 by Bitwig and u-he, was created to solve this. Adoption has grown rapidly: Bitwig, REAPER, and FL Studio now fully support it, with Studio One and Cubase following suit. However, industry giants like Ableton Live, Pro Tools, and Logic Pro still do not support it, ensuring a multi-format world for the foreseeable future.
Denormal numbers represent a lower-level hardware quirk. Extremely small floating-point values near zero can force CPUs into a slow "microcode" path, causing massive performance spikes. Preventing this still falls to the programmer—typically by enabling "flush-to-zero" CPU flags or adding a tiny, inaudible noise floor to the signal—rather than being handled automatically by the language or compiler.
Rust is the most frequently named alternative to C++ for this work. Its ownership system guarantees memory safety without a garbage collector, meeting the same real-time requirements as C++. While early frameworks like nih-plug focused on VST3 and CLAP, newer frameworks (e.g., truce) now offer native support for AU and AAX as well. Despite this technical maturity, Rust’s audio ecosystem remains a fraction of C++’s decades-old library collection. Whether it achieves mainstream adoption depends less on the language itself—which already handles the central requirements—and more on how quickly its ecosystem and tooling fill the remaining gaps.
Summary
Audio code has a few milliseconds to produce each block of samples, and that one constraint accounts for most of what this piece covered. A language running on the audio thread can't have a garbage collector pausing it at unpredictable times, can't afford to wait on locks, and benefits from sharing a language with the plugin formats and OS audio layers it connects to. C++ meets all of those conditions, and the industry's foundations, from VST3 and AAX to Core Audio and ASIO to JUCE, are already written in it. Where the deadline doesn't reach, as in Max/MSP's patching layer and Max for Live's JavaScript API, the same industry uses friendlier languages, which says a lot about why C++ holds the real-time layer in particular. The language costs something in return, in memory bugs, binary incompatibility, and time spent learning it. While new formats like CLAP are gaining traction in FL Studio and Studio One, and languages like Rust now offer native AU and AAX support via frameworks like truce, the work on the audio thread still gets written in C++.
Sources: Web Audio API specification, W3C CR-webaudio-20180918 (w3.org/TR); C++ history and ISO/IEC 14882, en.wikipedia.org and stroustrup.com; JUCE AudioProcessor class reference (docs.juce.com); MDN Web Docs, "JavaScript execution model" and "Concurrency model and Event Loop" (developer.mozilla.org); Ross Bencina, "Real-time audio programming 101: time waits for nothing" (rossbencina.com); Fabian Renn-Giles, "Using locks in real-time audio processing, safely" (timur.audio); Steinberg VST3 SDK (github.com/steinbergmedia/vst3sdk) and VST3 architecture documentation (deepwiki.com/steinbergmedia/vst3_public_sdk); Avid AAX developer/alliance documentation (developer.avid.com); Native Instruments GitHub (ni-media) and NKS developer page (native-instruments.com); Apple Core Audio Overview and Core Audio Essentials (developer.apple.com/library/archive); Steinberg ASIO driver documentation (helpcenter.steinberg.de); Cycling '74 max-sdk GitHub repository; Ableton Help Center, "Controlling Live using Max for Live" (help.ableton.com); CLAP announcement (bitwig.com) and CLAP adoption status (producergrid.com); Rust ownership documentation (doc.rust-lang.org/book); nih-plug ProducerGrid, (CLAP Plugin Format: Everything Music Producers Need to Know in 2026); truce.audio Documentation and GitHub Repository (truce-audio/truce); Intel® 64 and IA-32 Architectures Optimization Reference Manual (Section on MXCSR/FTZ/DAZ flags) or Stack Overflow, "Flushing denormalised numbers to zero".

Top comments (0)