Discrete Blackmer VCA

All Cytomic analog modelling DSP is written using a custom circuit modelling code generator, which we call “Cytomic Circuit Solver” or CCS for short. This way we can specify a circuit at a high level, and let CCS do the linear algebra to solve the system, and then apply symbolic simplifications to the solution. We can also generate code for different programming languages this way, but always use the same high level representation. When new methods or optimisations are added we can also re-generate the DSP quickly for all our products. The core analog modelling DSP in The Glue, The Drop, and The Scream are all generated by CCS, as well as DSP code licensed to Ableton for their various products.

A series of statements lets CCS know which components to add, how to connect them together, and what their parameters are. Below is part of the code that specifies the MXR Phase 90 I posted about recently (https://cytomic.com/mxr-phase-90-jfet-based-phaser/). It isn’t exactly clear to understand which is connected to what, it’s very easy to make mistakes with the number of nodes, which lead to many hours of head scratching trying to work out why the LFO of the Phase 90 model initially didn’t work properly. Also each time a c++ file is changed CCS had to be recompiled before it could generate the code, and that time adds up.

        cir.Add (ModelFactory::Create ("_1n914 d is=2.25n n=1.72 imax=0.1"));
        cir.Add (ModelFactory::Create ("_1n4733 d is=1.2f n=1 imax=0.5"));
        cir.Add (ModelFactory::Create ("_2n5952 njf beta=1.3m lambda=4m vto=-2"));
        cir.Add (new Ground     ("v0",    0));
        cir.Add (new VSource    ("v2",    0, 100, "vbat"));
        cir.Add (new Resistor   ("r15", 100, 101, "10k"));
        cir.Add (new Diode      ("d1",    0, 101, "_1n914"));
        cir.Add (new VSource    ("v3",    0, 104, "zener_v"));
        cir.Add (new Diode      ("dz1", 101, 104, "_1n4733"));
        cir.Add (new Capacitor  ("c8",    0, 101, "15u"));
        cir.Add (new Potentiometer ("rtrim", 0, 101, 102, "250k pos=trim"));
        cir.Add (new Resistor   ("r20", 102, 103, "1meg"));
        cir.Add (new Capacitor  ("c4",  101, 103, "47n"));

Over the past 3 months I’ve been very busy doing a large chunk of R&D to avoid these issues, and speedup our circuit modelling workflow. I use the excellent and free circuit simulator LTSpice (https://www.analog.com/en/resources/design-tools-and-calculators/ltspice-simulator.html) to check my results are as expected, and also check against the analog circuit I’m modelling to make sure I haven’t missed anything. If I have missed a behaviour then I update the circuit / parameters and check again, and this is repeated until I’m happy with the match. My long term aim was always to try and use LTSpice to draw the circuit and specify everything CCS needs to work. The LTSpice graphical program itself writes a plain text file called a “spice netlist” which it passes to its own internal simulator to model the circuit and generate data to plot. What if I could do the same, pass this spice netlist text file to my own solver, but instead of generating data to plot I could generate c++ code to compile to simulate the circuit?

I’m happy to say that I managed to do just that! I can now draw a circuit using LTSpice, and specify some extra manual guidance information CCS needs, and do all this from within the LTSpice user interface!

Part of the R&D involved was working out how best to get the information to CCS from LTSpice. Luckily LTSpice just ignores commands it doesn’t know about (thank you so much to the authors of LTSpice for doing this!). I can safely write my own custom spice directives in parallel to those needed for LTSpice to work, and even partially combine them. To make things clear all Cytomic specific custom spice directives begin with .cy, followed by the command. LTSpice can specify parameters to change via the .param command, for example a control voltage, or a current, or any other value you want to use somewhere else. I can wrap these in a .cybegin and .cyend block and so make these values parameters I can attach knobs to in my DSP.

.cybegin set
.param vctrlp = 5m
.param vctrln = 0m
.param sym = 0.2
.cyend

By far the largest part of the R&D was improving the stability of how PN-Junctions were solved. These contain implicit exponential equations, which lead to what in mathematics are called “stiff differential equations”. When using numerical equation solving methods to iteratively step towards a solution it is easy to step too far and fail to solve the equations. It is also easy to not step far enough, and so take a long time to find the solution – both situations are bad. This is what Wikipedia has to say on the topic:

“Finding a convergent iterative formulation is an art, and every problem is different.”
https://en.wikipedia.org/wiki/Diode_modelling

Not very encouraging, it seems that coming up with such a system is a bit of a black art, and in the specific situation of efficient DSP where I can’t dynamically change the time step used is even more difficult. After a lot of trial an error and critical thinking, and about a solid month’s worth of work, I came up with a method that held promise. It tackled the very difficult case of a fully discrete transistor ladder filter with un-idealities including finite forward and reverse beta as well as parasitic pn-junctions and non-linear diffusion capacitances. But would the method be able to tackle a completely new circuit I’d never done before? One that has a very different topology to that of a transistor ladder? I tried it out on a basic model of the discrete Blackmer VCA circuit as shown below, which is from this article by Rod Elliot: https://sound-au.com/articles/vca-techniques.html

The result? It worked perfectly! I had to double check to make sure, there is usually a mistake or something slightly wrong, but no, it was spot on to the results I got with LTSpice first try! The Blackmer VCA has exponential control of the input signal gain, and here is a plot of varying the positive control voltage from -100mV to 100mV, so from around -16dB to around +16dB gain. The number of iterations required to converge to an accurate solution is shown for each sample as an orange dot, the worst case is 9, and the average number of steps needed is just 2.4, which means the solver is very efficient.

It takes around 1 minute from making a change via the LTSpice user interface till I can hear the compiled version of the code. If I just want to change parameters of the model that can happen instantly, I just edit the parameter and re-initialise the model to use that value all while the plugin is loaded. This is absolutely brilliant, and something I’ve been working towards for the past 15 years, and I’ve finally got there.

The Blackmer VCA is at the heart of many compressors, and a macro model of this device will be included in the HD mode for The Glue v2. Here is a plot of the harmonics generated by the circuit, which are low second (-84 dBFS) and third (-97 dBFS) order – it’s very clean, but this is with very well matched values in the model, and it’s easy to mismatch things as desired.

I wanted to have a listen to the VCA when triggered by an envelope, so I set one up, and had a listen, and compared it to a completely clean algorithmic VCA. I didn’t really notice the added drive too much from the Blackmer model, but I used a sine wave as the input – it may be more evident on a more complicated signal. I did notice a little extra thump from the control signal feedthrough of the Blackmer VCA, a little extra heft was definitely present, which is very promising news for how this will sound at the heart of The Glue v2.

So what’s next? An important part of spice modelling is something called a sub-circuit. This is where you can specify a chunk of a circuit to re-use multiple times, for example an integrated circuit like an op-amp or an OTA, which is great to hide the complexity of these devices, while still offering any level of detail required. Another extension is to add support for arbitrary non-linear equation defined devices. Supporting sub-circuits is fairly straightforward, supporting arbitrary equation defined devices will be more challenging, especially if exponential functions are involved, since then we’re back to the stiff equations of pn-junctions, but now they can appear in any form! Even LTSpice has trouble with certain equation defined devices. One option to support them is to use a sub-circuit to contain the equation defined devices, and then when CCS finds one it can replace it with a custom written and already optimised device for greater cpu efficiency and stability. I’ve already begun doing exactly this for Boyle type op-amp macro models, and the results are excellent.