POC of simplified responders

99% of the time, it will be a single synth parameter to a simple slider or knob. Perhaps a second function for a GUI pair (slider + number box). That gets users in the door with a minimum of fuss and maximum of ease. More complex scenarios could be written out in full. (Similarly, there are things you can do with SynthDef that you can’t do with {}.play, but we’re not going to remove {}.play from tutorials in order to force users to learn SynthDef first.)

hjh

Okay, I’ve long thought something like this would be very nice, exploiting polymorphism:

// pseudocode

a = EZSlider.bus(..., \freq, ...); // make a gui element mapped to a bus with freq spec

Synth(\default, [\freq, a]); // View:asControlInput returns the bus map

Server-side Slider state is separate from any synth mapped to it, so no init and persistence issues. If we wanted to get super fancy, we could even do things like this

a = EZSlider(....); // make a gui element with no spec

Synth(\default, [\freq, a]); // View:asControlInput automatically connects to a bus if needed and looks up the spec for the control if it can

But perhaps that is too automagical!

I think the first version at least is very simple, intuitive, as concise as I can imagine, and with a reasonable separation of concerns. I suppose some might object about the polymorphism, but I think that is mainly a documentation issue (explain what is happening in examples) and there are clear precedents in passing Buffers and Busses as synth args.

At the very least it seems nuts to me that there is not a convenience method for mapping a view to a Bus, since this is such a common usage pattern. Even just a View:mapToBus that returned a Bus object would be a big improvement.

I think having a concise and standard notation for this would be a good idea!

(In the second example, how does a know it’s name is freq? Would it need to know that for the lookup?)

Going back to concise notations for controls in UGen graphs…

SuperCollider has unary - (- 3 . abs == 3) but not unary ! (! 3 => parse error).

So it could borrow the Kyma notation (and syntax highlighting).

{SinOsc.ar(!freq) * !amp}

!name parsing as 'name'.exclamationPoint.

Symbol>>exclamationPoint making a control object that is more expressive than Control, but acting as one for the purposes of the Synthdef builder.

An interface builder could run the block, collect all the controls, wire up an interface, call the Synthdef builder and connect the interface to the resulting sound.

Of course modulo syntax we can do this already, and obviously people do do it in different ways, but having a standard notation and a standard name for invoking it ({}.play(ui:true)) might mean that the ecosystem would coalesce a little?

Or perhaps the Ndef.gui/ProxyPresetGui system is already the standard way to do this? It is very nice! Or perhaps this is too obscure a thing to want a standard name for?

Best,
Rohan

Ps. Default default values could be derived from the symbol name, so {SinOsc.ar(!freq) * !amp} could make a soft middle register tone.

Annotations could be made using ordinary methods, !freq.value(220) to set the default value.

(Perhaps an operator alias, !freq <| 220 or !freq <-- 220 or…)

!freq.cc(5) to say “if I am in a context where I can receive indexed continuous control please assign me to continuous controller five”.

And so on.

That is not quite true – I’m pretty sure that all symbolic operators are binary in SC. Unary operators are all text identifiers.

// '-3' is an integer literal -- not '3.neg'
{ -3 }.def.dumpByteCodes
BYTECODES: (3)
  0   2C FD    PushInt -3
  2   F2       BlockReturn
-> < closed FunctionDef >

// Spaces are ignored when recognizing numeric literals
// -- the bytecodes are exactly the same
{ - 3 }.def.dumpByteCodes
BYTECODES: (3)
  0   2C FD    PushInt -3
  2   F2       BlockReturn
-> < closed FunctionDef >

// If we use parens to force it to parse differently,
// then it fails
{ -(3) }.def.dumpByteCodes
ERROR: syntax error, unexpected '(', expecting INTEGER or SC_FLOAT or ACCIDENTAL or PIE
  in interpreted text
  line 1 char 4:

  { -(3) }.def.dumpByteCodes 
     ^

With the current sclang definition, there is no way to support ! as a unary operator.

To be honest, even if we could, I would have questions about that approach.

One is, I think we should give users the components of an interface and make it easier to wire them up. I don’t think we should be 100% hiding the connections.

Second – I have a repetitive strain injury – for me to type one page by hand in a day is a challenge. (I’m using voice recognition software to write this, which is why sometimes my posts have odd little typos like “in” for “and.”) So you would think I would favor maximally concise symbolic notation as much as possible. But my coding has gone in the opposite direction – longer variable names, spelling out method calls more often etc. – I even use Array.fill(10, 0) instead of 0 ! 10. Why? Because if there’s too much symbol magic, I can’t flipping read it a couple months later.

Convenience and concision are not the same thing.

This is a cute idea! But… use case: frequency represented as an on-screen slider, connected to a synth input, and also controlled by TouchOSC on a tablet (with state changes in the language synced out to the tablet as well).

I’m pretty sure scztt is right that the parameter value needs to be at the center of this construction, not a GUI+bus pair.

GenericGlobalControl in my performance setup is a value+bus pair – so the synth wiring happens by bus mapping, and both the GUI and OSC hang off of that as dependents. There are some mistakes in this class’s interface, so I wouldn’t propose it for the main library, but the architecture has been very successful for me.

(Aside: I wouldn’t rely exclusively on Spec.specs for name lookup – it’s too common to have variants of names, e.g., freq, filt_freq, ffreq, mod_freq…)

hjh

Ah yes, well spotted! {var x = 3; - x} // => parse error

On the other hand, I guess this means the syntax is definitely available if someone wanted to use it?

I don’t think we should be 100% hiding the connections.

Well, yes, I think that is in fact exactly what I would like! :slight_smile:

But also I am very happy with the current auto-wiring situation.

I’ve just noticed it’s quite easy for people not to know it exists.

Also I’m sure any improvements in the low-level wiring will make the auto-wiring nicer too eventually, so thanks kindly for working on that!

Best,
Rohan

We’d need some wiring in asControlInput to pass the control name and see if it corresponds to something in the ControlSpec dict. I’m less sure about that tbh than the first version. That said, NdefGui already does something like this.

Perhaps. Though what you’re talking about is of course possible now, through a variety of approaches. And I think GUI + OSC, while not rare, is probably not the majority of use cases.

I always think these things come down to a question of what do we want to make easy? Just wanting to get a sound going with an easy (or EZ) GUI is super common, but surprisingly hard in vanilla SC. That’s why that is a common complaint, and why Ndefs (rather than the various bespoke attempts at GUI/state management/adaptors/etc. that people have made over the years) are pretty popular, if not always ideal in other ways.

So what I’ve proposed here is to make a common case easy. The complicated case should be possible, and of course there’s nothing wrong with it being easy too, if that’s not a bad tradeoff! As you said though

FWIW similar convenience methods could allow you to route an OSCdef/MIDIdef to a bus as well. Poorly considered back of an envelope sketch ;-):

OSCdef.bus(\myParam, path); // not ideal way to do this but you get the idea.

Synth(\default, [\freq, OSCdef(\myParam)]);

I agree that for multi-connected stuff you need some kind of ‘model’ (if not necessarily MVC), but even there it might be nice if some of that was dealt with auto-magically through polymorphism.

2 cents anyway.

Sure, and I do realize that I myself made just about the same point.

I think, though, that we are hampered by the habit of thinking in terms of SynthDef and Synth.

SynthDef is a complicated class, but it is a very low-level class. It’s complicated because building and optimizing a graph is complicated, but architecturally, it’s only a thin layer over the server’s GraphDef, and its limitations are the same as GraphDef’s limitations. There are many things we want SynthDefs to do, which they can’t do, because they are low-level abstractions (and I think we can’t do without low-level abstractions, so we need to keep SynthDef the way it is).

Synth is nothing more than a thin layer over synth nodes.

I could imagine a superstructure around Synth. Let’s say that when you create one of these, it automatically creates a model for every input (or, optionally, you could suppress those, or specify the ones that you’re interested in) – or, even better, creating the model could be deferred until the moment when it’s needed for mapping.

Napkin pseudocode:

a = SynthPlayer(\name, [args]);

// splitting into two lines for commentary
// the filter_freq control model need not exist at this point
// but the attempt to access it would create the model
g = a.control(\filter_freq)

// and the slider a/ refers back to this model
// and b/ becomes a dependant of it
.makeSlider(...);

And this would set up MVC behind the scenes, and remove the dependency when either the node ends or when the slider gets closed.

Then the distance between the convenience method and a robust architecture is very small. Whereas, the problem with my original proposal (scztt was exactly right about this), and the problem with aGUIElement.bus, is that it would get the user accustomed to a less-extensible architecture, which they would have to unlearn later when their needs grow.

IMO (and I’ve been saying this for some years now) we can bring SC much further into the future by building more powerful abstractions around the ones we already have. But Synth is very deeply entrenched in SC culture – not that anyone is actively resisting the idea of more powerful abstractions, but that their potential seems to go unseen, and the discussion always seems to come back to basic Synth and its limitations.

hjh

I’m pretty much in agreement, James, and I’ve often thought that if we were redesigning this from the ground up, we should make the default way of working higher level. I think that would mean something like Ndefs for nodes, not necessarily with the proxy placeholder stuff, but certainly with abstracting busses and interconnection, simple fadeouts, levels, etc. away, and also better state management in the way you’re describing. This should interface seamlessly with something like an Event/patterns approach. I think that would make a nice and consistent ‘vanilla’ way of working, which would make lots of things easier than they are now, and easily allow extension.

But the reason I suggest the sort of thing above is that that kind of major reworking is just hard to do and get traction for. It could be a good idea for an ‘SC4’, but as it is it just risks being yet another working style, with additional technical debt, etc. Incremental improvements on existing styles of working might be more achievable, I’d guess. Though I’d be happy to be proved wrong!

With regards “abstractions”, I think Kyma’s an interesting comparison (it’s such a close relation!)

Kyma has one kind of sound object (called Sound).

SuperCollider has two (UGen and SynthDef).

Having two categories makes things much more complicated.

I know why the distinction exists, and I know the nice aspects of the SuperCollider model.

But also Kyma shows how well a single Sound object model can work, how expressive it can be.

A lot of the abstractions people write in SC seem to be about reducing the complexity of working with these two different kinds of sound objects.

Bringing all event processing inside UGen graphs is an approach to this, but there’s lots of others.

It’s a tricky problem!

Best,
Rohan

Ps. There’s a very nice PhD by Madison Heying (advised by Amy Beal) about Kyma (https://escholarship.org/uc/item/31c988ds, 2019).

I think it’s interesting that SC started with the same community structure as Kyma.

There are some nice aspects to such small scale commercial systems.

The person (or people) writing the system wants to make it work for everyone who uses it.

But they also want to ensure it stays coherent and comprehensible and maintainable as it grows.

The revised model for SC makes it simpler for everyone to adapt it to their own work.

But it also makes overall coherence a bit problematic.

Part of the answer there is to commit to it, update documentation to use the new structure consistently (fully understanding how massive a job that is…), and use it consistently pedagogically as well (e.g. when answering questions on the forum). That’s hard to organize but it’s not impossible.

I’d like to see something midway between Node and NodeProxy. NodeProxies already have a lot of the features we’re talking about, indeed. I don’t have a strong association in my mind between NodeProxies and polyphony – probably because I’ve tended to use NodeProxies in a ProxySpace or with Ndef, where polyphony seems to me not well supported.

Using NodeProxy directly is slightly clumsier syntactically when trying to roll off a large number of them at once.

(
a = [48, 53, 55, 58, 62, 64, 69, 72].collect { |midi|
	Synth(\default, [freq: midi.midicps])
};
)

a.do(_.release);

// with NodeProxy, AFAIK you need THREE
// method calls to get it to sound
(
a = [48, 53, 55, 58, 62, 64, 69, 72].collect { |midi|
	NodeProxy(s, \audio, 2)
	.set(\freq, midi.midicps)
	.source_(\default)
	.play;
};
)

a.do { |np| np.release };  // leaves behind 16 synths...?
a.do { |np| np.release.clear };  // still leaves behind 16...?

a.do { |np| np.release.stop };  // ok...

… so if NodeProxy were to be adopted generally, I think there would need to be an easier way to do the above.

hjh

Going back once more to “making GUI’s is difficult”, and trying to be clearer.

By ‘a nice notation’ I mean something like the way SC uses procedure call graphs to write data flow.

{SinOsc.ar(440,0) * 0.1} is nice because the node names and edges are implicit.

Writing the same graph as a list of nodes and edges is less nice, regardless of the syntax.

n1 = sinosc, n2 = 440, n3 = 0, n4 = *, n5 = 0.1;
e1 = n2->n1:0, e2 = n3->n1:1, e3 = n1->n4:0, e4 = n5->n4:1;
graph([n1,n2,n3,n4,n5],[e1,e2,e3,e4])

{SinOsc.ar([440,441],0) * 0.1} is nice because it makes the parallelization implicit.

Sometimes the various UI notations seem a little like the nodes and edges variant.

Hence a vague sense there should be a nicer way.

One more remark about polyphony and the “low-level”-ness of UGen graphs.

Below is the common case of a polyphonic event processor where voices share an input signal, and the voices are sent to a post-processor stage.

{arg freq = 0.5, delay = 0.2
;var lfo = SinOsc.kr(freq, 0)
;var osc = Event.voicer(16, {arg e; SinOsc.ar(e.p.midicps, 0) * lfo * e.w * e.z})
;CombC.ar(Splay.ar(osc), 0.5, delay, 3)}

When written as a UGen graph the wiring is very simple.
Shared signals are passed to voices using variable scoping rules.
Event.voicer simply assigns names to the control buses the system writes incoming events to.
Voice signals are defined as functions that receive event data as an argument.
Signals are passed to the post-processor as an array, one voice per channel.
Interface elements are declared as outer arguments.

Writing this using a SynthDef-per-voice model in one sense complicates the wiring.
There is now a graph of graphs, and the meta-graph has a distinct semantics.
Signals are passed about on shared audio buses.
Some of the nodes are dyamically replaceable.
But the shape of the wiring stays the same, it’s not more intricate, it doesn’t have more wires.

The Ndef system is quite close to the above notation,
which considering the extra complexity I think is really very nice!

Anyway, just writing in defense of the simplicity and elegance of the plain old UGen graph…

Ps. This does have some implications for actual work. If you mostly write UGen graphs, then the thing that is awkward and that you want to fix is the handling of cyclic and demand rate subgraphs at the synthesiser. If you mostly write SynthdefDef graphs you want more and nicer language level abstractions. Very different thing to want!

1 Like

Disconnected thoughts:

  • If DSP units can be arranged in chains, do we need to arrange GUI widgets in chains too? That could be a useful trick for slider + numberbox.
    A processing chain implies an input signal. In Saw → RLPF, it’s very clear which information flows into which object, and that the RLPF doesn’t have autonomy wrt its input. If it’s Slider → number box → UGen input, the number box does have an independent source of information (user input). What happens when the user types a number into the box? You can’t really do Slider ↔ number box because in DSP terms, that’s a feedback loop. (Max / Pd would require a “set” message back into the earlier widget.) But without some mechanism, the number box and slider could get out of sync. The data flow model doesn’t express this common use case accurately.

  • If GUI objects are created within UGen graph code, who “owns” them? That is, which object / set of objects would be responsible for maintaining and cleaning up those connections? asUGenInput could be exploited to make the connection but something has to clean them up.

  • The idea of a large UGen graph plus interaction defined within the graph sounds a lot like a throwback to SC2’s “one and only one massive Synth” architecture.

  • I get a lot of mileage out of dynamic connections – I tend to create empty GUI widgets and then swap controls in and out of them during performance. (The GUI follows the hardware controller’s design rather than the synthesis design.) As a convenience, it may be very good to just write the GUI object as the source of a UGen’s input, just noting that there are other use cases.

hjh

Another sketch. This doesn’t handle the notational issue of GUI-as-input (still pondering that one), but it is MVC without requiring the user to write MVC.

Here, the synth exists before the GUI. That may not be a good requirement. There would be ways around that, but this is enough for today.

Classes: test-synthplus-gui.sc · GitHub

Demo:

(
var dualControlLayout = { |synth, name|
	var number, slider;
	var layout = HLayout(
		StaticText().fixedWidth_(100).string_(name).align_(\center),
		number = NumberBox().fixedWidth_(80),
		slider = Slider().orientation_(\horizontal)
	);
	synth.mapToGUI(name, slider);
	synth.mapToGUI(name, number, normalized: false);
	layout
};

s.waitForBoot {
	a = SynthPlus(\default, [freq: 220]);
	w = Window("Test", Rect(800, 200, 400, 125)).front;
	w.layout = VLayout(
		dualControlLayout.(a, \freq),
		dualControlLayout.(a, \pan),
		dualControlLayout.(a, \amp),
	);
}
)

a.set(\freq, 300);  // GUI updates

// a catch -- it's currently necessary to address changes
// to the object... Event doesn't do that... so this is ugly
// but the GUI responds!
(
p = Pbind(
	\degree, Pwhite(-7, 7, inf),
	\dur, 0.125,
	\pan, Pwhite(-0.8, 0.8, inf),
	\play, {
		s.makeBundle(0.2, {
			a.set(
				\freq, ~detunedFreq.value,
				\pan, ~pan
			)
		})
	}
).play;
)

p.stop;

a.release;  // GUIs become inactive

hjh

Sorry, I wasn’t very clear.

I was trying to be more precise about the word ‘notation’, which I’d been using a bit ambiguously.

I.e. ‘notation’ as distinct from ‘syntax’: the same notation is also nice in languages with very different syntaxes.

And distinct from ‘implementation’: the same notation works well when implemented in a completely different way, say as plain value construction rather than the framed manner SC does it.

I didn’t mean to suggest it was a nice notation for writing interfaces…

Only that it is a very nice and perspicuous notation!

And yes, I do think that SC2 was a very beautiful system!

And you’re right it’s connected.

For instance that none of the SC2 Texture functions, or the very many SC2 examples that were written using them, are in SC3.

And that there are at least three different ways to write those functions in SC3, none quite so elegant and clear as the SC2 way.

But of course there are things that are the other way about.

And simple UGen graphs are much less expressive.

I like SC3 very much and am very grateful for it, but also it’s model is inherently a bit complicated.

If GUI objects are created within UGen graph code, who “owns” them?

I’m confused about this.

I think the general idea for auto-guis is that when writing the UGen graph (or the graph of UGen graphs) we already say (in a sense) what values can be set dynamically.

Interface builders use that information to derive an interface.

The Synthdef builder is only interested in the name, and index and default value for each control, all of which it fetches from the Function object.

But interfaces work better if they have more information.

It’d be nice to be able to annotate the existing control declarations in a simple manner, but it’s not obvious how to, so they need to be stored somewhere else.

And yes, I don’t mean dynamic graphs aren’t nice, only that they’re more complicated, and harder to make very clear notations for.

I tried out you example, and it works (of course!), but I’m not expert enough to say anything useful I’m afraid.

The .playWithUi above is the best way I know to do this.

FWIW, I struggled with SC2, but SC3 came relatively naturally for me. For me, it’s easier to build large systems when you can add elements dynamically, rather than having to tear down the entire One Synth To Rule Them All and rebuild all of it again with a new element added. (This is especially important for live improvisation.)

Neither of us is wrong, just recognizing a fairly deep difference of opinion here.

hjh

Ah, interesting. And I certainly don’t think you are wrong about anything! Apologies for going so off topic in this thread. I think I wrote so much because I sometimes wonder, as you and muellmusik were writing earlier, if maybe there is a sort of helpful unifying abstraction/notation hiding in plain sight, and I’m very curious about it. But I’ve no good ideas, barely even vague intimations, so I’ll stop!

1 Like

I certainly didn’t mean it like that! You’ve brought up some really excellent points to think about. The idea of integrating controllers into synth-style dataflow is fascinating and I’m still thinking about it, though I admit I don’t see how to make it work in a general way in SC Server yet.

Exactly – there’s got to be a way to do this.

IMO it’s a very useful high-level design conversation, of the sort that I don’t remember us having very often in SC-land.

hjh

Don’t stop! Sometimes these kind of ‘off-topic’ things bear amazing fruit!