C++ Cross-Platform, Object-Oriented, Flow-Based Programming Library
DSPatch, pronounced "dispatch", is a powerful C++ flow-based programming library that allows you to create and route (or "patch") high performance data processing circuits. DSPatch is not limited to any particular type of circuit or signal, its generic object-oriented API allows you to create almost any system imaginable, from simple logic circuits to high performance audio process chains and electronics simulation. DSPatch's simple framework makes development quick and easy, allowing you to hit the ground running on every project.
DSPatch is designed around the concept of a "circuit" containing "components", interconnected via "wires" that transfer "signals" to and from input and output "buses". For more detail on how DSPatch works, check out the DSPatch Design Specification.
The two most important classes to consider are DspComponent and DspCircuit. In order to route data to and from DspComponents they can either be added to an DspCircuit, where they can be wired together (recommended), or they can be wired directly via public DspComponent methods.
The DSPatch engine takes care of data transfer between interconnected components, when data is ready for a component to process, a callback: "Process_()" is executed in that component. For a component to form part of the DSPatch framework, designers simply have to derive their component from the DspComponent base class, configure the component's IO buses, and implement the virtual Process_() callback method.
The "Tutorials" section below covers 2 vital aspects to developing with DSPatch:
In the "example" folder (in the DSPatch root directory) you will find a DSPatch demo project, written to assist developers in understanding the DSPatch API as well as to demonstrate how it could be used to build audio process chains. This project uses the open-source library RtAudio in order to stream sound to your computer's audio device -Linux users will need to install "libasound2-dev" (ALSA) before attempting to build.
A fast way to create your own DspComponent could be to copy an existing component to another destination, rename it, and edit the contents to satisfy your component's required behavior. In the "example" folder (from Step 3) you will find 2 source files: "DspAdder.h" and "DspAdder.cpp". These files make up a very simple DspComponent that receives 2 floating-point buffers into it's 2 inputs, adds each buffer element of the 1st buffer to the corresponding element of the 2nd buffer, then passes the resultant buffer to the output. Alternatively, you could just copy / reference the source code from the "Creating a DspComponent" tutorial (found under the "tutorial" folder in the root directory).
As DSPatch is not reliant on any non-standard 3rd party sources, getting a DSPatch project to compile and run is relatively painless. All you need to do from your project is include "DSPatch.h" from the "include" folder (in the DSPatch root directory), and link to the DSPatch library (either by including all DSPatch source or by linking to a compiled library file). To speed things up you may want to copy, rename, and edit the example project from step 3 to get up and running faster.
Between the example project, the DspAdder component template, and the documentation found here, you should have enough resources to get started with DSPatch straight away. If you have any questions or wish to report a bug, feel free to email me at firstname.lastname@example.org@email@example.com@m.dio..firstname.lastname@example.org.
Lets take a look at how we would go about creating a boolean logic "AND" component. This component will accept 2 boolean input values and output the result of: input 1 && input 2.
We begin by deriving our new "DspAnd" component from DspComponent:
The next step is to configure our component's input and output buses. This is achieved by calling the base protected methods AddInput_() and AddOutput_() respectively from our component's constructor. Each method must be called once per input / output required. In our component's case, we require 2 inputs and 1 output, therefore our constructor code will look like this:
The string values passed into the AddInput_() and AddOutput_() method calls are signal names / IDs. As component IO can be referenced by either string ID or index, IO signal names are optional. If we do not require a signal to have a string ID associated with it, we can simply leave the parenthesis empty.
Lastly, our component must implement the DspComponent virtual Process_() method. This is where our component does it's work. The Process_() method provides us with 2 arguments: the input bus and the output bus. It is our duty as the component designer to pull the inputs we require out of the input bus, process them accordingly, and populate the output bus with the results. Our component's process method will look something like this:
In order for us to get any real use out of our DspComponents, we need them to interact with each other. This is where the DspCircuit class comes in. A DspCircuit is a workspace for adding and routing DspComponents. In this section we will have a look at how to create a simple DSPatch application that generates random boolean pairs, performs a logic AND on each pair, then prints the result to screen.
First we must include the DSPatch header and any other headers that contain DspComponents we wish to use in our application:
Next, we must instantiate our DspCircuit object and all DspComponent objects needed for our circuit. Lets say we had 2 other components included with "DspAnd" (from the first tutorial): "DspRandBool" (generates a random boolean value then outputs the result) and "DspPrintBool" (receives a boolean value and outputs it to the console):
Now that we have a circuit and some components, lets add all of our components to the circuit:
The string values passed into the AddComponent() method calls are component names / IDs. Although we still have the option of referencing a component via it's pointer in a circuit, component string IDs can allow circuit objects to be entirely self-contained. This could give us the ability to pass circuits around by reference, allowing the receiver access to all circuit components via their string IDs, without having to manage both component and circuit references everywhere.
We are now ready to begin wiring the circuit:
The code above results in the following wiring configuration:
N.B. Each component input can only accept one wire at a time. When another wire is connected to an input that already has a connected wire, that wire is replaced with the new one. One output, on the other hand, can be distributed to multiple inputs.
Lastly, in order for our circuit to do any work it must be ticked over. This is performed by repeatedly calling the circuit's Tick() and Reset() methods. These methods can be called manually in a loop from the main application thread, or alternatively, by calling StartAutoTick(), a seperate thread will spawn, automatically calling Tick() and Reset() continuously. A circuit's thread count can be adjusted at runtime, allowing us to increase / decrease the number of threads use by the circuit as required during execution:
Lastly, the DSPatch::Finalize() method must be called on application exit in order for DSPatch to perform its own internal memory cleanup.
(All the source code from these tutorials can be found under the "tutorial" folder in the DSPatch root directory).