Connoise

Connected Noise Machine

CONNOISE was an exercise in exploring software architecture for real-time audio synthesis. I wanted to take the concept of a modular synthesizer and replicate that in software, whereby each module has a number of inputs and outputs and performs a single function. This was also an exercise in learning some of the newer c++17 language features, but I also set myself the goal that the project should be as well documented as possible. The project builds with Doxygen docs and has a pretty complete README.md file. The documentation was a lot of extra work, but ensured that I took the project seriously and encouraged me to publish it all as open source.

I stared this in January 2021 and worked on it as my primary spare time project for about 2 months.

Architecture

CONNOISE ended up being structured as a library (libcno) including all of the synthesizer implementation and a file format parser and serialiser, a CLI application (connoise) and a GUI application (connoise-gui).

This makes the CLI application pretty trivial, it parses the arguments and instantiates the system from the library. Similarly for the GUI, the application is all GUI code and uses the library APIs to manage the synthesizer system.

  graph LR
connoise <--> System
connoise-gui <--> System

subgraph C[libcno]
direction TB
System

    Parser -.-> System
    System <-.-> SystemModel
    Transport -.-> System
    System -.-> ModulesManager
    ModulesManager -.- Registry
    Registry -.- Inputs
    Registry -.- Outputs
    Registry -.- Modules

    Inputs -.- I01[Constant]
    Inputs -.- I02[MIDI Device]
    Inputs -.- I03[Null]

    Outputs -.- O01[Audio Device]
    Outputs -.- O02[Null]
    Outputs -.- O03[WAV File]

    Modules -.- M01[ADSR]
    Modules -.- M02[Delay]
    Modules -.- M03[FIR Filter]
    Modules -.- M04[Hold]
    Modules -.- M05[Hz to MIDI Note]
    Modules -.- M06[MIDI Note to Hz]
    Modules -.- M07[Math Abs]
    Modules -.- M08[Math Add]
    Modules -.- M09[Math Differential]
    Modules -.- M10[Math Multiply]
    Modules -.- M11[Mixer]
    Modules -.- M12[Oscillator Saw]
    Modules -.- M13[Oscillator Sine]
    Modules -.- M14[Oscillator Square]
    Modules -.- M15[Random]
    Modules -.- M16[Sequencer]

end

System and SystemModel

The interesting part here is the use of the SystemModel which implements the directed graph of Module port connections - this is used not only in the GUI for rendering the system layout, but also to figure out the graph dependencies in order to actually render the flow of signals in the correct order. The SystemModel is an abstract model representation of the actual System. The System owns all the objects which actually do any useful work, but the SystemModel contains the description of the System which makes it easier to inspect and work with. The SystemModel implements the algorithm for figuring out how to process the nodes in the graph in parallel. Given that all modern computers have multi-core processors, and real-time audio rendering requires the entire graph to be calculated with strict timing requirements, the algorithm is required in order to distribute the calculations amongst all CPU cores (using threads) and therefore allowing larger graphs to be rendered than would be possible if only one CPU core were available.

The parallel calculation algorithm takes the system graph and performs a breadth first search across all nodes from inputs to outputs. As it does this, it records the distance of each node from the output, and groups the nodes by distance. Since the distance describes the dependency order of the nodes, we can process all nodes with the same distance value at once. This is how rendering proceeds - all nodes in each distance group are processed in parallel before moving over to the next group. The distance groups themselves are sorted highest to lowest, meaning the nodes furthest from the output node are processed first, as we would expect the signals to be flowing from inputs to outputs.

Modules

The Modules themselves have a fairly basic API, they can declare their own type, get allocated unique IDs and have accessors for any number of named input and output ports. The remaining methods allow the System to initialise and de-initialise the module's ports for rendering and call the ports themselves for each update. Notably, there is not a single update method in each Module, the processing actually happens at each input and output port. For example, in a module which implements a sine wave oscillator, the sine function would be part of the v output port and not the module itself.

As an aside, in this manner actually CONNOISE only really needs one oscillator module type, because they all have mostly the same input parameters (frequency, phase) but each waveform type could be implemented on each of the output ports, we could have a port for sine, a port for square, etc. In practice though there are some variations on the parameters for each oscillator type, so it makes sense to have them as separate module types.

Other components

Transport controls the playing state of the system, much like the controls on any media player, it has a basic state machine to ensure only the correct transitions can happen (although in practice this is mostly Stop <--> Play). It is also responsible for preparing port rendering buffers before rendering starts, and for calling all of the port update functions in the rendering loop, and for cleaning up the buffers at the end of rendering.

Parser implements the grammar for the cno file format, and during parsing can populate the System with all of the valid items found in the file data.

We needed ModulesManager and Registry to be able to find module implementations by name, this maps the names declared in the cno file description to code implementation.

GUI

I wrote the GUi using Dear Imgui and imgui-node-editor. Dear Imgui is amazingly simple to use and extremely performant. Being a immediate mode renderer the GUI code ends up being just a big long list of things to draw. I find that this makes the code a lot easier to follow and there is not much in the way of framework or abstractions to get in the way. That said, I'm not sure I would want to use it for any application more complex than this, but in this case I was happy with the choices made.

The current state of the GUI application is shown here; this model has a variety of nodes created and connected together. There is an inspector panel on the right for adjusting the static node parameters, and most numerical node parameters can receive inputs from other nodes. Nodes can be fully created, configured and connected in the GUI, and the system saved to a .cno file. The colour of the connections between nodes changes according to the current value of the signal on that connection.

File format

CONNOISE implements its own file format for describing a synthesis system, a basic example shown here:

Module Sine {
  id 1
  params {
    frequency 220.0
  }
}

Module Square {
  id 2
  params {
    frequency 7.65
  }
}

Module Multiply {
  id 3
}

Connection {
  2 v
  3 a
}

Connection {
  1 v
  3 b
}

Output AudioDevice {
  id 4
}

Connection {
  3 v
  4 L
}

Modules, Inputs, Outputs, are all declared separately. Each of these has an id and a number of named ports. The Connections declare which ports are connected together by referencing an id and a port name.

The full file format specification is here.