Skip to content

Instantly share code, notes, and snippets.

@swannodette
Forked from scztt/Twister.sc
Created March 21, 2019 20:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save swannodette/30b42349ac040128050cc4ba44ee8f7f to your computer and use it in GitHub Desktop.
Save swannodette/30b42349ac040128050cc4ba44ee8f7f to your computer and use it in GitHub Desktop.
DummyMIDI {
var dummy = 0;
*new {
^super.newCopyArgs();
}
noteOn {}
noteOff {}
control {}
sysex {}
cc { ^127 }
channel {^16}
latency_{
dummy = 0;
}
}
PropertyAnimator {
classvar fps, animators, finished, routine;
var obj, prop, fps;
var getter, setter, start, end, dur, fps, currentTime, endTime;
var routine;
*new {
|obj, prop, fps=15|
^this.newCopyArgs(obj, prop, fps).init;
}
init {
getter = prop.asSymbol;
setter = prop.asSymbol.asSetter;
currentTime = 0;
}
set {
|value, inDur=0|
dur = inDur;
if (dur == 0) {
this.stop();
this.prSet(value);
} {
if (value.isArray) {
#start, end = value;
} {
start = obj.perform(getter);
end = value;
};
this.start();
}
}
prSet {
|value|
obj.perform(setter, value);
}
start {
routine.stop();
routine = Routine({
| startTime |
currentTime = startTime;
endTime = startTime + dur;
while { currentTime < endTime } {
obj.perform(setter, currentTime.linlin(startTime, endTime, start, end));
currentTime = fps.reciprocal.wait;
};
obj.perform(setter, end);
});
routine.play(AppClock);
}
stop {
routine.stop();
routine = nil;
}
}
Twister {
var <device;
var <knobs;
var <deviceConnections;
*new {
|device|
^super.new.init.connect(device);
}
init {
knobs = 16.collect {
TwisterKnob()
}
}
rows {
|x, y|
var result = knobs.clump(4);
if (x.notNil) {
result = result[x];
if (y.notNil) {
result = result[y];
}
};
^result;
}
cols {
|y, x|
var result = knobs.clump(4);
if (y.notNil) {
result = result[y];
if (x.notNil) {
result = result[x];
}
};
^result;
}
connect {
|inDevice|
if (inDevice.isKindOf(Symbol)) {
inDevice = TwisterDevice(inDevice);
};
if (inDevice != device) {
if (device.notNil) {
this.disconnect();
};
device = inDevice;
if (device.notNil) {
device.changed(\modelConnected, this);
"Connecting TwisterDevice(%) to Twister(%)".format(device.identityHash, this.identityHash).postln;
knobs.do({
|knob, i|
knob.connect(device.knobs[i])
});
deviceConnections = ConnectionList [
device.signal(\deviceConnected).connectTo(this.methodSlot(\onDeviceConnected)),
device.signal(\modelConnected).connectTo(this.methodSlot("onModelConnected(value)")),
];
}
}
}
disconnect {
if (device.notNil) {
"Disconnecting TwisterDevice(%) from Twister(%)".format(device.identityHash, this.identityHash).postln;
device = nil;
knobs.do(_.disconnect());
};
deviceConnections.free.clear;
}
updateDevice {
knobs.do(_.updateDevice())
}
onDeviceConnected {
this.updateDevice();
}
onModelConnected {
|twisterObj|
if (twisterObj != this) {
this.disconnect();
}
}
}
TwisterKnob {
classvar <>animator;
classvar <>animations, <>pausedAnimations;
var <device;
var <knobConnections, <buttonConnections;
var <ledColor, <buttonState=\up, enabled=false;
var <hueAnimator, <brightnessAnimator, dimAmt = 0.5, fadeoutCollapse;
var deviceConnections, knobConnections, buttonConnections;
var <>toggle = false;
var <knobCV, <buttonCV;
var knobInputOverflow = 0;
var <>knobScale = 0.005;
var animTarget, animRate, resumeAnim;
*new {
|device|
^super.new.init().connect(device)
}
init {
animations = animations ?? { IdentitySet() };
pausedAnimations = animations ?? { IdentitySet() };
ledColor = Color.blue(0.9);
this.buttonCV = OnOffControlValue(\off);
resumeAnim = Collapse({
this.class.pausedAnimations.remove(this);
this.class.animations.add(this);
}, 2);
}
connect {
|inDevice|
if (inDevice.isKindOf(Symbol)) {
inDevice = TwisterDevice(inDevice);
};
if (inDevice != device) {
if (device.notNil) {
this.disconnect();
};
device = inDevice;
if (inDevice.notNil) {
// "Connecting device(%) to knob(%)".format(device.identityHash, this.identityHash).postln;
device = inDevice;
hueAnimator = PropertyAnimator(device, \ledHue);
brightnessAnimator = PropertyAnimator(device, \ledBrightness);
deviceConnections = ConnectionList [
device.signal(\knobRelative).connectTo(this.methodSlot("onKnobRelative(value)")),
device.signal(\button).connectTo(this.methodSlot("onButton(value)")),
];
this.updateDevice();
}
}
}
disconnect {
device.ledBrightness = 0;
device.value = 0;
deviceConnections.free;
hueAnimator = brightnessAnimator = deviceConnections = device = nil;
}
onKnob {
|value|
knobCV !? { knobCV.input = value };
if (animTarget.notNil) {
this.class.animations.remove(this);
this.class.pausedAnimations.add(this);
resumeAnim.value();
}
}
onKnobRelative {
|value|
var input;
knobCV !? {
input = (knobCV.input + knobInputOverflow + (value * knobScale)).clip(0, 1);
knobCV.input = input;
knobInputOverflow = input - knobCV.input;
};
if (animTarget.notNil) {
this.class.animations.remove(this);
this.class.pausedAnimations.add(this);
resumeAnim.value();
}
}
onButton {
|value|
buttonCV !? {
if (toggle) {
if (value == \on) {
buttonCV.toggle;
}
} {
buttonCV.value = value
}
};
}
knobCV_{
|cv|
knobCV = cv;
knobInputOverflow = 0;
knobConnections.free.clear;
if (knobCV.notNil) {
knobConnections.add(
knobCV.signal(\value).connectTo(this.methodSlot("updateValue"))
);
this.enable();
} {
this.disable();
}
}
buttonCV_{
|cv|
buttonCV = cv;
buttonConnections.free.clear;
if (buttonCV.notNil) {
buttonConnections.add(
buttonCV.signal(\value).connectTo(this.methodSlot("updateLed"))
)
}
}
mapTo {
|target, cvClass=(BusControlValue)|
var def, spec;
if (target.isKindOf(SynthArgSlot).not) {
Error("'target' must be a SynthArgSlot.").throw
};
spec = target.spec ?? ControlSpec(0, 1, default:target.synth.defArgAt);
if (knobCV.isNil) {
knobCV = BusControlValue(spec:spec);
} {
knobCV.spec = spec;
};
target.synth.map(target.argName, knobCV.asMap);
}
enable {
enabled = true;
this.updateDevice();
}
disable {
enabled = false;
this.updateDevice();
}
ledColor_{
|inColor|
ledColor = inColor;
this.updateDevice();
}
brightenLed {
brightnessAnimator.set(ledColor.asHSV[2], 0);
}
dimLed {
brightnessAnimator.set(ledColor.asHSV[2] * dimAmt, 0.2);
}
updateDevice {
this.updateValue();
this.updateLed();
}
updateValue {
if (device.notNil) {
if (knobCV.notNil) {
device.value = knobCV.input;
device.ringBrightness = knobCV.input.linlin(0, 1, 0.3, 0.8);
} {
device.value = 0;
device.ringBrightness = 0;
};
}
}
updateLed {
var hsv = ledColor.asHSV;
if (device.notNil) {
if (buttonCV.value == \on) {
hueAnimator.set(hsv[0], 0.0);
brightnessAnimator.set(hsv[2], 0.0);
} {
if (enabled) {
hueAnimator.set(hsv[0], 0.2);
brightnessAnimator.set(hsv[2] * dimAmt, 0.2);
} {
hueAnimator.set(hsv[0], 0.2);
brightnessAnimator.set(0, 0.2);
}
}
}
}
slew {
|target, duration|
animTarget = target;
animRate = (knobCV.input - knobCV.spec.unmap(target)).abs / duration;
this.class.animations = this.class.animations.add(this);
this.class.animator ?? {
this.class.animator = Routine({
while { (this.class.animations.size + this.class.pausedAnimations.size) > 0 } {
this.class.slewAnimate();
0.05.wait;
};
this.class.animator = nil;
});
this.class.animator.play;
};
}
*slewAnimate {
animations.copy.do({ |k| k.slewAnimate(0.05) });
}
slewAnimate {
|delta|
var maxChange = animRate * delta;
var deltaAmt;
knobCV !? {
if (knobCV.input < animTarget) {
knobCV.input = (knobCV.input + min(maxChange, animTarget - knobCV.input));
} {
knobCV.input = (knobCV.input - min(maxChange, (animTarget - knobCV.input).abs));
};
if ((knobCV.input - animTarget).abs < 0.001) {
knobCV.input = animTarget;
this.class.animations.remove(this);
animTarget = nil; animRate = nil;
}
}
}
}
TwisterDevice : Singleton {
classvar <endpointDevice="Midi Fighter Twister %", <endpointName="Midi Fighter Twister";
classvar <deviceChangedConnection;
classvar registeredDevices;
var <knobs;
var <endpoint;
*initClass {
Class.initClassTree(MIDIWatcher);
registeredDevices = ();
deviceChangedConnection = ConnectionList();
this.registerDevice(\default, endpointDevice.format(1), endpointName);
this.registerDevice(\secondary, endpointDevice.format(2), endpointName)
}
*registerDevice {
|name, endpointDevice, endpointName|
if (registeredDevices[name].isNil) {
registeredDevices[name] = [endpointDevice, endpointName];
deviceChangedConnection.add(
MIDIWatcher.deviceSignal(endpointDevice, endpointName).connectTo(
this.methodSlot("deviceChanged(%, changed, value)".format("\\" ++ name))
)
)
} {
"TwisterDevice % is already registered.".format(name).error;
}
}
*deviceChanged {
|deviceName, changeType, endpoint|
if (changeType == \sourceAdded) {
"Added % MIDI Fighter Twister [%]".format(deviceName, endpoint.uid).postln;
TwisterDevice(deviceName, endpoint);
{ TwisterDevice(deviceName).connectAnimation(); }.defer(0.5)
};
if (changeType == \sourceRemoved) {
"Removed MIDI Fighter Twister [%]".format(endpoint.uid).postln;
}
}
init {
knobs = 16.collect {
|i|
TwisterDeviceKnob(-1, DummyMIDI(), i)
};
}
connectAnimation {
var k = this.knobs.collect(TwisterDeviceKnob(_));
fork {
var dur = 1.5;
(0,0.02..dur).do {
|seconds|
k.do {
|knob, i|
var hue, brightness;
i = (i / 3).floor * 3;
hue = (seconds / dur).pow(1.1 + (i / k.size)) * 1.5;
brightness = (seconds / dur).pow(2.4 - (i / k.size * 2.2));
knob.ledColor = Color.hsv(hue % 1, 1, brightness % 1);
};
0.02.wait;
};
0.1.wait;
(0,0.02..0.5).do {
|n|
k.do({|k| k.ledBrightness = (0.5 - n) * 2 });
0.02.wait;
}
}
}
set {
|inEndpoint|
var midiInUid, midiOut;
endpoint = inEndpoint;
if (endpoint.isNil) {
midiInUid = 0;
midiOut = DummyMIDI();
} {
MIDIIn.connectAll();
midiInUid = endpoint.uid;
midiOut = MIDIOut.newByName(endpoint.device, endpoint.name).latency_(0);
knobs.do {
|knob|
knob.setMidiDevices(midiInUid, midiOut);
};
knobs.do(_.off());
};
this.changed(\deviceConnected);
}
rows {
|x, y|
var result = knobs.clump(4);
if (x.notNil) {
result = result[x];
if (y.notNil) {
result = result[y];
}
};
^result;
}
cols {
|y, x|
var result = knobs.clump(4);
if (y.notNil) {
result = result[y];
if (x.notNil) {
result = result[x];
}
};
^result;
}
knob {
|x, y|
^this.knobRows(x, y)
}
}
TwisterDeviceKnob {
classvar brightnessChan=2, brightnessScale=#[17, 47],
ringBrightnessChan=2, ringBrightnessScale=#[65, 95],
hueChan=1, hueScale=#[1, 126],
knobChan=0, buttonChan=1;
var <cc, <midiInUid, <midiOut,
<isOn = false, buttonFunc, knobFunc;
var <ledHue=0, <ledBrightness=0, <ringBrightness=1;
*new {
arg midiInUid, midiOut, cc;
var obj = super.newCopyArgs(cc).setMidiDevices(midiInUid, midiOut);
CmdPeriod.add(obj);
^obj;
}
*from {
arg otherDevice;
^this.new(otherDevice.midiInUid, otherDevice.midiOut, otherDevice.cc);
}
setMidiDevices {
|uid, inMidiOut|
if (midiInUid != uid) {
midiInUid = uid;
};
this.makeMIDIFuncs();
midiOut = inMidiOut ?? midiOut;
}
doOnCmdPeriod {
this.makeMIDIFuncs();
}
makeMIDIFuncs {
buttonFunc.free; knobFunc.free;
buttonFunc = MIDIFunc.cc({
|val|
this.changed(\button, (val == 0).if(\off, \on));
}, cc, buttonChan, srcID:midiInUid);
knobFunc = MIDIFunc.cc({
|value|
this.changed(\knobRelative, value - 64);
}, cc, knobChan, srcID:midiInUid);
}
ledBrightness_{
|brightness|
ledBrightness = brightness;
midiOut !? { midiOut.control(brightnessChan, cc, brightness.linlin(0, 1, *brightnessScale)) };
}
ledHue_{
|hue, offset=0.33333|
ledHue = hue;
hue = (1.0 - hue - offset) % 1.0;
midiOut !? { midiOut.control(hueChan, cc, hue.linlin(0, 1, *hueScale)) };
}
ledColor_{
|color|
var hsv = color.asHSV;
this.ledHue = hsv[0].min(1).max(0);
this.ledBrightness = hsv[2].min(1).max(0);
}
ringBrightness_{
|brightness|
ringBrightness = brightness;
midiOut !? { midiOut.noteOn(ringBrightnessChan, cc, ringBrightness.linlin(0, 1, *ringBrightnessScale)) };
}
value_{
|value|
midiOut !? { midiOut.control(0, cc, (value * 127.0).round(1)); }
}
off {
this.value = 0;
this.ledBrightness = 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment