Skip to content

Instantly share code, notes, and snippets.

@jamshark70
Last active June 12, 2021 10:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamshark70/7571ef65cb3aef58ff14b014b672c91b to your computer and use it in GitHub Desktop.
Save jamshark70/7571ef65cb3aef58ff14b014b672c91b to your computer and use it in GitHub Desktop.
A prototype for automatically mapping control synths onto other synth parameters
s.boot;
// a kr synth
(
SynthDef(\ctlEnv, { |out, levelScale = 1, levelBias = 0, time = 1, connect = 1|
var env = \env.kr(Env.newClear(12).asArray);
var init = In.kr(out, 1);
var start = Select.kr(connect, [env[0], init]);
Out.kr(out, EnvGen.kr(env, 1, levelScale, levelBias, time, doneAction: 2));
}).add;
)
// use an envelope to control pan
(
(type: \notemap,
pan: (
instrument: \ctlEnv,
env: Env([-1, 1, -1, 1], [1, 1, 1] / 3),
time: 5,
addAction: \addBefore
),
sustain: 5
).play;
)
(
(type: \notemap,
pan: (
instrument: \ctlEnv,
env: Env([-1, 1, -1, 1], [1, 1, 1] / 3),
time: 5,
addAction: \addBefore
),
// ok, 'detunedFreq' is a bit ugly...
detunedFreq: (
instrument: \ctlEnv,
env: Env(
Array.fill(8, { exprand(200, 1000) }),
Array.fill(7, { rrand(1.0, 5.0) }).normalizeSum,
\exp
),
time: 5,
addAction: \addBefore
),
sustain: 5,
amp: 0.3
).play;
)
(
(type: \notemap,
pan: Array.fill(2, { |i|
(
instrument: \ctlEnv,
env: Env([-1, 1, -1, 1].rotate(i), [1, 1, 1] / 3),
time: 5,
addAction: \addBefore
)
}),
detunedFreq: Array.fill(2, { (
instrument: \ctlEnv,
env: Env(
Array.fill(8, { exprand(200, 1000) }),
Array.fill(7, { rrand(1.0, 5.0) }).normalizeSum,
\exp
),
time: 5,
addAction: \addBefore
) }),
sustain: 5,
amp: 0.3
).play;
)
// demo of modulation
(
SynthDef(\testMod, { |out, gate = 1, amp = 0.1|
var eg = EnvGen.kr(Env.asr(0.01, 1, 0.1), gate, doneAction: 2);
var freq = ModControl.kr(\freq, 440, \exp);
Out.ar(out, (SinOsc.ar(freq) * (eg * amp)).dup);
}).add;
SynthDef(\lfo, { |out, gate = 1, rate = 2|
FreeSelf.kr(gate <= 0);
Out.kr(out, LFTri.kr(rate))
}).add;
SynthDef(\ctlEnv, { |out, levelScale = 1, levelBias = 0, time = 1, connect = 1|
var env = \env.kr(Env.newClear(12).asArray);
var init = In.kr(out, 1);
var start = Select.kr(connect, [env[0], init]);
Out.kr(out, EnvGen.kr(env, 1, levelScale, levelBias, time/*, doneAction: 2*/));
}).add;
)
SynthDescLib.at(\testMod);
Controls:
ControlName P 0 out control 0.0
ControlName P 1 gate control 1.0
ControlName P 2 amp control 0.10000000149012
ControlName P 3 freq control 440.0
ControlName P 4 freqMod control 1.0 <<-- added for you
ControlName P 5 freqModDepth control 2.0 <<-- added for you
O audio Out out 2
(
(
type: \notemap,
instrument: \testMod,
degree: 2,
amp: 0.5,
freqMod: (instrument: \lfo, addAction: \addBefore),
freqModDepth: (
instrument: \ctlEnv,
env: Env([1, 1.5], [2], 4),
time: 1,
addAction: \addBefore
),
sustain: 3
).play;
)
/* hjh 6/12/2021 */
ModControl {
var control, modulator, modDepth;
*new { |rate = \control, name, default, warp, modulator, modDepth|
^super.new.init(rate, name, default, warp, modulator, modDepth)
}
*ar { |name, default = 0, warp = \lin, modulator, modDepth|
^this.new(\audio, name, default, warp, modulator, modDepth)
}
*kr { |name, default = 0, warp = \lin, modulator, modDepth|
^this.new(\control, name, default, warp, modulator, modDepth)
}
init { |rate, name, default, warp, aModulator, aModDepth|
control = NamedControl.perform(
UGen.methodSelectorForRate(rate), name, default
);
if(aModulator.isUGen.not) {
modulator = NamedControl.perform(
this.methodSelectorForRate,
(name ++ "Mod").asSymbol,
if(aModulator.isNil) {
(lin: 0, exp: 1, linear: 0, exponential: 1).at(warp) ?? { 0 }
} { aModulator };
);
} { modulator = aModulator };
if(aModDepth.isUGen.not) {
modDepth = NamedControl.perform(
this.methodSelectorForRate,
(name ++ "ModDepth").asSymbol,
if(aModDepth.isNil) {
(lin: 1, exp: 2, linear: 1, exponential: 2).at(warp) ?? { 1 }
} { aModDepth };
);
} { modDepth = aModDepth };
^control.modulate(warp, modulator, modDepth, name);
}
methodSelectorForRate {
^control.methodSelectorForRate
}
}
+ UGen {
// warp must be \lin or \exp for now
// it appears to be difficult
// to extract only the math formulas from Spec and Warp
// without also importing all the clip, round etc. logic
// I don't have time to deal with excessive tight coupling
// this morning. Somebody else refactor it and then
// other warps could be supported
modulate { |warp, modulator, modDepth, name|
if(modulator.isUGen.not) {
modulator = NamedControl.perform(
this.methodSelectorForRate,
(name ++ "Mod").asSymbol,
modulator
);
};
if(modDepth.isUGen.not) {
modDepth = NamedControl.perform(
this.methodSelectorForRate,
(name ++ "ModDepth").asSymbol,
modDepth
);
};
case
{ #[lin, linear].includes(warp) } {
^(modDepth * modulator) + this
}
{ #[exp, exponential].includes(warp) } {
^(modDepth ** modulator) * this
}
{ Error("Unsupported warp '%' for modulation".format(warp)).throw };
}
}
// OutputProxy doesn't know its rate??? really???
+ OutputProxy {
rate { ^source.rate }
methodSelectorForRate {
^UGen.methodSelectorForRate(this.rate)
}
}
TempBus : Bus {
var conditions;
var cond;
*new { arg rate = \audio, index = 0, numChannels = 2, server;
^super.new(rate, index, numChannels, server).tempBusInit;
}
tempBusInit {
conditions = Array.new;
cond = Condition {
conditions.every { |condfunc| condfunc.value }
}
}
setStop { |test, action, clock(thisThread.clock)|
conditions = conditions.add(test);
^Routine {
cond.wait;
action.value;
this.free;
}.play(clock)
}
stopOnNodeEnd { |node, action|
var responder = SimpleController(node)
.put(\n_end, { cond.signal });
NodeWatcher.register(node);
^this.setStop({ node.isPlaying.not }, action.addFunc({ responder.remove }));
}
signal { cond.signal }
*makeMapNodeForEvent { |event|
var synthLib, desc, outdesc;
var bus;
synthLib = event[\synthLib] ?? { SynthDescLib.global };
desc = synthLib.at(event[\instrument]);
outdesc = desc.outputs.detect { |desc|
desc.startingChannel == \out
};
if(outdesc.isNil) {
Error("SynthDef '%' for mapping event has no 'out' control".format(event[\instrument])).throw;
};
bus = this.perform(outdesc.rate, event[\server], outdesc.numberOfChannels);
event.put(\out, bus);
// stop condition depends on parent synth, not this event's synth
// so just give this object back
^bus
}
*initClass {
// sadly the default event type is not even close to well-factored
// so the only way to extend it is to copy it wholesale :-\
Event.addEventType(\notemap, { |server|
var freqs, lag, strum, sustain;
var bndl, addAction, sendGate, ids;
var msgFunc, instrumentName, offset, strumOffset;
var playingNodeCount;
var allMaps;
freqs = ~detunedFreq.value;
// msgFunc gets the synth's control values from the Event
msgFunc = ~getMsgFunc.valueEnvir;
instrumentName = ~synthDefName.valueEnvir;
// determine how to send those commands
// sendGate == false turns off releases
sendGate = ~sendGate ? ~hasGate;
// update values in the Event that may be determined by functions
~freq = freqs;
~amp = ~amp.value;
~sustain = sustain = ~sustain.value;
lag = ~lag;
offset = ~timingOffset;
strum = ~strum;
~server = server;
~isPlaying = true;
addAction = Node.actionNumberFor(~addAction);
// compute the control values and generate OSC commands
bndl = msgFunc.valueEnvir;
bndl = [9 /* \s_new */, instrumentName, ids, addAction, ~group] ++ bndl;
if(strum == 0 and: { (sendGate and: { sustain.isArray })
or: { offset.isArray } or: { lag.isArray } }) {
bndl = flopTogether(
bndl,
[sustain, lag, offset]
);
#sustain, lag, offset = bndl[1].flop;
bndl = bndl[0];
} {
bndl = bndl.flop
};
// produce a node id for each synth
playingNodeCount = bndl.size;
~id = ids = Array.fill(bndl.size, { server.nextNodeID });
bndl = bndl.collect { | msg, i |
msg[2] = ids[i];
(6, 8 .. msg.size-1).do { |j|
var map, event;
if(msg[j].isKindOf(Event)) {
event = msg[j].copy;
map = TempBus.makeMapNodeForEvent(event); // sets 'out'
allMaps = allMaps.add(event);
map.setStop(
test: { playingNodeCount <= 0 },
action: { event.put(\type, \off).play }
);
event.put(\group, ids[i]);
msg[j] = map.asMap;
};
};
OSCFunc({
playingNodeCount = playingNodeCount - 1;
allMaps.do { |mapEvent| mapEvent[\out].signal };
}, '/n_end', server.addr, argTemplate: [ids[i]]).oneShot;
msg.asOSCArgArray
};
// schedule when the bundles are sent
if (strum == 0) {
~schedBundleArray.(lag, offset, server, bndl, ~latency);
if (sendGate) {
~schedBundleArray.(
lag,
sustain + offset,
server,
[15 /* \n_set */, ids, \gate, 0].flop,
~latency
);
}
} {
if (strum < 0) {
bndl = bndl.reverse;
ids = ids.reverse
};
strumOffset = offset + Array.series(bndl.size, 0, strum.abs);
~schedBundleArray.(
lag, strumOffset, server, bndl, ~latency
);
if (sendGate) {
if (~strumEndsTogether) {
strumOffset = sustain + offset
} {
strumOffset = sustain + strumOffset
};
~schedBundleArray.(
lag, strumOffset, server,
[15 /* \n_set */, ids, \gate, 0].flop,
~latency
);
}
};
allMaps.do(_.play);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment