Teaser: ddwPlug quark

Did you ever want to patch a signal directly to a synth argument?

Watch this… all sounds produced from the (modulation-deficient) \default SynthDef.

Also, search here for “superstructure” and… nobody is talking about this except me :laughing: – now it’s not only talk.

hjh

17 Likes

I’ve put up a preliminary version.

Quarks.install("https://github.com/jamshark70/ddwPlug.git");

Here’s a pattern playing \default with enveloped vibrato:

(
p = Pbind(
	\type, \syn,
	\instrument, \default,
	\dur, Pwhite(1, 5, inf) * 0.25,
	\legato, 0.98,
	// frequency stuff
	\degree, Pwhite(-7, 7, inf),
	\freqPlug, { |freq|  // <-- base value gets patched here
		// lazy: I'll just use an ad-hoc function
		Plug({ |freq, depth|
			(depth ** LFTri.kr(4)) * freq
		}, [
			freq: freq,
			depth: Plug({ EnvGen.kr(Env([1, 1.06], [0.7], 4)) })
		])
	}
).play;
)

p.stop;

(Edit: I documented the Syn class, but not Plug yet… soon.)

hjh

4 Likes

One big piece that I didn’t have working before is the ability to add Plugs after the fact. This is now prototyped but not documented yet. (I’ll check it into the public repository after a bit more testing, and documenting it.)

Here’s a quick video starting with default parameter values for the default SynthDef. Then, the familiar set interface 1/ changes a numeric value and 2/ patches a Plug (synth) into freq. Now, at risk of banging the drum perhaps a little too much – when someone asks how to run a signal into a synth parameter, the story so far is that it’s time to learn about buses and mapping (and probably order of execution, especially for audio signals). With this alternate architecture, the story is “just set the control to a Plug instance.”

And of course, new controls exposed by new Plugs can themselves be set to Plugs.

Now… c’mon: this is pretty cool. It seems to answer a long-standing problem.

hjh

6 Likes

That looks extremely appealing, looking forward to trying it out.

Yes that looks quite intuitive - seems to blur the boundaries with Ndef.

OK, pushed the latest changes @ GitHub - jamshark70/ddwPlug: SuperCollider dynamic per-note synth patching .

It is somewhere in between Synth and Ndef. Ndef is good for relatively persistent structures, long-lasting, with easy and comprehensive malleability. Syn is not quite as flexible in terms of on-the-fly routing, but it’s disposable and it’s designed for polyphony. If you want to throw a bunch of signals on the table and patch them arbitrarily, Ndef (NodeProxy) would be the way to go. If you want to play a polyphonic pattern with a SynthDef and dynamically add modulation that wasn’t written into the original SynthDef, Syn is made for this (while NodeProxy would be clumsy).

I also designed it around Synth’s interface because everybody knows Synth, and the simple case of Syn (no plugs) is basically identical to Synth. Syn.new, set, release, free are the main operations and those are generally compatible (with extensions for ‘set’ especially, because of the tree structure). If there were interest, Syn could even be moved into core, and the help updated to steer new users toward Syn, and then everybody gets improved patchability for free. Now… I’m not seriously proposing this (it’s way premature). But I could imagine it.

I decided to do this now because, in my live coding set up, I was getting frustrated with limitations of my prepared instruments. If I wanted to modulate a parameter in some way, or pass the signal through some per-note fx on the way out, previously, the only way was to copy the SynthDef, add stuff to it, and add another variant of the Factory wrapper – a lot of pollution in the codebase. This way, I can just set a property xxxPlug in the player process, and the Event type uses Syn to wire it up for me. I should even be able to save the plug-ged setting as a preset if I like the result.

Early in my SC time, I had thought to use crucial library nested Patches as composable Voicer sources, but a Patch is bound to specific buses, so polyphony never really worked, and I stopped using it. So this is an almost 20-year wishlist item for me, finally achieved.

hjh

3 Likes

It is so good! Will you include it in a core library? (I hope…)

I think it’s premature to add it to core. I have a few test cases which cover a lot of territory, but I don’t have a good sense at this time of what I missed (except that I realize offhand that I’ve done very little testing of arrayed arguments – I can think of one potential bad behavior, just didn’t have time to check it yet).

Also, I haven’t adapted Pmono yet. I think that would be essential.

I think if anyone’s interested in promoting this approach, the best thing to do would be to download it and use it, a lot, and log bugs. It needs to get beaten up a bit in a larger range of real-world use cases. For my part on that, I’ll probably switch my live coding instruments to use Syn under the hood, and then just jamming is also testing.

hjh

3 Likes

This looks awesome!

(You mentioned that few people are using it) perhaps add some description to GitHub with examples, the page just opens on the license. Likewise there’s a description field in the quark.

Since you can have plugs connected to plugs… Is there any need for a top level syn, could they be united into the same class?

What happens if you send too many channels from a plug? Anyway to define a channel mapping strategy (perhaps at the sink rather than source)? Similar question with rates.

If you replace a plug, is there an easy way to cross fade to the new one?

In theory, yes.

The current design concentrates tree/path-style access to args in Syn (Syn has argAtPath; Plug does not). This is good for performance – the arg-path dictionary lives in only one location, and (after paths are cached), set by path is practically constant-time. (First set will be slower because it populates the subtree at this time; after that, it’s cached.) If this were distributed across Plugs (which would be conceptually better), access time might depend on the depth of the tree.

I was thinking in terms of a more-or-less drop-in replacement for Synth – if you use Syn without any Plugs, it should be basically the same as Synth, ideally not adding (much of) a performance hit. So the head of the tree is the focal point. I would have to think about what it would mean for the design if the focal point were, erm, less focal.

It’s not really meant to be an “everything” patching solution. My primary use case is polyphonic synths where synth inputs can be supplied by simple values or Plug signals. Ndefs are meant to stay around for a long time, and you can play with them freely, but they are terrible at polyphony. Syns are meant to play for a relatively short time and get released. A more purely recursive design would be more beautiful, but might also distract from the primary use case.

The server presently disallows this.

(
a = { |array3 = #[1, 2, 3], fourth = 4|
	(array3 ++ [fourth]).poll(1);
	Silent.ar(1)
}.play;
)

UGen Array [0]: 1
UGen Array [1]: 2
UGen Array [2]: 3
UGen Array [3]: 4

a.set(\array3, [100, 200]);  // not enough: only first 2 change

UGen Array [0]: 100
UGen Array [1]: 200
UGen Array [2]: 3
UGen Array [3]: 4

a.set(\array3, [10, 20, 30, 40, 50]);  // too many: only the 3 change

UGen Array [0]: 10
UGen Array [1]: 20
UGen Array [2]: 30
UGen Array [3]: 4

a.free;

Not currently. Server messaging wouldn’t be difficult, because mapping a multichannel control to buses already requires an array such as [\c1, \c2, \c3]. So channel mapping would simply be pre-ordering this array.

If you accidentally map a kr input to an ar bus, I think it just naïvely decimates down to control rate.

The other way is probably dangerous – good issue to log :wink:

Audio sources can crossfade:

(
a = Syn({ |ffreq = 2000|
	LPF.ar(\in.ar(0!2), ffreq) * 0.1
}, [in: Plug({ Saw.ar([440, 441]) }, [fadeTime: 5], rate: \audio)]);
)

a.argAtPath("in").source = { VarSaw.ar([220, 221], 0, 0.7) };

a.free;

But that’s not quite enough – I’ll have to look at that later, in the code.

No, but I guess that isn’t really any different from Jordan’s question about deleting Syn and generalizing Plug.

Does it add to latency as jitlib?

If you mean InFeedback block delay, Syn/Plug try to keep the nodes in the right order so that there should be no additional latency in feed-forward graphs.

If you mean server messaging latency, /s_new messages should be performed on time (unless you have such a complex Syn, with a large number of ad hoc-function Plugs, that it blows away s.latency).

I think a nice design would be to evaluate as many SynthDefs early (with some kind of macro, to produce variations of the functions); but also allow low latency and composability

Hm, the job of anticipating SynthDef variants that the user didn’t specify seems unrelated to automated bus mapping. I think that should be a separate quark.

Of latency, one absolute requirement of mine is that event type \syn should sound on time, even when ad hoc SynthDefs need to be made from functions.

The big problem with composability is argument names. Syn’s path-style addressing is at least unambiguous, but requires intimate knowledge of the Syn’s structure (which discourages large and complex trees). At this time, I don’t have a better solution. (Crucial library seemed to be looking for general composability, but I never figured out the arg name problem in it.)

hjh

2 Likes

Maybe the user can give a hint. How many times haven’t we heard that “you can’t change that after evaluating the SD”? Crucial-lib also tried to solve this, since an Intr can be the base for different Synthdefs, with different number of outputs, for example.

It’s certainly doable. You just build an abstract synthdef which exists as a template, and then when you instantiate it it will create a synthdef for you based upon that template (if one doesn’t already exist). From memory I think that’s what CrucialLib does.

It’s a useful abstraction and I use something similar in Common Lisp for cl-collider. Doing it for this would be more complicated though. I’ve only used it for creating new synthdefs, rather than trying to change existing ones on the fly like JITLib/ddwPlug does.

OK, I see. I needed to think about it overnight. I had thought that this is a general problem, so, should have a general solution outside of my quark. But… the advantage of putting it in my quark is that Syn already has the infrastructure to prepare the concrete SynthDef while handling sync and latency.

The devil is in the details. I remember cruciallib having some sticky problems with naming InstrSynthDefs. If we want to cache concrete defs (and for performance, we do), then the defname needs to reflect non-control argument values. IIRC at first, crucial used a hash, but a hash doesn’t guarantee collision avoidance, so there were wrong cache hits. Then he tried to serialize the args into ASCII soup, and it mostly worked, but there were some weird edge cases which I forget now. I know my limits as a programmer – I don’t think I have better ideas.

The other way would be to let the user name it. If they name it incorrectly, this would break the cache – a bit dangerous.

But sure, I can imagine, perhaps something like:

SynDef(\oscillator, { |freq = 440, numOscs = 7, detun = 1.008, outChannels = 2|
	var oscs = Saw.ar(freq * Array.fill(numOscs, {
		detun ** Rand(-1, 1)
	}));
	// this is wrong but DSP isn't the point of this example
	PanAz.ar(outChannels, oscs, (0..numOscs-1) / outChannels)
});

x = Syn(Def(\oscillator, [numOscs: 7, outChannels: 2]), [freq: 800]);

… where the Def args are non-controls, and Syn args are kr, ir or ar. (Or all args go into the Syn arg list, but that would mean scanning for non-controls every time, which is a performance impact I would prefer to avoid for regular SynthDefs.)

hjh

Yea, that’s one thing that needs to get right using templates/macros. I think the SynthDefs should be generated and evaluated early, and their names should follow the transformations. For example, how many inputs and outputs, as one of the simple cases.

But, if we want to avoid the kind of problem you mentioned, I think we need to have access to something else. We need to know 1) Names that mean something in the current context, when the metaprogramming generates the code and names. 2) Names that do not mean anything yet and 2.1) can be retrieved and interpreted or 2.2) we can’t get access to them in a way to do this

To avoid conflicts with the names chosen by the user, there must be just one reserved characteristic in the name. Something like ending with an underscore, or something like this.

I think SC is mature enough in terms of introspection, and retrieving information about many things, in the language and the server.

Well, the idea is very broad still, it all depends on how far we decide to go. I believe it can generate a lot of good things if well-designed.

Not sure what you mean by “generated early,” can you clarify?

I’m not sure templates / macros are necessary. In my hypothetical example, numOscs and outChannels must be non-controls. At evaluation time, these two args would get concrete numbers and others would each get an OutputProxy sourced from a Control UGen. The current SynthDef builder makes a Control channel for every function argument but this doesn’t always have to be the case.

Now, what is a good syntax to specify which function arguments are noncontrol? This, I don’t know right now. The hypothetical above makes the noncontrols a property of the concrete def; a problem is that you’d have to specify every non-control (no good way to specify a default for a non-control). Macros or templates may be a good way to solve that, or there might be another way.

hjh

Well, I tried to differentiate it from the crucial-lib model. Which I believe keeps Intr and Patch instances a modular version of SynthDef, and generates the synthdefs and server nodes only when necessary.

What I thought was to do something similar, with blocks (audio functions with ugens), but use those elements and create several SynthDefs from that material, that would work with 1, 2, 4, input, and/or outputs, etc. Generating SynthDefs is cheap, but increasing latency and disturbing timing is expensive, especially when playing live if you’re not into slow soundscapes/drones/etc.

That’s just an idea of an idea

That sounds a bit orthogonal to Syn and Plug – though it would be nice to build a composite def and then be able to patch dynamically into it.

I think, though, that the system should not try to guess what the user might want. It would be good to have a method for the user to pre-request the versions that they want (“I would like stereo and quad versions” rather than “gosh, the system built mono, stereo and quad versions but I will only use the stereo one”).

I took care in Syn to avoid disturbing timing (OSCBundle:sendOnTime).

You could disturb timing with it if you build a very large, complex Syn with hundreds of plugs, all based on functions, but any system will perform badly when stressed past a reasonable point. I think it will perform well in more typical use cases (e.g. typical musical event density). Doing client side granular synthesis with a complex, function-based syn would not be a good idea.

hjh

I used the example of numChannels because it is the most explicit to communicate the idea. I didn’t suggest what you described)) Of course, the user would be under control, it’s a “macro expansion” but with user-defined procedures

Ok, cool, it wasn’t quite clear to me.

I don’t have much bandwidth in the immediate future for another engineering effort, but I’ll come back to this when I have more time.

hjh

I didn’t mean it as a suggestion for your class, which is a good idea already and can have better developments than this.

Post new ideas and updates here, it would be cool.