SuperCollider 4: First Thoughts - #151 by Rainer cites “making GUI’s is difficult” as a pain point.
That got me thinking… what would make it easier to make GUIs? (Or, really, anything involving a callback function.)
The most common question I recall hearing about this is “how do I get a value out of my GUI or OSCFunc or MIDIFunc”? Then we have to explain that the GUI or other responder is just a conduit and isn’t really storage, or proxy, for a value. This is a concrete area where usage is not intuitive. Users think (especially for MIDI and GUI) that the object represents a control, and the control has a current state.
What if responders did maintain state? At any time, you could ask for its current value, by the same interface across GUI, MIDI, OSC etc.? (MIDI and OSC don’t know their value. GUIs do, but only in AppClock, making GUIs no easier than MIDI to use in patterns, for instance.)
As a proof of concept, I tweaked View so that it connects a second method to the Qt-side action()
signal. This method updates the variable belonging to the View (currentValue
– this is an awkward name, but I don’t want to go through all the view subclasses and changed the value
method just now). This could be implemented for MIDIFunc/defs and even OSCFunc/defs – OSC is trickier because of messages’ free format, but there are likely some standard formats e.g. [cmdPath, value]
where it would be easy to extract a value.
So then every type of external input responder could be polled for a value.
Going a step further – the updateCurrentValue
method can also broadcast to dependents. So you can attach some object to forward the value to any place you like. Some standard types of connections could be automated using mapToSomething
methods. If this were implemented consistently across Views, OSC, MIDI, HID, then the common user request, say, "I want to control a synth parameter by GUI would require only:
- make the synth
- make the GUI
mapToNodeControl
… and a lot of the glue that users are currently responsible for managing by hand gets hidden.
For example: Apply these commits –
- https://github.com/jamshark70/supercollider/commit/00e24f899c43cdf9b2d67abd1c2002f2467e795e
- https://github.com/jamshark70/supercollider/commit/d0b2d22a574778805b4c83ce2a27110de7e33d10
Then:
(
s.waitForBoot {
a = { |freq = 440, ffreq = 2000, rq = 0.1|
var sig = Saw.ar(freq * [1, 1.008]);
BLowPass4.ar(sig, ffreq, rq) * 0.1
}.play;
v = Slider2D(nil, Rect(800, 200, 400, 400)).front;
// GUI --> freq (50 - 600)
// because this is a 2D slider, extract 'x'
v.mapToNodeControl(a, \freq, outSpec: [50, 600, \exp])
.translate_(_.x);
// GUI --> ffreq (225 - 8000)
// because this is a 2D slider, extract 'y'
v.mapToNodeControl(a, \ffreq, outSpec: [225, 8000, \exp])
.translate_(_.y);
// ideally the user wouldn't have to 'didFree' the model
// but I don't want to do that much surgery in this POC
v.onClose = { v.changed(\didFree); a.release };
};
)
There’s probably something that I’m missing, but I tried to design the POC so that it could apply in general to just about anything where we currently use a callback function. I could anticipate issues about translating various data formats, though that could be handled by plug-n-play translator objects.
It does directly express the idea of “connect this control source to that thing.”
For comparison, the current canonical way. It seems shorter, but I’m not sure it’s clearer – e.g. a new user might ask, why do you need vars up front for the specs?
(
s.waitForBoot {
var freqSpec = [50, 600, \exp].asSpec;
var ffreqSpec = [225, 8000, \exp].asSpec;
a = { |freq = 440, ffreq = 2000, rq = 0.1|
var sig = Saw.ar(freq * [1, 1.008]);
BLowPass4.ar(sig, ffreq, rq) * 0.1
}.play;
v = Slider2D(nil, Rect(800, 200, 400, 400)).front;
v.action = { |view|
a.set(
\freq, freqSpec.map(view.x),
\ffreq, ffreqSpec.map(view.y)
)
};
v.onClose = { a.release };
};
)
Anyway… thinking out loud for now. (I think Miguel(?) had done some work on functional reactive programming in SC, didn’t quite get traction either.)
hjh