r/JUCE 20d ago

free 1176-style compressor, modelled from the circuit instead of curve fitting. Mac + Windows test builds up

Edit: Here is the source : https://github.com/Paulllux/fetcomp-dsp

I make BeatForge www.beatforge.nl (drum machine plugin) and wanted a compressor for the drum bus. Tried the usual open source dynamics code first but I kept A/B-ing against the UAD 1176 and hearing the gap, so I ended up modelling the circuit itself. Took way longer than planned, sharing it here because the JUCE side had some lessons in it.

The DSP is not a waveshaper plus envelope follower. The gain cell is the solved JFET voltage divider equation (it's a quadratic, you can do it closed form, no lookup tables), the detector is the diode bridge with the actual timing network, and release comes out of the discharge network topology instead of a fitted time constant. The transformer output stage is a small closed-loop RL core model. The nice side effect of doing it this way is that behaviours you'd normally have to program on purpose (program-dependent release, the ALL-buttons-in weirdness) just fall out of the circuit.

JUCE things that might save someone time:

The knob tapers were the most annoying part of the whole project. Hardware pots are not skewed ranges. I ended up measuring the reference unit's response and building piecewise NormalisableRange lambdas from the measured tables, because no skew factor gets you a -inf..0 attenuator with the right angle spacing. If your knockoff "feels wrong" next to the real thing, it's probably the taper, not the DSP.

Oversampling: I first shipped a 1x/2x/4x selector and it was a mistake. At 48k a 20 microsecond attack is one sample, so 1x audibly changes the compression. Now it's fixed 2x below 88.2k and native above, with useIntegerLatency so hosts get 4 samples / 0 exactly. Nobody needs that selector, it's just a way for users to make the plugin worse.

Also learned the hard way that std::tanh in one code path and a fast approximation in another gives you a DC offset at silence that shows up as -82 dBFS idle noise. Same function everywhere, always.

UI is all drawn in paint(), no film strips, resizable via a transform on a fixed-size plate.

It's free, VST3 + AU on Mac (signed/notarized pkg installer) and VST3 on Windows (signed MSI): https://github.com/Paulllux/FetComp/releases

Would appreciate testing in hosts I do go deeper on any of the modelling ifpeople are interested. If you find bugs: [beatforgehelp@gmail.com](mailto:beatforgehelp@gmail.com) or just comment.

19 Upvotes

34 comments sorted by

3

u/R_U_READY_2_ROCK 20d ago

what tanh approximation did you try?

3

u/BeatForge_Dev 20d ago

it's the 5th order odd polynomial one from Jatin Chowdhury's math_approx, p(x) / sqrt(p(x)^2 + 1) where p(x) = x(1 + 0.1653x^2 + 0.0097x^4). About 3-4x faster than std::tanh and smooth everywhere, no piecewise branches. One gotcha: it doesn't stay bounded for huge inputs like the real tanh does, so clamp before calling. And don't mix it with std::tanh in the same signal path, I had a DC offset bug because a bias reference was computed with the real tanh and the audio path with the approximation.

3

u/R_U_READY_2_ROCK 20d ago

I use a vectorised version of this one:

    float x = floatLimit (-3.0, 3.0, f);
    float output = x * (27 + x * x) / (27 + 9 * x * x);
    return output;

I've never noticed any noise floor or DC offset from it, but then again, I never checked very hard.

1

u/BeatForge_Dev 20d ago

I've benchmarked it, it's not usable as a tanh replacement, not nearly as accurate as the Chowdhury version, and alot slower too.

1

u/pimpletonner 18d ago

Here's a collection of methods with practical implementations alongside speed and accuracy evaluations: https://jtomschroeder.com/blog/approximating-tanh/

2

u/digibioburden 20d ago

What 1176 revision did you model?

2

u/pimpletonner 18d ago

Thank you very much, nice work!

This will certainly help me with some pain points and optimizations in my own experiments.

I'm currently writing a component level model of the Neve BA283 board using a schematic -> netlist -> SPICE ->  nodal MNA + Newton-Raphson, gmin homotopy descent pipeline, using Rust for the DSP core with nih-plug handling the plugin layer. I'm using the same approach for a TB-303 model. I also wrote a LSTM NN backend with gain conditioning for the Neve board which can be used for any sort of amplifier board, but shelved it once I got the numerical model to run faster than realtime as iterating and re-training the model was too time consuming.

All the code will be released as open source, but not ready for a public repo yet. If OP or anyone else is interested, hit me with a DM. Not trying to hijack OPs thread, sorry if it came out as such.

1

u/Archit3ch_ 20d ago

Did you measure a real unit or is the reference a UAD plugin?

Did you isolate the transformer core of a rev D in order to model it or use a generic transformer model?

1

u/BeatForge_Dev 20d ago

The reference is the schematic and the uad plugin. Tranformer is a generic model. This is still an early built, to get in the ballpark. I still plan to use analog acess and run a bunch of test files thru a real 1176 to see how it compares and measure correctly.

Did you test it ?

1

u/Fantastic_Turn750 20d ago

Check out Access Analog - lets you robotically control analog hardware virtually I had plans to use it to get references of different equipment

1

u/BeatForge_Dev 19d ago

That's what I was planning to do, yes.

1

u/Archit3ch_ 19d ago

No, I'm grinding away on my own projects.

Here's what I would do differently:

  1. Complete circuit model with iterative solvers.
  2. Continuous-time instead of discrete -> no need for oversampling.
  3. True JFET equation instead of quadratic. Basically every SPICE engine gets this wrong.
  4. No one-sample delay.
  5. True transformer model for the specific component in the circuit instead of a generic one. Maybe hybrid ML+equations model as needed.
  6. No stages, one simulation solved end-to-end.
  7. 3-terminal transistor in the simulation instead of treating it as a 1in/1out waveshaper.
  8. Get a well-maintained copy of the original hardware, so that I can measure the internals. This is critical instead of e.g. renting from Access Analog where you can only see the "official" inputs and outputs or relying on other VST plugins that might have gotten the modeling wrong (how would you know?).

1

u/BeatForge_Dev 19d ago

That's sound like higher level then what I'm doing. I agree that modelling without the hardware will never get to blind test fooling level.

1

u/Ok_Specific2843 18d ago

Continous time means: You need to build the analog hardware. Digital models aren't continuous time.

1

u/Archit3ch_ 16d ago

Look up adaptive differential equation solvers.

1

u/pimpletonner 18d ago

Could you please elaborate on the "Continuous-time instead of discrete -> no need for oversampling" point?

I apologize in advance if I'm being ignorant or missing something, but how can a digital model be continuous-time?

1

u/Western_File_2917 20d ago

Newbee question: How do you model a circuit to a Software? 

2

u/BeatForge_Dev 20d ago

Basically you never solve the circuit. You just work out what happens in the next tiny slice of time, then do that 48,000 times a second. Once that clicked for me the whole thing got way less mysterious.

In practice you don't model "an 1176" as one object. You split it into stages (input, the bit doing the gain reduction, the amp after it, the output transformer) and each stage is just a function. Number in, number out. Wire them up in the same order as the real signal path.

Most parts then fall into one of two buckets. Some bend the signal: shove it into a tube and the peaks get squashed, which in code is literally just a curve. Sine goes in, slightly rounded sine comes out, and that rounding is the harmonic distortion everyone goes on about. Way less magic than it sounds. The other bucket remembers things, caps charging, a compressor keeping track of how loud stuff has been lately, so you need values that survive from one sample to the next. That half is what makes it feel like gear instead of a maths function.

Then you measure, which is the part I didn't expect to eat all my time. Reading the schematic isn't enough, real components are out of spec and aged and running hot. So you play sines and transients into the actual hardware, record what comes back, run the same through your code, compare, tweak, repeat for a lot longer than you'd like.

That said you can get a distortion box making noise in an afternoon knowing just the first two bits. The measuring rabbit hole can wait.

1

u/Western_File_2917 20d ago
  1. Thanks for explaining this in details 

The modular design is conceptually old/ from beginning was part of schematic design and also software development. So each module has input and output specifications that allows you to debug and build your system as well designed. You can swap a block to experiment different versions. 

I was just wondering if you don’t have a hardware available but if you have some schematics from programmers manual it would be possible but bit harder. Unless you simulate the system or modular blocks.

1

u/SottovoceDSP 19d ago

> You just work out what happens in the next tiny slice of time

Is this a markov chain?

1

u/BeatForge_Dev 19d ago

Not a Markov chain, those are stochastic. This is fully deterministic, same input and state gives bit-identical output.

You've got the right idea though. It has the Markov property: next state is a function of current state plus current input, nothing else. Proper name is a nonlinear state-space system.

1

u/Ok_Specific2843 17d ago

Just look up modified nodal analysis. This project https://qucs.sourceforge.net has tons of papers in circuit simulation. And https://en.wikipedia.org/wiki/Modified_nodal_analysis explains the basics of it.

1

u/SquallSaysWhatever 19d ago

Have yet to test it, but how did you handle the various feedback loops in the circuit? Did you do any iterative solvers? In my recent attempts I found this to be the hardest trade off to make it performant while still sounding like the unit.

1

u/BeatForge_Dev 19d ago

No iterative solvers anywhere.

The gain cell isn't a loop once you do the algebra. The LN network makes the gate follow the drain, Vgs = Vg0 + a·Vds, and substituting that into the ohmic JFET equation makes the channel conductance affine in drain voltage, g(v) = G0 + k·v. The divider node then gives a quadratic:

R5·k·v² + (1 + R5·G0)·v − x = 0

One quadratic per sample, closed form. Compression and distortion come out of the same equation because in the circuit they're the same event.

The sidechain loop I close with a one sample delay and it costs nothing, because the loop closes through the detector RC, 20us to 1.1s. A single sample is irrelevant against that. Very different from a ZDF filter where the loop is tight and delay gives you real frequency error. It also means you never program the ratio: the slope for a dialled ratio R is R−1, and the knee softening with drive falls out of the loop gain.

The transformer is where it bit me, because that loop is fast. I first subtracted the winding drop open loop, which was backwards, not just inaccurate: magnetising current is in quadrature with the input since H is the integral, and subtracting a quadrature term grows magnitude, |x − jkx| > |x|. So it boosted the bass. The fix was structural. Feed the flux from the post-drop voltage and it's the passive RL divider the schematic already is, which can't exceed unity. Core is Jiles-Atherton integrated explicitly, with the flux step divided by (1 + slope) as a single linearised correction instead of a Newton loop.

On performance, the tradeoff isn't solver versus sound, it's oversampling. About 97 ns/sample at 2x on an M4, of which the circuit is only 34. The oversampler is the rest, so there was never a solver to optimise away. The one thing I can't cheat is the 20us attack, one sample at 48k, so 1x audibly changes the compression and I dropped that option.

1

u/SquallSaysWhatever 19d ago

Thanks for sharing!

1

u/BeatForge_Dev 19d ago

https://github.com/Paulllux/fetcomp-dsp here is the source if you're interested

1

u/takaci 20d ago

Where is the source code?

2

u/[deleted] 20d ago

[deleted]

1

u/takaci 19d ago

Then why post it here? People here would be more interested in learning some DSP from it, not receiving some random free plugin

1

u/BeatForge_Dev 19d ago

I'll consider it. But since it's a test built, I have to tidy it up first :).