Skip to content

Instantly share code, notes, and snippets.

@caseyanderson
Last active February 8, 2026 21:55
Show Gist options
  • Select an option

  • Save caseyanderson/1c1aea83762b0801830834fc2ef88eec to your computer and use it in GitHub Desktop.

Select an option

Save caseyanderson/1c1aea83762b0801830834fc2ef88eec to your computer and use it in GitHub Desktop.
/* Deterministic Synth Control
Synth.new with Busses
*/
(
SynthDef(\sin, { |amp = 0.5, end = 2, freq = 0.5, len = 5, max = 0.7, maxdelay = 1, min = 0.2, out = 0, start = 1, trig = 1|
var freqHz = freq.linlin(0,1, 50.0, 500.0);
var ampScaled = if(amp < 0.1, 0.0, amp.lincurve(0,1, 0.0, 1.0, -4));
var env = EnvGen.kr(Env.perc(len * 0.25, len * 0.75), trig, doneAction:2);
var line = Line.kr(start, end, len, doneAction:2);
var sig = SinOsc.ar(freqHz * DelayN.kr(line, maxdelay, Rand(min, max)), 0, ampScaled) * env;
Out.ar(out, Pan2.ar(sig * 0.25));
}).add;
SynthDef(\formant, { |amp = 0.5, end = 2, freq = 0.5, len = 5, max = 0.7, maxdelay = 1, min = 0.2, out = 0, start = 1, trig = 1|
var freqHz = freq.linlin(0,1, 50.0, 500.0);
var ampScaled = if(amp < 0.1, 0.0, amp.lincurve(0,1, 0.0, 1.0, -4));
var env = EnvGen.kr(Env.perc(len * 0.25, len * 0.75), trig, doneAction:2);
var line = XLine.kr(start, end, len, doneAction:2);
var sig = Formant.ar(
freqHz * DelayN.kr(line, maxdelay, Rand(min, max)),
freqHz * DelayN.kr(line, maxdelay, Rand(min, max)),
freqHz * DelayN.kr(line, maxdelay, Rand(min, max)),
ampScaled
) * env;
var clean = HPF.ar(sig, 50);
Out.ar(out, Pan2.ar(clean * 0.25));
}).add;
)
// OSCFunc
(
OSCFunc({ |msg|
var idx = msg[1];
var val = msg[2];
if(val == 1) {
switch(idx,
0, {
var currentFreq = ~twisterBusses[4].getSynchronous;
var currentAmp = ~twisterBusses[0].getSynchronous;
var synth;
synth = Synth(\sin, [
\out, 0,
\start, rrand(2,13),
\end, rrand(2,13),
\len, rrand(20,50),
\trig, 1,
\freq, currentFreq,
\amp, currentAmp
]);
synth.map(\freq, ~twisterBusses[4]);
synth.map(\amp, ~twisterBusses[0]);
},
1, {
var currentFreq = ~twisterBusses[5].getSynchronous;
var currentAmp = ~twisterBusses[1].getSynchronous;
var synth;
synth = Synth(\formant, [
\out, 2,
\start, rrand(2,17),
\end, rrand(2,17),
\len, rrand(20,50),
\trig, 1,
\freq, currentFreq,
\amp, currentAmp
]);
synth.map(\freq, ~twisterBusses[5]);
synth.map(\amp, ~twisterBusses[1]);
},
2, {
var currentFreq = ~twisterBusses[6].getSynchronous;
var currentAmp = ~twisterBusses[2].getSynchronous;
var synth;
synth = Synth(\formant, [
\out, 4,
\start, rrand(2,17),
\end, rrand(2,17),
\len, rrand(20,50),
\trig, 1,
\freq, currentFreq,
\amp, currentAmp
]);
synth.map(\freq, ~twisterBusses[6]);
synth.map(\amp, ~twisterBusses[2]);
}
);
};
}, '/buttonControl').add;
)
/* SUS SOUNDS
*/
(
~reaper = NetAddr( "127.0.0.1", 8000 );
// ~synthControl = NetAddr( "127.0.0.1", NetAddr.langPort ); // generic OSC responder for internal Synths
)
// low wind
// ready for tuning
(
SynthDef( \staticWind, { | amp = 0.0, mFreq = 0.1, fFreq = 1348, fRq = 0.5, trig = 0|
var env, src, mod;
env = EnvGen.kr( Env.adsr( 4.0, 1.0, 0.95, 4.0), trig );
mod = Mix.fill( 6, { LFNoise2.ar( mFreq ).range( 0.0, 1.0 )});
src = PinkNoise.ar( mod.lagud( 1, 4 ) );
src = RLPF.ar( src, fFreq.lag( 1 ), fRq ) * env * amp;
Out.ar(0, Pan2.ar( src ) );
}).add;
// swirly wind, ready for tuning
SynthDef( \swirlyWind, { | amp = 0, maxFreq = 2e4, maxRQ = 1.5, minFreq = 20, minRQ = 0.01, trig = 0 |
var env, freq, gen, in, mul, noises, rq, sig;
env = EnvGen.kr( Env.adsr( 4.0, 2.0, 0.95, 4.0 ), trig );
gen = { rrand( 1,10 ).reciprocal }; // 1, 10
noises = [ BrownNoise.ar, WhiteNoise.ar, PinkNoise.ar ];
in = SelectX.ar( Array.fill(4, { LFNoise2.kr( gen.value ) } ).exprange( 0.02, 2 ), noises).mean;
freq = Array.fill(8, { LFNoise2.ar( gen.value ) }).exprange( minFreq, maxFreq );
rq = Array.fill(8, { LFNoise2.ar( gen.value ) }).exprange(minRQ, maxRQ); // 0.1, 2.0
mul = Array.fill(8, { LFNoise2.ar( gen.value ) }).range(0.1, 0.95);
sig = BPF.ar(in, freq, rq, mul).clump(2).mean * amp;
Out.ar(2, sig * env);
}).add;
)
///////// With OSC messages
(
// run the synths
~staticWind = Synth( \staticWind, [ \amp, 0.0, \fFreq, 100, \fRq, 0.5, \mFreq, 0.1, \trig, 0 ] );
~swirlyWind = Synth( \swirlyWind, [ \amp, 0.0, \maxFreq, 2e4, \maxRQ, 1.5, \minRQ, 0.01, \trig, 0 ] );
)
/////
(
OSCFunc( { | msg |
//msg.postln;
switch( msg[1],
// staticWind controls
0, { ~staticWind.set( \amp, msg[2] ) },
4, { ~staticWind.set( \mFreq, msg[2].linlin( 0.0, 1.0, 0.1, 100.0 ) ) },
8, { ~staticWind.set( \fFreq, msg[2].linlin( 0.0, 1.0, 50, 2000 ) ) },
12,{ ~staticWind.set( \fRq, msg[2].linlin( 0.0, 1.0, 0.1, 1.0 ) ) },
// swirlyWind controls
1, { ~swirlyWind.set( \amp, msg[2] ) },
5, { ~swirlyWind.set( \maxFreq, msg[2].linlin( 0.0, 1.0, 1000, 1e4 ) ) },
9, { ~swirlyWind.set( \maxRQ, msg[2].linlin( 0.0, 1.0, 0.55, 1.5 ) ) },
13,{ ~swirlyWind.set( \minRQ, msg[2].linlin( 0.0, 1.0, 0.01, 0.45 ) ) }
);
}, '/knobControl');
OSCFunc( { | msg |
//msg.postln;
switch( msg[1],
0, { ~staticWind.set( \trig, msg[2] ) },
1, { ~swirlyWind.set( \trig, msg[2] ) }
)
}, '/buttonControl');
)
////////////////////////////////////////////////////
x = Synth( \staticWind, [ \mFreq, 0.1, \fFreq, 100, \fRq, 0.5 ] );
x.set( \trig, 1, \amp, 0.2 );
x.set( \trig, 0 );
x.set( \fFreq, 1000 );
x.set( \fRq, 0.5 );
x.set( \amp, 0.3 );
y = Synth( \swirlyWind, [ \trig, 1, \amp, 0.5 ] );
y.set( \trig, 0 );
y.set( \amp, 0.4 );
y.set( \maxFreq, 500 );
y.set( \minRQ, 0.1 );
/*
MIDI Fighter Twister + Spectra Control Panel
Casey Anderson
Twister: CC 0-15, Channel 1 (chan=0)
Spectra: CC 36-51, Channel 4 (chan=3)
TODO:
Test Control Bus mapping in other files (should allow for patterns)
*/
(
Server.killAll;
s = Server.default;
s.options.device = "ASIO : Voicemeeter Virtual ASIO";
s.options.numOutputBusChannels = 8;
s.options.sampleRate = 48000;
s.options.blockSize = 256;
s.options.hardwareBufferSize = 256;
s.options.memSize = 65536;
s.boot;
s.waitForBoot({
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ CONSTANTS & STATE │
// └─────────────────────────────────────────────────────────────────────────────┘
~synthControl = NetAddr("127.0.0.1", NetAddr.langPort);
~knobs = (0..15);
~buttons = (0..15);
~ccToButton = Dictionary[
48 -> 0, 49 -> 1, 50 -> 2, 51 -> 3,
44 -> 4, 45 -> 5, 46 -> 6, 47 -> 7,
40 -> 8, 41 -> 9, 42 -> 10, 43 -> 11,
36 -> 12, 37 -> 13, 38 -> 14, 39 -> 15
];
~twisterLastValue = Array.fill(16, -1);
~twisterLastBusValue = Array.fill(16, 0.0);
~twisterLastSetTime = Array.fill(16, -999999.0);
~spectraLastValue = Array.fill(16, -1);
// Declare data dictionaries early so they exist when helpers use them
~twisterData = Dictionary.new;
~spectraData = Dictionary.new;
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ CONTROL BUSSES │
// └─────────────────────────────────────────────────────────────────────────────┘
~twisterBusses = ~knobs.collect { Bus.control(s, 1).set(0.0) };
~spectraBusses = ~buttons.collect { Bus.control(s, 1).set(0.0) };
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ GUI LAYOUT HELPERS │
// └─────────────────────────────────────────────────────────────────────────────┘
~twisterLayout = { |knobNum|
var statusButton = Button()
.minSize_(80@30)
.states_([
["KNOB " ++ knobNum ++ " Off", Color.black, Color.gray],
["KNOB " ++ knobNum ++ " On", Color.white, Color.red]
])
.value_(0);
var knob = Knob()
.minSize_(80@80)
.value_(0.0);
var knobValDisplay = NumberBox()
.minSize_(80@30)
.align_(\center)
.value_(0.0)
.clipLo_(0.0)
.clipHi_(1.0)
.decimals_(3);
var view = View()
.background_(Color.gray(0.9))
.layout_(
VLayout(
statusButton,
knob,
knobValDisplay
).spacing_(5).margins_([10,5,10,5])
);
IdentityDictionary[
\layout -> view,
\button -> statusButton,
\knob -> knob,
\number2 -> knobValDisplay
];
};
~spectraLayout = { |buttonNum|
var statusButton = Button()
.minSize_(80@30)
.states_([
["BUTTON " ++ buttonNum ++ " Off", Color.black, Color.gray],
["BUTTON " ++ buttonNum ++ " On", Color.white, Color.red]
])
.value_(0);
var button = Button()
.minSize_(80@80)
.states_([
["OFF", Color.black, Color.gray],
["ON", Color.white, Color.red]
])
.value_(0);
var view = View()
.background_(Color.gray(0.9))
.layout_(
VLayout(
statusButton,
button
).spacing_(5).margins_([10,5,10,5])
);
IdentityDictionary[
\layout -> view,
\button1 -> statusButton,
\button2 -> button
];
};
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ GUI CREATION & LAYOUT │
// └─────────────────────────────────────────────────────────────────────────────┘
~makeCombinedPanel = {
var twisterStrips, spectraStrips;
twisterStrips = ~knobs.collect(~twisterLayout.(_));
spectraStrips = ~buttons.collect(~spectraLayout.(_));
if(~combinedPanel.notNil, { ~combinedPanel.close });
~twisterData[\twisterStrips] = twisterStrips;
~spectraData[\spectraStrips] = spectraStrips;
~combinedPanel = Window.new("Twister + Spectra Control Panel", Rect(210.0, 65.0, 892.0, 692.0))
.alwaysOnTop_(true)
.layout_(
HLayout(
GridLayout.rows(
*twisterStrips.clump(4).collect { |row|
row.collect { |strip| strip[\layout] }
}
).hSpacing_(10).vSpacing_(10),
GridLayout.rows(
*spectraStrips.clump(4).collect { |row|
row.collect { |strip| strip[\layout] }
}
).hSpacing_(10).vSpacing_(10)
).spacing_(10)
)
.front;
};
~makeCombinedPanel.value;
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ KNOB ACTION LOGIC │
// └─────────────────────────────────────────────────────────────────────────────┘
~knobs.do { |num|
var strip = ~twisterData[\twisterStrips][num];
if(strip.notNil && strip[\knob].notNil, {
strip[\knob].action_({ |obj|
strip[\number2].value_(obj.value);
if(strip[\button].value == 1, {
var val = obj.value;
var now = thisThread.seconds;
var lastVal = ~twisterLastBusValue[num] ? 0.0;
var delta = (val - lastVal).abs;
var lastTime = ~twisterLastSetTime[num] ? -999999.0;
if(delta > 0.015 && (now - lastTime > 0.02), {
var finalVal = val;
~twisterBusses[num].set(finalVal);
~twisterLastBusValue[num] = finalVal;
~twisterLastSetTime[num] = now;
});
if(val < 0.04, {
~twisterBusses[num].set(0.0);
~twisterLastBusValue[num] = 0.0;
~twisterLastSetTime[num] = now;
});
});
});
});
};
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ MIDI CLIENT & RESPONDERS │
// └─────────────────────────────────────────────────────────────────────────────┘
MIDIClient.init;
MIDIIn.connectAll;
~twisterUID = MIDIClient.sources.detect { |src| src.name.contains("Twister") }.tryPerform(\uid);
~spectraUID = MIDIClient.sources.detect { |src| src.name.contains("Spectra") }.tryPerform(\uid);
if(~twisterUID.notNil, {
~knobs.do { |i|
MIDIFunc.cc({ |val, cc, chan, src|
if(chan == 0 && cc == i, {
var mappedVal = val.linlin(0,127,0.0,1.0);
if(~twisterLastValue[i].isNil || mappedVal != ~twisterLastValue[i], {
~twisterLastValue[i] = mappedVal;
{
var strip = ~twisterData[\twisterStrips][i];
if(strip.notNil && strip[\button].value == 1, {
strip[\knob].valueAction_(mappedVal);
~synthControl.sendMsg('/knobControl', i, mappedVal);
});
}.defer;
});
});
}, i, 0, ~twisterUID);
};
});
if(~spectraUID.notNil, {
MIDIFunc.cc({ |val, cc, chan, src|
var idx = ~ccToButton[cc];
if(idx.notNil && chan == 3, {
var mappedVal = if(val > 0, 1, 0);
if(~spectraLastValue[idx].isNil || mappedVal != ~spectraLastValue[idx], {
~spectraLastValue[idx] = mappedVal;
AppClock.sched(0, {
var strip = ~spectraData[\spectraStrips][idx];
if(strip.notNil && strip[\button1].value == 1, {
strip[\button2].valueAction_(mappedVal);
~synthControl.sendMsg('/buttonControl', idx, mappedVal);
});
});
});
});
}, nil, 3, ~spectraUID);
});
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ CLEANUP │
// └─────────────────────────────────────────────────────────────────────────────┘
~combinedPanel.onClose_({
~twisterBusses.do(_.free);
~spectraBusses.do(_.free);
MIDIIn.disconnectAll;
});
});
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment