chowdsp_wdf: An Advanced C++ Library for Wave Digital Circuit Modelling

chowdsp_wdf is a C++ library for implementing real-time wave digital models of analog circuits. chowdsp_wdf differs from existing wave digital modelling libraries by providing a template meta-programming interface for modelling circuits with a fixed topology, and providing support for explicit SIMD acceleration. The motivation and design of the library are described, as well as real-world use-cases, and performance comparisons with other wave digital modelling libraries.


INTRODUCTION
Wave Digital Filters (WDFs) are a circuit modelling paradigm originally developed by Alfred Fettweis in the 1970's and '80's [1]. WDFs have gained popularity in recent years for developing "virtual analog" (VA) emulations of audio circuits [2]. VA emulations are often implemented as part of software synthesizers or audio effects that are designed to run in real-time, so the run-time performance of these emulations is an important consideration.

Wave Digital Filters Background
While this paper will not attempt to provide a complete introduction to the WDF paradigm, there are several properties of WDFs which should be mentioned. First, WDFs are "modular," meaning that a model of a circuit can be separated into its component parts. This property makes it possible for a wave digital modelling library to implement a single "Resistor" element, which can then be re-used in any wave digital circuit model (and so on with most other circuit elements). These element models may also provide interfaces for changing their parameters (e.g. the resistance value of a resistor).
Wave digital models are comprised of the circuit's element models, along with a set of "adaptors" describing how the circuit elements are connected. Common adaptors include "series" and "parallel" adaptors, as well as polarity inverters. The circuit elements are typically arranged in a tree-like structure, so that each adaptor has a single "parent" element and one or more "child" elements. When processing signals through the wave digital model, each element requires only information that may be provided by it's child elements. However, when the circuit topology changes, or when a component value is changed (e.g. a potentiometer has been adjusted), it is sometimes necessary for an element to signal the change to its parent element.
Finally, rather than using traditional circuit quantities such as voltage and current, WDFs operate on wave variables. Most WDF circuit elements accept a single "incident" wave and output a single "reflected" wave. Adaptors accept N incident waves and output N reflected waves, where N is the total number of child and parent elements connected to the adaptor. The process of computing the output reflected waves from the incoming incident waves for an adaptor may generally be expressed in the form of a scattering matrix, however one-multiply forms of the scattering operation exist for series and parallel adaptors.

PREVIOUS WORK
Several libraries for creating real-time wave digital models exist, including RT-WDF, a C++ library published in 2016 [3], and wdmodels, a Faust library published in 2021 [4]. There are also several libraries not being considered in this paper (e.g. [5]) since they are written in languages which are not well-suited for real-time audio applications, however, these libraries may be useful for prototyping and for performing non-real-time simulations.
RT-WDF is based on C++'s concept of run-time polymorphism. Each wave digital element and adaptor is derived from a base class which provides interfaces for all shared WDF functionality. Additionally, RT-WDF defines the sample rate as a parameter of the constructor for classes that require knowledge of the system sample rate (capacitors, inductors, etc.). Since the system sample rate may not be known at compile-time, circuit elements are typically constructed dynamically using "heap" memory, i.e. with C++'s std::unique_ptr<>. For custom adaptors that require more complex scattering matrices, RT-WDF interfaces with the Armadillo 1 library to perform optimized matrix operations.
wdmodels is written using the Faust programming language. Faust is a functional programming language for expressing audio processing algorithms, which may be compiled into C++, LLVM Bitcode, Rust, and other programming languages. Faust requires the signal flow of the system to be fixed at compilation time, meaning that the topology of the circuit being modelled must be fixed at compile-time as well. Faust also does not provide the programmer with mechanisms for interacting with hardware-level instructions, including SIMD intrinsics (helpful for implementing faster matrix operations for adaptors that require more complex scattering matrices), or bit-twiddling (helpful for implementing approximations of various math functions, see e.g. [6]).

LIBRARY DESIGN
chowdsp_wdf intends to provide the best features of both RT-WDF and wdmodels, with improved performance and flexibility. 2 The library is open-source and is published under the BSD "3-clause" license.

Run-Time Polymorphism
For situations where the circuit topology may need to be changed at run-time, chowdsp_wdf adopts a similar strategy as RT-WDF, using C++'s run-time polymorphism, except that components may have their sample rate changed after construction. This property allows circuit elements to be constructed using "stack" memory rather than "heap" memory, resulting in improved locality of reference [7]. However, the polymorphic approach has significant performance limitations when compared with wdmodels's strategy of defining the circuit topology at compile-time (limiting though it may be). Whenever an adaptor in RT-WDF needs to access some property of one of its children, it must do so through the polymorphic interface of the child's base class, which is typically implemented as a virtual method table or "vtable" [8]. Performing an operation by vtable lookup introduces a small performance overhead and prevents the compiler from performing an inline expansion of the code being executed [9].
The negative performance effects of implementing a wave digital circuit model using run-time polymorphism can be demonstrated by analyzing the assembly instructions generated by an optimizing compiler. When compiling the simple circuit model shown in listing 1 using the Clang 13.1.6 compiler, with the -std=c++20 -O3 compiler flags, the generated assembly for the process() method contains four virtual function call instructions (two for S1 to receive the reflected waves from R1 and R2, and two to send incident waves to those same elements). These virtual function calls add a small amount of computational overhead and prevent the compiler from performing an inline expansion of the code being run behind the call instructions, which then prevents further optimizations from taking place. For larger circuit models containing more circuit elements, these issues become an increasingly significant performance bottleneck.

Optimizations Via Template Meta-Programming
chowdsp_wdf provides an API for constructing wave digital circuit models using compile-time template meta-programming. While this API does not have the flexibility to construct arbitrary circuit models at run-time, it can provide significant performance improvements for cases where the circuit topology is fixed at compiletime. The fundamental idea behind this design choice is that giving the compiler the maximum possible information about the circuit model will allow the compiler to perform the best possible optimizations for that model. In the compile-time API, resistors, capacitors, and most other "one-port" circuit elements are defined almost identically to their run-time counterparts. However, compiletime adaptors are defined with additional template arguments to determine the types of the child elements which are to be connected to the adaptor. Since the adaptor is aware of the types of its child elements at compile-time, all audio processing operations can be performed directly, i.e. without requiring a vtable lookup. Listing 2 shows the same circuit model as listing 1, implemented using chowdsp_wdf's compile-time API. When compiling this code with the same compiler and settings, the generated assembly contains zero call instructions, implying that all the necessary interfaces between the adaptor and its child elements have been inlined by the compiler. A further comparison between the performance of chowdsp_wdf's run-time and compile-time API's is provided in Section 4.

Data Type Abstraction
RT-WDF uses double-precision floating point numbers to store all quantities in the wave digital circuit model. As with all Faust code, wave digital models written with wdmodels use singleprecision floating point values by default, however, the Faust compiler contains optional arguments for using double-precision or quad-precision floating point values instead.
chowdsp_wdf provides a template abstraction for the data type used to store quantities in the circuit. By default, chowdsp_wdf supports C++'s native floating point data types, as well as SIMD wrappers around those floating point data types, via the XSIMD C++ library. 3 The ability to construct wave digital circuit models using SIMD data types creates several interesting optimization opportunities for implementers of wave digital circuit models.

Polyphonic Synthesizer
Consider a polyphonic synthesizer being implemented for a platform that supports Intel SSE or ARM NEON SIMD intrinsics. The synthesizer voices could be implemented using a SIMD data type containing 4 single-precision floating point numbers, providing up to a 4x performance improvement over the same synthesizer using scalar floating point numbers for each voice. Listing 3 shows a minimal example of how such a synthesizer voice might be implemented.

Parallel Circuits
Constructing wave digital circuit models using SIMD data types may also be useful when emulating devices that contain multiple instances of the same sub-circuit. One example of this phenomenon is the Buchla 259 wavefolder [10], which contains 5 instances of the same "folder cell" circuit in parallel with each other. A similar approach could also be used to emulate bucket-brigade device circuits, which contain many capacitor "bucket" circuits in series. For example, a bucket-brigade device with 1024 buckets could be split into 4 parallel sub-circuits each containing 256 buckets, arranged such that the output of the first sub-circuit feeds into the input of the second sub-circuit and so on.

R-Type Adaptors
For circuits with topologies that cannot be broken down strictly into series and parallel connections between components, constructing a wave digital model of the circuit may require the use of an R-Type adaptor [11]. A simple R-Type adaptor may have any number of child elements, and up to one parent element, and uses a scattering matrix to compute the outgoing reflected waves from the incoming incident waves.
chowdsp_wdf contains interfaces for constructing and working with R-Type adaptors, based on the original implementation provided in [12]. In order to update the scattering matrix when a "downstream" component value changes, the R-Type adaptors in the compile-time API are implemented using a "template functor" so that the adaptor can use some custom logic to compute the scattering matrix. Listing 4 demonstrates a simple circuit model containing an R-Type adaptor with 3 child elements and no parent elements. For performing operations with the scattering matrix chowdsp_wdf will optionally use XSIMD to perform SIMDaccelerated matrix operations. Listing 4: A simple circuit model containing an R-Type adaptor.

Deferring Adaptor Updates
For circuits with control parameters, a situation may arises when several component values need to be updated at once. In this situation, some computations may become redundant if an adaptor is alerted multiple times of some changes in "downstream" circuit elements. This redundancy can affect run-time performance, particularly for adaptors that require relatively large computations in response to changes in downstream elements, as is often the case with R-Type adaptors. To mitigate these performance impacts, chowdsp_wdf provides a mechanism for "deferring" adaptor updates until after all downstream elements have been updated.

PERFORMANCE COMPARISONS
In order to compare the run-time performance of each modelling library, a variety of circuit models were implemented with each library. 4 Chosen circuits include a passive second-order RC lowpass filter (LPF2), a sub-circuit from the Klon Centaur guitar pedal (FF-2), an RC diode clipper, and the tone stack circuit from the '59 Fender Bassman guitar amplifier [13]. The LPF2 and FF-2 circuits were chosen as example circuits of varying complexity with wave digital models containing only linear elements and simple adaptors. The diode clipper circuit was chosen as an example circuit containing a single nonlinear element. The Bassman tone stack was chosen as an example circuit requiring an R-Type adaptor. No implementation of the diode clipper circuit was attempted with RT-WDF, since the library does not provide wave domain implementations of diode elements. Each model was also implemented using chowdsp_wdf's compile-time API, as well as chowdsp_wdf's polymorphic run-time API.
A 1000 second long audio signal at a sample rate of 48 kHz was processed through each model implementation, using a 2018 Mac Mini, with a 3.2 GHz Intel Core i7 CPU. The time needed for each model implementation to process the signal was measured, and is shown in table 1. For each circuit model, the chowdsp_wdf polymorphic implementation outperforms the RT-WDF implementation, with the performance difference widening for the more complicated circuits. Between the two model implementations that are fixed at compile-time, chowdsp_wdf's compile-time API outperforms the wdmodels implementation for all circuit models, excepting the FF-2 circuit.
The largest difference in performance between the chowdsp_wdf compile-time API and the wdmodels library can be seen in the diode clipper circuit. Both libraries use diode implementations based on the Werner et al.'s model [14], which requires evaluation of the Lambert W function. The large performance difference can likely be ascribed to the fact that chowdsp_wdf implements the Lambert W with a numerical approximation that utilises floatingpoint bit twiddling [6], while wdmodels uses a Newton-Raphson solver with a fixed number of iterations.

EXAMPLES
For students and engineers looking to learn how to implement circuit models using chowdsp_wdf, a GitHub repository 5 has been created containing models of several simple circuits (voltage divider, current divider, RC lowpass filter, diode clipper, etc.), as well as several more complex audio circuits, such as the Baxandall tone control circuit [15], and the Sallen-Key filter [16]. There are also several open-source examples of chowdsp_wdf being used in audio plugins. 6 ,7

CONCLUSION
chowdsp_wdf is a C++ library for implementing wave digital circuit models, with separate APIs for construct circuit models with run-time polymorphism, or with compile-time template metaprogramming. The library also supports explicit SIMD acceleration, R-Type adaptors, and deferred adaptor updates. Circuit models implemented with chowdsp_wdf typically exhibit superior run-time performance when compared with the same circuit model implemented using other wave digital modelling libraries.
Future work will involve setting up bindings to use the chowdsp_wdf library from other programming languages, including Rust, and Javascript (through the web audio API). Additionally, the author will continue to investigate further performance improvements for the library as whole.

ACKNOWLEDGMENTS
The author would like to thank Jingjie Zhang, Kurt Werner, and Dirk Roosenburg for sharing their knowledge about the theory and implementation of wave digital filters, as well as Sam Schachter for his help with implementing R-Type adaptors. Thanks as well to Eyal Amir and Paul Walker for their crucial insights regarding C++ optimization.