-
-
Save swannodette/30b42349ac040128050cc4ba44ee8f7f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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