Roar saturator

I was messing around a bit tonight to try to tip off the signal chain of Ableton’s new Roar saturator, because it seems super simple but really versatile and nice sounding.

I didn’t make a lot of progress, but I thought I’d post it as a bit of a group project if anyone wants to make amendments, suggest changes, or add things on. Leave comments or new implementations as comments and i’ll amend the original!

My intention isn’t to EXACTLY copy the sound or implementation, only to copy the overall signal chain and approach, in a straightforward and relatively clear way. I got only as far as most of one module, probably the modulation matrix stuff and the multi-module stuff is best done in several synthdefs versus in just one.

You’ll need this extension, as well as sc3-plugins to run - but everything else is stock.

(
SynthDef(\roarTest, {
    var sig, sigDry, sigWet, sigShaped, feed;
    var tone, toneFreq, toneComp, toneAmpLo, toneAmpHi, drive, bias, amount;
    var filterFunc, filterFreq, filterLoHi, filterBP, filterRes, filterBW, filterPre;
    var feedAmt, feedFreq, feedBW, feedDelay, feedGate;
    
    drive       = \drive.kr(spec:ControlSpec(       0, 48,      default: 14         )).dbamp;
    tone        = \tone.kr(spec:ControlSpec(        -1, 1,      default:-0.4        ));
    toneFreq    = \toneFreq.kr(spec:ControlSpec(    20, 20000,  default: 5520       ));
    toneComp    = \toneComp.kr(spec:ControlSpec(    0, 1,       default: 1          ));
    amount      = \amount.kr(spec:ControlSpec(      0, 1,       default: 0.8        ));
    bias        = \bias.kr(spec:ControlSpec(        -1, 1,      default: 0.0        ));
    
    filterFreq  = \filterFreq.kr(spec:ControlSpec(  20, 20000,  default: 12800      ));
    filterLoHi  = \filterLoHi.kr(spec:ControlSpec(  -1, 1,      default: -1         ));
    filterBP    = \filterBP.kr(spec:ControlSpec(    0, 1,       default: 0.2        ));
    filterRes   = \filterRes.kr(spec:ControlSpec(   0, 1,       default: 0.3        ));
    filterBW    = \filterBW.kr(spec:ControlSpec(    0, 4,       default: 0.5        ));
    filterPre   = \filterPre.kr(spec:ControlSpec(   0, 1,       default: 1          ));
    
    feedAmt     = \feedAmt.kr(spec:ControlSpec(     -90, 12,    default: 14         )).dbamp;
    feedFreq    = \feedFreq.kr(spec:ControlSpec(    20, 20000,  default: 80         ));
    feedBW      = \feedBW.kr(spec:ControlSpec(      0, 4,       default: 0.1        ));
    feedDelay   = \feedDelay.kr(spec:ControlSpec(   0, 4,       default: 1/6        )) - ControlDur.ir;
    feedGate    = \feedGate.kr(spec:ControlSpec(    0.02, 0.3,  default: 0.1        ));
    
    toneAmpLo   = tone.lincurve(-1.0, 1.0, 2.0, 0.0, -0);
    toneAmpHi   = tone.lincurve(-1.0, 1.0, 0.0, 2.0,  0);
    
    // sig = \in.ar([0, 0]);
    // sig = SAMP("/Users/Shared/_sounds/photek/full/photek1.wav")[0].ar(loop:1);
    sig = PlayBuf.ar(1, \buffer.ir, loop:1);
    
    // WET TONE
    sigWet = sig
        |> BHiShelf.ar(_,  toneFreq, 1, toneAmpHi.ampdb)
        |> BLowShelf.ar(_, toneFreq, 1, toneAmpLo.ampdb);
    
    // DRY TONE
    sigDry = sig
        |> BHiShelf.ar(_,  toneFreq, 1, 0)
        |> BLowShelf.ar(_, toneFreq, 1, 0);
    
    // Dry should be silent if tone = 0, else it should "make up" 
    // the attenuation from the shelf filters? Use no-op filters on the dry
    // signal so delay from filter matches wet signal?
    sigDry = (sigDry - sigWet);
    
    // FEEDBACK
    feed = LocalIn.ar(2);
    feed = feed
        *> feedAmt
        |> BBandPass.ar(_, feedFreq, feedBW)
        |> DelayC.ar(_, 4, feedDelay)
        |> LeakDC.ar(_)
        *> Amplitude.ar(sig, 0.01, feedGate);
        
    // FILTER
    // filterLoHi blends between a lowpass and highpass
    // filterBP blends between the lo-hi signal and a bandpass
    filterFunc = {
        |sig|
        blend(
            blend(
                BLowPass.ar(sig, filterFreq, filterRes),
                BHiPass.ar(sig, filterFreq, filterRes),
                filterLoHi.linlin(-1, 1, 0, 1)
            ),
            BBandPass.ar(sig, filterFreq, filterBW),
            filterBP
        )
    };
    
    // SHAPE: PRE-FILTER
    // filterPre blends between filtering befor the shape stage, or after
    sigShaped = sigWet + feed;
    sigShaped = blend(sigShaped, filterFunc.(sigShaped), filterPre);
    
    // SHAPE
    sigShaped = sigShaped
        *> drive
        +> bias
        // |> tanh(_);
        |> SoftClipAmp8.ar(_, drive);
        // |> SmoothFoldQ.ar(_, -1, 1, 0.8, 0.5);
    
    // SHAPE: POST-FILTER
    sigShaped = blend(sigShaped, filterFunc.(sigShaped), 1 - filterPre);
    LocalOut.ar(sigShaped);
    
    sigWet = blend(sigWet, sigShaped, amount);
    
    sig = sigWet + (toneComp * sigDry);
    
    Out.ar(\out.kr(0), \amp.kr(1) * sig * [1, 1]);
}).addReplace;
)

(
// \roarTest.asSynthDesc.controls.collect({
//     |c|
//     "%%,%".format(
//         $\\,
//         c.name, 
//         c.defaultValue
//             .round(0.01)
//             .asString
//             .padLeft(20 - c.name.asString.size.postln)
//     )
// }).join(",\n");

Pdef(\roadTestControls, Pbind(
    \drive,            6.0,
    \tone,            -0.2,
    \toneFreq,       820.0,
    \toneComp,         0.7,
    \amount,           0.4,
    \bias,             0.1,
    \filterFreq,    5800.0,
    \filterLoHi,      -0.7,
    \filterBP,         0.5,
    \filterRes,        0.7,
    \filterBW,         0.8,
    \filterPre,        0.0,
    \feedAmt,          5.0,
    \feedFreq,       120.0,
    \feedBW,             2,
    \feedDelay,       1/60,
    \feedGate,         0.06,
    \buffer,           0.0,
));

Pdef(\roadTest, Pmono(
    \roarTest,
    \dur, 1/4,
    \buffer, ~buffer = ~buffer ?? { Buffer.read(Server.default, "/Users/Shared/_sounds/photek/full/photek3.wav") },
    \amp, -3.dbamp,
) <> Pdef(\roadTestControls)).play
)

14 Likes

If anyone wants the test files I was using…


2 Likes

It’s really cool, scottt. and the way you wrote the code is inspiring, seems to tell me that there’s a new language wanting to blossom out there. This thing of easily folding signals seems to be in the air.

I GOT THE EYE OF A TIGER, A FIGHTER, DANCING THROUGH THE FIRE

(
SynthDef(\roar, {
	var snd, fb, duration;
	duration = \duration.kr(1.0);
	snd = Saw.ar(Env([10, 300, 70, 230, 90, 20].cpsmidi, [1, 4, 1, 1, 5, 3].normalizeSum * duration, curve: -2).ar.midicps);
	snd = RLPF.ar(snd, XLine.ar([800, 300], [1200, 400], duration) * (LFNoise2.kr(3 ! 2) * 3).midiratio * 0.5, 0.08).sum;
	snd = snd * Env.linen(0.01, duration, 0.03).ar;

	snd = BHiShelf.ar(snd, 1200 * LFNoise2.kr(8).linexp(-1, 1, 0.5, 2), 0.3, -10);
	snd = BLowShelf.ar(snd, 200 * LFNoise2.kr(8).linexp(-1, 1, 0.5, 2), 0.3, -10);

	fb = LocalIn.ar(1);
	fb = BPF.ar(snd, LFNoise2.kr(3).linexp(-1, 1, 100, 3000), 0.3);
	fb = fb * LFNoise2.kr(8).linlin(-1, 1, 5, 10).dbamp;
	fb = LeakDC.ar(fb);
	snd = snd + fb;
	snd = (snd * 3.dbamp).tanh;
	snd = (snd * 3.dbamp).fold2;
	LocalOut.ar(snd);

	snd = BHiShelf.ar(snd, 5200, 0.3, 5);
	snd = BLowShelf.ar(snd, 200, 0.3, 5);

	snd = snd + GVerb.ar(snd * -20.dbamp, 50, 3);
	snd = Limiter.ar(snd);
	Out.ar(\out.kr(0), snd);
}).play;
)
4 Likes

yes, same impression here.
Your extension looks cool, scott! mind to explain a little what each of the operators do?

I started:

1 Like

It’s not directly related to any of this discussion, but I just want to drop this entertaining article so everyone can have a chance to read))

Yes - sorry for using esoteric operators! This is just what my code was it and it was a bit troublesome to rewrite it.

They’re pretty simple:

// this:
  a |> b

// is equivalent to:
  b(a)

// when combined with the partial application operator _, this:
  sig |> LPF.ar(_)

// is equivalent to:
  LPF.ar(sig);

// these are roughly the same, but combining with the left hand signal using an operator:
  a +> b
  a *> b
  a ++> b

// are equivalent to:
  a + b(a)
  a * b(a)
  a ++ b(a)

// and finally, these are arrayed versions of |>:
  a ||> b
  a >|| b

// are equivalent to:
  a.collect{ |i| b(i) }
  b.collect{ |i| a(i) }

// or, for example:
  [a, b, c] ||> d
  a >|| [b, c, d] 

// are equivalent to:
  [d(a), d(b), d(c)]
  [b(a), b(a), c(a)]

They’re all more or less ways of composing operations in a SynthDef together without nesting, so they can be read left-to-right, top-to-bottom. I would consider these VERY tentative in terms of usefulness, design, syntax :slight_smile: - I’ve been experimenting with different versions of these to find ways of making complex signal chains clearer.

One specific advantage is that you can generally have one unit operation per line of code, which means you can easily comment/uncomment lines to change your signal chain - which ends up just being a good way of working.

6 Likes

I love it, and I’ve been using it for years, I was even reprimanded on the list years ago for bringing up these ideas. (Someone even tried to recall the “pythonic way” , which is kind of common here from time to time)

But nowadays I think these are not “linguistic idiosyncrasies”,(as they could be in some cases, just “a way of
expressing things in terms of other things” as the author of the article said), but are the seeds of a need to reformulate language in relation to the concrete practice, opening up the possibilities operating with audio signals.

The author (Landin) seems like just an annoying academic complaining about things, but he brings some important wisdom with lots of ramifications.

1 Like

this is awesome, thanks! While teaching (and live coding), I am always thinking on how to create a “processing flow” within the sequential writing… I like text-based but sometimes I get annoyed by the “inverse-intuitive” way OOP notation forces me to write things (inside out).

1 Like

I think you’re not alone, 1977 Turing Award kind of felt it too

Complete text: http://worrydream.com/refs/Backus-CanProgrammingBeLiberated.pdf

1 Like

big thread about these operators from last year…:

I use the equivalent of |> everywhere (for me it’s =>) and feel we should add this to the class library.

There are also at least a couple variants that are needed - @Jordan proposed a .tap adverb so that |>.tap is

{ |that| that.(this); this }

so you can write for example:

{
SinOsc.ar() * Env.perc() |> FreeVerb.ar(_) 
|>.tap DetectSilence.ar(_)
}
2 Likes