musikinformatik / SuperDirt

Tidal Audio Engine
GNU General Public License v2.0
519 stars 75 forks source link

Problems while adding a custom GlobalDirtEffect #251

Open geikha opened 2 years ago

geikha commented 2 years ago

This is a repost from: https://club.tidalcycles.org/t/innard-help-problems-with-globaldirteffects-while-coding-a-compressor/3397

System: Win8.1, SC 3.12.0, SD 1.7.2, Tidal 1.7.8


I'm trying to make a compressor for SuperDirt, specifically because I want to achieve " https://club.tidalcycles.org/t/sidechain-compression-in-tidal/509?u=ritchse ". I've been running into many problems and my main concern right now is default values.

Setup

I added the global effect to the orbits globalEffects array:

~dirt.orbits.do { |x|
    var l = x.globalEffects.size;
    x.globalEffects = x.globalEffects.insert(l-2,
        GlobalDirtEffect(\dirt_compressor,['comp','thresh','atktime','reltime','upcomp','makeup','sidechain']));
    x.initNodeTree;
};

I defined some default values to the arguments of the SynthDef as one usually does:

~dryBuses = [];
~dirt.orbits.do { |x|
    ~dryBuses = ~dryBuses ++ x.dryBus
}
(
SynthDef.new("dirt_compressor" ++ ~dirt.numChannels,
    {|dryBus, effectBus, comp=1, thresh=0.5, atktime=0.01, reltime=0.1, upcomp=1, makeup=1, sidechain=(-1)|
        var sound, in, control, chs;
        chs = ~dirt.numChannels;
        in = In.ar(dryBus, chs);
        control = In.ar(Select.kr(sidechain+1,[in]++~dryBuses),2);
        sound = Compander.ar(in, control, thresh: thresh, slopeAbove: (1/comp),
            clampTime: atktime, relaxTime: reltime, mul: makeup, slopeBelow: (1/upcomp));
        Out.ar(effectBus, sound)
}, [\ir, \ir]).add;
s.freeAll;
)

And set my params in tidal as such:

let comp = pF "comp" -- compression ratio
    thresh = pF "thresh" -- threshold
    atktime = pF "atktime" -- attack time
    reltime = pF "reltime" -- release time
    upcomp = pF "upcomp" --upwards compression ratio
    makeup = pF "makeup" --amplitude of the output
    sidechain = pI "sidechain" --orbit from which to sidechain

Results

d1 $ s "bd*2" # makeup 1 # comp 1 # thresh 1

^ This is a neutral state of compression, nothing is happening, and sound is coming out as expected: the same as with no effects.

d1 $ s "bd*2" # makeup 0 # comp 1 # thresh 1

^ This should produce no sound, yet it does, at a lower volume than before. I'd say about half the volume; so, is it running on parallel?

d1 $ s "bd*2"

^ This should sound the same as in the beginning, but it sounds the same as the previous example. I figured it must have to do with default values or something so I checked the node tree:

8768 dirt_compressor2
        dryBus: 48 effectBus: 50 comp: 0 thresh: 0 atktime: 0.0099999997764826 reltime: 0.10000000149012 upcomp: 1 makeup: 0 sidechain: -1
      8767 dirt_rms2

Here I run into my first problem: When erasing a paramater, it doesn't go back into its default value. I'm guessing SC just ignores the last change of values into 0 since it would generate a division by zero error, hence the same output as before.

d1 $ s "bbtdr*2" # makeup 1 # comp 1 # thresh 1

^ This one is mind-boggling to me. After that mess of division by zero, when I try to go back into the neutral state of compression, now I hear nothing! There's no output.

Here's the whole node tree of the first orbit:

2 group
         9347 group
            -101992 dirt_sample_2_2
              out: 46 bufnum: 44 sustain: 0.41246938705444 begin: 0 end: 1 speed: 1 endSpeed: 1 freq: 261.62557983398 pan: 0 span: 1
            -102000 dirt_gate2
              out: 48 in: 46 sustain: 0.41246938705444 fadeInTime: 0 fadeTime: 0.0010000000474975 gain: 1 overgain: 0 amp: 0.40000000596046 sample: 1436622976 gateSample: 0 cutAll: 0
      8771 dirt_delay2
        dryBus: 48 effectBus: 50 gate: 1 delaytime: 0 delayfeedback: 0 delaySend: 1 delayAmp: 0 lock: 0 cps: 0.5625 resumed: 0
      8770 dirt_reverb2
        dryBus: 48 effectBus: 50 gate: 1 room: 0 size: 0.10000000149012 dry: 0 resumed: 0
      8769 dirt_leslie2
        dryBus: 48 effectBus: 50 gate: 1 leslie: 0.5 lrate: 6.6999998092651 lsize: 0.30000001192093 resumed: 0
      8768 dirt_compressor2
        dryBus: 48 effectBus: 50 comp: 1 thresh: 1 atktime: 0.0099999997764826 reltime: 0.10000000149012 upcomp: 1 makeup: 1 sidechain: -1
      8767 dirt_rms2
        dryBus: 48 effectBus: 50 gate: 1 rmsReplyRate: 0 rmsPeakLag: 0 orbitIndex: 0 resumed: 0
      8766 dirt_monitor2
        dryBus: 48 effectBus: 50 outBus: 0 gate: 1 limitertype: 1 resumed: 0

I realized that every global effect except the one I'm trying to add has the argument resmued at the end. I tried looking for it in the SuperDirt repo but couldn't really figure out what it is exactly.

Final questions

Thank you for reading all this mess ! Love

telephon commented 2 years ago

^ This should produce no sound, yet it does, at a lower volume than before. I'd say about half the volume; so, is it running on parallel?

yes, effects run in parallel by default. To replace the audio and not use the effect bus:

ReplaceOut.ar(dryBus, sound)

not sure if this is what you need.

This will not add the sound from the other effects:

d1 $ s "bd*2" # makeup 0.5 # comp 1 # thresh 1 # room 1 # size 0.5

Here I run into my first problem: When erasing a paramater, it doesn't go back into its default value.

For me it does.

I'm guessing SC just ignores the last change of values into 0 since it would generate a division by zero error, hence the same output as before.

No, I just get an instrument not found error in sclang (because I don't have bbtdr).

fracnesco commented 2 years ago

Maybe something like this is what you are going after? @ritchse

~bus1 = Bus.audio(s, numChannels:2);
~dirt.orbits[0].outBus = ~bus1;

(Ndef(\test, {
    var dirt = InBus.ar(~bus1, 2);
    dirt = GVerb.ar(dirt); // i use a reverb as an example here you add your compressor
}).play;);

and then you control by tidal with

~dirt.soundLibrary.addSynth(\test, (play:
    {|dirtEvent|
        ~dirt.server.makeBundle(dirtEvent.event[\latency], {
             Ndef(\test).xset(compressor parameters);}
        });
}));
telephon commented 2 years ago

Wasn't the original problem solved? I understood that my suggestion helped?

fracnesco commented 2 years ago

We were further discussing this on discord today and ritchse suggested to post this alternative also here. The limitation with the current logic in SuperDirt is that Global effects can't silence the dry sound, while standard effect modules truncate the sound at the end of the timespan. So it's not possible to have a total wet and not time span dependent sound process. Further processing SuperDirt output with NodeProxies like showed in the Hacks folder seems to me the more straight forward way to achieve this.

telephon commented 2 years ago

The limitation with the current logic in SuperDirt is that Global effects can't silence the dry sound

If you write your own effect, I believe you can:

// instead of
Out.ar(outBus, signal)
// you can write
ReplaceOut.ar(outBus, signal)
// and if you don't want to replace it completely all the time
ReplaceOut.ar(outBus, signal + (In.ar(outBus) * \prevLevel.kr(1)))

Then you can add prevLevel as a parameter.

Routing through the Ndef is good as well, I'd probably do it like this myself. But this shows how to do it "in dirt".

geikha commented 2 years ago

tried a dummy example, but it doesn't seem to work:

(
SynthDef.new("test" ++ ~dirt.numChannels,
    {|dryBus, effectBus, makeup=1 | // makeup being a multiplier for signal
        var sound, in, control, chs;
        chs = ~dirt.numChannels;
        in = In.ar(dryBus, chs);
        sound = in*makeup;
        ReplaceOut.ar(effectBus, sound)
}, [\ir, \ir]).add;
)

(
~dirt.orbits.do { |x|
    var l = x.globalEffects.size;
    x.globalEffects = x.globalEffects.insert(l-2, //adding after leslie, before rms
        GlobalDirtEffect(\test,['makeup']));
    x.initNodeTree;
};
)

s.freeAll;
d1 $ s "bbtbd*4" # pF "makeup" 0
-- above should produce no sound, yet it does
telephon commented 2 years ago

Ah yes, I see. The example I gave was wrong. What is needed is a change in the monitor, which routes the signals from the effect bus to the output bus. I can't implement/test this now, but this would be the place to start. It currently just adds wetSignal + drySignal. There is a commented out line that could be used for a global wet and dry parameter of the bus as a whole.

SynthDef("dirt_monitor" ++ numChannels, { |dryBus, effectBus, outBus, gate = 1, limitertype = 1|
        var drySignal = In.ar(dryBus, numChannels);
        var wetSignal = In.ar(effectBus, numChannels);
        //var signal = XFade2.ar(wetSignal, drySignal, dry * 2 - 1);
        var signal = wetSignal + drySignal;
        var post = if(SuperDirt.postBadValues) { 2 } { 0 };

        signal = Select.ar(CheckBadValues.ar(signal, post: post) > 0, [signal, DC.ar(0)]);

        signal = Select.ar(limitertype,
            [
                signal,
                Limiter.ar(signal),
                softclip(signal * 0.5) * 2
            ]
        );

        DirtPause.ar(signal, graceTime:4);

        signal = signal * EnvGen.kr(Env.asr, gate, doneAction:2);
        Out.ar(outBus, signal)
    }, [\ir, \ir, \kr, \kr, \kr]).add;
fracnesco commented 2 years ago

I think using the XFade2 on all Global Effects would introduce problems for other kind of effects (imaging opening a reverb and hearing the dry sound fading out, could be very confusing). The main point we are facing here is that some global effects needs to work in parallel some in insert. One way to implement this could be to create a variant of "dirt_monitor" that handles the summing of dryBus and wetBus differently for some specific global effects maybe? But the point is also is that you can easily route SuperDirt orbits in busses and do the processing there, so Idk if it's necessary.

geikha commented 2 years ago

i agree with your main point, just wanted to point out that some reverbs actually use a single dry to wet knob, where 0% is dry and 100% is only wet. for example the Valhalla DSP reverbs.

telephon commented 2 years ago

One way to implement this could be to create a variant of "dirt_monitor" that handles the summing of dryBus and wetBus differently for some specific global effects maybe?

Yes, that would be the way to go: each global effect could do the mixing and the dirt_monitor would just be the default effect. This might break some code, though.

Alternatively, each effect could be followed by its own dirt_monitor, that cares for the distribution of the signal between effectBus and outBus. But their parameters might be complicated to control, I need to think about how to do this.

fracnesco commented 2 years ago

i agree with your main point, just wanted to point out that some reverbs actually use a single dry to wet knob, where 0% is dry and 100% is only wet. for example the Valhalla DSP reverbs.

True, but here we are not talking about the knobs of the reverb but rather the routing in the mixer. Generally reverb is applied using a send/return putting the effect on full wet and adjusting the amount of reverb from the send. But I also think that imitating standard approaches is not really the spirit of live coding so defenetly this could be a nice expansion of possibilities.

BTW what if you put the compressor in the dirt_monitor itself? It's not a general solution to this but could fit your specific case.

telephon commented 2 years ago

Here is a page to discuss: https://club.tidalcycles.org/t/the-routing-structure-of-a-dirt-orbit/3902

Probably it is very easy, all that we need is to do it like leslie does. I had forgotten about that one.