Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

livecodable dsp #309

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions classes/DirtEvent.sc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ DirtEvent {
}

play {
var play;
event.parent = orbit.defaultParentEvent;
event.use {
// s and n stand for synth/sample and note/number
Expand All @@ -21,8 +22,19 @@ DirtEvent {
this.calcTimeSpan; // ~sustain is calculated here
if(~sustain >= orbit.minSustain.value or: { ~play.notNil }) {
this.finaliseParameters;
// unless event diversion returns something, we proceed
~play.(this) ?? { this.playSynths };
play = {
// unless event diversion returns something, we proceed
~play.(this) ?? { this.playSynths };
};
if(~syncableDiversions.notEmpty) {
// run in a routine so we can wait for server sync
Routine {
runSyncableDiversions(this);
play.();
}.()
} {
play.();
}
} // otherwise drop the event.
}
}
Expand Down Expand Up @@ -218,6 +230,27 @@ DirtEvent {

}


runSyncableDiversions { |dirtEvent|
var postSyncCallbacks;
// call all the diversions, gathering all the resulting post-sync
// functions.
postSyncCallbacks = ~syncableDiversions.collect { |div|
div.(orbit, this);
}.select { |cb| cb.notNil };

if(postSyncCallbacks.notEmpty) {
// wait for the server to finish adding the synthdef(s). this will
// eat into the time buffer provided by our latency setting, but
// it should be fine with typical latency settings.
server.sync;

// adjust the latency value to compensate for the time spent
// syncing.
~latency = ~timeStamp - thisThread.seconds;

// run all the post-sync functions
postSyncCallbacks.do { |cb| cb.() };
};
}
}

193 changes: 193 additions & 0 deletions classes/DirtLiveDsp.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
DirtLiveDsp {
var <dirt, <gdspSource, <>defaultGdspSynth, <>gdspDiversion, <>dspDiversion, <allowDangerousRemoteCodeExecution;

*new { |dirt|
^super.newCopyArgs(dirt).init
}

init {
gdspSource = nil ! dirt.orbits.size;

// the default no-effect global effect synth
defaultGdspSynth = { |dryBus, effectBus, gate=1|
var dry, wet, sum;
dry = In.ar(dryBus, dirt.numChannels);
wet = In.ar(effectBus, dirt.numChannels);
EnvGen.kr(Env.asr, gate, doneAction: Done.freeSelf);
DirtPause.ar(sum, graceTime: 4);
};

gdspDiversion = { |o, dirtEvent|
var gdspEffect;
// if the effect code has changed since the last event received,
// recreate the effect using the new code.
if(~gdsp != gdspSource[o.orbitIndex]) {
// remember the code so we can keep this synth running until the
// code changes.
gdspSource[o.orbitIndex] = ~gdsp;

"redefine %".format("dirt_live_global_dsp_%_%".format(o.orbitIndex, ~numChannels)).postln;
SynthDef("dirt_live_global_dsp_%_%".format(o.orbitIndex, ~numChannels),
if(~gdsp.notNil) {
// if we have some code, wrap it in a function
// definition to provide the dry signal in the 'in'
// variable, and interpret.
//
// the newline before the closing bracket allows the
// synth code to include single-line comments.
{ |dryBus, effectBus, gate=1|
var dry, wet, sig;
dry = In.ar(dryBus, ~numChannels);
// wet = In.ar(effectBus, ~numChannels);
sig = "{ |in, dryBus, effectBus| %\n}".format(~gdsp.asString).interpret.(dry, dryBus, effectBus);
sig = sig * EnvGen.kr(Env.asr, gate, doneAction: Done.freeSelf);
ReplaceOut.ar(dryBus, sig);
}
} {
// if we have no code, restore the default no-effect synth
defaultGdspSynth
}
).add;

gdspEffect = o.globalEffects.detect { |fx| fx.name.asString.beginsWith("dirt_live_global_dsp_") };

// once any new synthdefs are ready...
{
// we want to allow the ~gdsp code to use event variables as
// controls in NamedControl style, e.g. \freq.kr. this requires
// specifying the control names in the GlobalDirtEffect's
// paramNames, which we can do automagically with the help of
// SynthDescLib!
gdspEffect.paramNames = SynthDescLib.global[(gdspEffect.name.asString ++ ~numChannels).asSymbol].controls.collect(_.name);

// finally, start the effect synth
gdspEffect.play(o.group, o.outBus, o.dryBus, o.globalEffectBus, o.orbitIndex);
}
};
};

dspDiversion = { |o, dirtEvent|
if(~dsp.notNil) {
// generate temporary synthdef name. by default, these run from
// 'temp__0' to 'temp__511' and then loop back, so old names
// eventually get reused and we dont accumulate synthdefs
// indefinitely.
~dspSynthDef = SystemSynthDefs.generateTempName.asSymbol;

// build the synthdef. this synth will run after conventional
// SuperDirt synths specified with 's' (e.g. dirt_sample), and
// can process their output!
SynthDef(~dspSynthDef, { |out, pan|
var in, sig;
// wrap the code to be interpreted in a function definition
// to provide two special variables:
//
// - out: output (and input) bus
// - in: input signal from the previous synth
//
// everything else is accessible via the event, e.g. ~freq.
in = In.ar(out, ~numChannels);
sig = "{ |in, out| %\n}".format(~dsp.asString).interpret.(in, out);
sig = DirtPan.ar(sig, ~numChannels, pan);
ReplaceOut.ar(out, sig);
}).add;
};

// once any new synthdefs are ready...
{
// we have nothing to do here, as the default SuperDirt path
// will play the synths afterwards. but we still need to return
// a callback to signal that a sync is needed.
//
// on playback, the synthdef name stored in ~dspSynthDef will
// activate the 'live-dsp' module, defined in the 'start' method.
}
};

allowDangerousRemoteCodeExecution = false;
}

start {
var event;
if(allowDangerousRemoteCodeExecution.not && { this.listeningOnLoopback.not }) {
"Refusing to start DirtLiveDsp, because SuperDirt seems to listening on a non-loopback network address. This would allow anyone who can send OSC messages to SuperDirt to run arbitrary code on your system. If you really want to do this, please ensure every device on your network is trusted, and call enableDangerousRemoteCodeExecution(true)".error;
} {
dirt.orbits.do { |o|
event = o.defaultParentEvent;

// handle ~gdsp (code for the orbit's global effect synthdef)
event[\syncableDiversions] = event[\syncableDiversions].add(gdspDiversion);

// handle ~dsp (code for the module synthdef)
event[\syncableDiversions] = event[\syncableDiversions].add(dspDiversion);
};

// define the module which will play our temporary synthdefs.
dirt.addModule('live-dsp', { |dirtEvent|
var args, val;

// support NamedControls (e.g. \cutoff.kr) by detecting all the
// controls used in the ~dsp code, and passing their values as
// arguments.
args = SynthDescLib.global[~dspSynthDef].controls.collect { |c|
val = currentEnvironment[c.name];
[c.name, val]
}.flatten;

dirtEvent.sendSynth(~dspSynthDef, args);
}, { ~dspSynthDef.notNil });

dirt.orderModules(['sound', 'live-dsp']);

// set up global effects
{
// even if our ~dsp code does not use an input signal, a conventional synth
// needs to be specified in 's', otherwise Tidal will not send the event at all.
// thus, it is convenient to have a silence synthdef.
SynthDef(\dirt_silence, { |out|
FreeSelf.kr(1);
Out.ar(out, Silent.ar(dirt.numChannels));
}).add;

// initialize livecodable global effects with the default no-effect synth
dirt.orbits.do { |o|
// each orbit gets its own synthdef name
SynthDef("dirt_live_global_dsp_%_%".format(o.orbitIndex, dirt.numChannels).asSymbol, defaultGdspSynth).add;
};

// wait for synthdefs to be added
dirt.server.sync;

// create effects (or recreate if they already exist)
dirt.orbits.do { |o|
var insertIx, effect;
o.globalEffects = o.globalEffects.reject { |fx|
if(fx.name.asString.beginsWith("dirt_live_global_dsp_")) {
"release %".format(fx).postln;
fx.release;
true;
} {
false;
};
};
effect = GlobalDirtEffect("dirt_live_global_dsp_%_".format(o.orbitIndex).asSymbol, []).alwaysRun_(true);
o.globalEffects = o.globalEffects.insert(0, effect);
o.initNodeTree;
};

"DirtLiveDsp started".postln;
}.forkIfNeeded;
}
}

enableDangerousRemoteCodeExecution { |enable|
if(enable) {
"DirtLiveDsp remote code execution is enabled. This is an insecure configuration that allows anyone who can send OSC messages to SuperDirt to run arbitrary code on your system. Please ensure every device on your network is trusted.".warn;
};
allowDangerousRemoteCodeExecution = enable;
}

listeningOnLoopback {
^dirt.senderAddr.hostname == "127.0.0.1"
}
}
1 change: 1 addition & 0 deletions classes/DirtOrbit.sc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ DirtOrbit {
"no synth or sample named '%' could be found.".format(~s).postln;
};

~syncableDiversions = [];
}
}

Expand Down
21 changes: 21 additions & 0 deletions hacks/livecoding-dsp.scd
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*

livecode DSP graphs!

(see livecoding-dsp.tidal for the corresponding Tidal code)

since OSC messages can contain arbitrary strings, we can put sclang code in
them and .interpret it to build synth graphs on the fly.

!!! WARNING !!!
this is a huge security hole if you configure SuperDirt to listen on a non-
loopback network interface - anyone who can send you OSC will be able to
execute arbitrary code on your system.

*/

// once SuperDirt has started, we can enable DirtLiveDsp
(
~liveDsp = DirtLiveDsp(~dirt);
~liveDsp.start;
)
58 changes: 58 additions & 0 deletions hacks/livecoding-dsp.tidal
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{-

livecode DSP graphs!

(see livecoding-dsp.scd for the corresponding SuperCollider code)

-}


-- define a couple of helpers:

dsp' code = pS "dsp" code # s "dirt_silence"
dsp code = dsp' (pure code)
gdsp' code = pS "gdsp" code
gdsp code = gdsp' (pure code)

-- and write some patterns:

-- basic synth with some sclang randomness
d1
$ note "<<7 <10 5>> <3!2 -5> 2 0>*<8!3 16 16>*16"
# dsp "VarSaw.ar(~freq, rrand(0.0, 1.0), rrand(0.0, 1.0))"
# attack 0.001 # release (rangex 0.1 0.5 rand)
# delay 0.5 # delayt (3.0/16) # lock 1 # delayfb 0.5
# gain 0.8 # pan (fast 1.1 rand)

-- sampler fx
d2
$ n "0@2 1@2 2@2 3 4 5 6 7 8 2@2 10 4"
# dsp "(in + DelayC.ar(in, 0.01, (~cycle % 2pi).sin.linexp(-1, 1, 0.001, 0.01)) * 3).scurve"
# s "amencutup" -- needs to go after dsp or it will be overridden by the default "silence"

-- patterned synths and string concatenation
d3 $ let
addFilter code = "RLPF.ar(" ++ code ++ ", \\ctf.kr, 0.2)"
synths =
[ ("saw", pure $ addFilter "Saw.ar(~freq)")
, ("sqr", pure $ addFilter "Pulse.ar(~freq)")
]
in id
$ note ("[0@2 -12@3 0@3]*2" - 24)
# dsp' (inhabit synths "<saw sqr!2>*8")
# release 0.3
# pF "ctf" (rangex 400 8000 rand)

-- global effect with patterned controls
d4
$ ply (choose [1,2])
$ n "<0 1 2 3 1 2 3>*8"
# s "dr55"
# speed (rangex 1.5 3 $ fast 1.5 rand)
# gdsp "in + (RHPF.ar(CombC.ar(in * \\delAmt.kr, 1, Saw.ar(SinOsc.ar(0.1).exprange(0.2, 9)).exprange(0.07, 0.01), \\delDecay.kr) * 0.5, \\delHpf.kr, 0.5))"
# pF "delAmt" rand
# pF "delDecay" (range 1 5 $ fast 1.1 rand)
# pF "delHpf" (rangex 150 400 $ fast 1.2 rand)
# pan (fast 1.3 rand)
# shape 0.8