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
)

18 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.

8 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

Operators looks a bit “haskelly”. Very nice. For my taste extentions like this really make function composition much easier in term of syntax.

1 Like

Hey, that was an excellent presentation… So many YT instructors haven’t accounted for the viewers ability to pause, replay, look up unknown terms themselves etc… and so are painfully wordy and slow. Looks like nice software too, but I’m sure those features are old stuff for SC masters.

You can hack OscOS in the OversamplingOscillators to act as a variable waveshaper. You could also do this with VOsc, but with distortion like this, the oversampling is your friend for sure, and the way OscOS uses buffers is just way more intuitive to me.

(
//fill a signal with 19 progressively aggressive waveshapers
~sig = (2..20).collect{|i| Env([i.neg, i],[1]).asSignal(2048).tanh.normalize}.flatten;

//see the shape of the waveshaper 
~sig.plot;

//load the shaper into a buffer
b = Buffer.loadCollection(s, ~sig);
)
(
{
    var drive = MouseY.kr(0,1);
    //OscOS is designed to be a wavetable lookup, so if it goes over 1-(1/2048), it will try to interpolate back to other side of the lookup table - so we need to clip the value
    var sig = SinOsc.ar(100, 0, drive).linlin(-1,1,0,0.999);
    //hacking OscOS to be a waveshaper
    sig = OscOS.ar(b, sig, 19, MouseX.kr, 2);
    sig.dup
}.scope
)

c = Buffer.read(s, "/Users/spluta1/Library/Application Support/SuperCollider/sounds/AmenBreak.wav");

c.numChannels()

(
    {
        var local_in = LocalIn.ar(2);
        var drive = MouseY.kr(0,1);
        //OscOS is designed to be a wavetable lookup, so if it goes over 1-(1/2048), it will try to interpolate back to other side of the lookup table - so we need to clip the value
        var sig = PlayBuf.ar(2, c, loop:1)+local_in;
        var dc = SinOsc.ar(0.2, 0, 0.01); //some DC offset
        sig = (sig*drive+dc).linlin(-1,1,0,0.999);
        // //hacking OscOS to be a waveshaper
        sig = OscOS.ar(b, sig, 19, LFNoise2.ar(1).range(0,1), 4);
        LocalOut.ar(DelayC.ar(sig, 0.2, MouseX.kr(0.01,0.2))*0.2);
        sig
    }.scope
)

The current version of OscOS has a little bug that I have fixed on my local build and isn’t yet in github, but I don’t think it will show up in this case.

Nothing they are doing otherwise is a mystery. MidEq before the drive and after. I’m not sure if there is a tanh on the left hand side or not, before the shaper. Then a compressor at the end. This design is surprisingly tranparent for Ableton as they normally give dumb names to relatively straight forward processes to make things seem more special than they are.

Sam

2 Likes

I take it back. You cannot do this with VOsc. OscOS is the only way. I will include a dedicated variable waveshaper with the next release of OversamplingOscillators, with some other goodies.

Sam

2 Likes

I was trying to download the OversamplingOscillators_mac.zip file from github, but the download never finishes and the temporary file remains ‘Unconfirmed xxxxxx.crdownload’ without converting to a real zip file.

Works fine for me. That sounds like a GitHub issue. Also, make sure to run xattr on the directory after you download.

Sam

Changing browser fixed it - did not work for me in Chrome, Safari is ok.