Pattern/event/synth defaults behave surprisingly - a fix

Continuing the discussion from How could patterns be better?:

I really like this, and agree it’s a problem, but could we look at the synth description to see if there is a default value assigned?

Yes, my idea was to get the default control values from the SynthDesc and use this as the protoEvent. This can be done by inserting the following code at the top of the \note event type function (around line 511 of Event.sc). Update: I modified the last line so that the defaults are added to the protoEvent, rather than overriding it completely (I think this is better?):

if(~useDefaultControls ? false){
    var synthLib, desc, defaults;
    synthLib = ~synthLib ? SynthDescLib.global;
    desc = synthLib.at(~instrument.asDefName);
    defaults = desc.controlDict.collect(_.defaultValue);
    currentEnvironment.proto = defaults ++ currentEnvironment.proto;
};

I haven’t tested this beyond the basic kick drum example from the other thread.

In broader terms, this is really about allowing the user to modify the hierarchy of inheritance in \note events. In my current understanding, the order of precedence currently goes like this:

user-supplied event keys
protoEvent
parentEvent
default control values of the SynthDef

With the above code added to the \note event type, if \useDefaultControls is true, the order of precedence becomes:

user-supplied event keys
protoEvent
default control values of the SynthDef
parentEvent

Are there other better ways to achieve this?

This all seems reasonable and can’t think of another way to implement it… I’m just wondering if we could make this the default? That is, make the default specified in the synthdef by the user, the actual default.

I think that would break a lot of existing code, no? Most of my pattern code relies on the defaultParentEvent taking precedence over the synthdef’s default control values.

If a user always wanted the synthdef’s default control values to take precedence over the parent event, they could put this in their startup.scd:

Event.addParentType(\note, (useDefaultControls: true));

Had a look at this – I have a couple of suggestions, and one issue to raise.

Suggestion: Should it be named useControlDefaults?

Suggestion: If the event’s proto is nil, the ++ fails.

currentEnvironment.proto = (currentEnvironment.proto ?? IdentityDictionary.new).proto_(defaults);

I believe this will work in all cases, and also not incur the cost of iterating over the proto.

Also, if you put this logic into getMsgFunc, then it will a/ be available automatically everywhere that a SynthDesc lookup is needed and b/ look up the SynthDesc only once, instead of twice (once for msgFunc, once in \note).

(Removing some leading tabs for forum readability.)

	getMsgFunc: { |instrument|
		var synthLib, desc, defaults;
		// if user specifies a msgFunc, prefer user's choice
		if(~msgFunc.isNil) {
			instrument = ~instrument = instrument.asDefName;

			synthLib = ~synthLib ?? { SynthDescLib.global };
			desc = synthLib.at(instrument);

			// experimental
			if(~useDefaultControls ? false){
				defaults = desc.controlDict.collect(_.defaultValue);
				currentEnvironment.proto = (currentEnvironment.proto ?? IdentityDictionary.new).proto_(defaults);
			};

			if (desc.notNil) {
				~hasGate = desc.hasGate;
				~msgFunc = desc.msgFunc;
			} {
				~msgFunc = ~defaultMsgFunc;
			};
		} { ~msgFunc };
	},

Hm… although that doesn’t handle the case where a user provides their own msgFunc… though tbh I’ve never seen anyone actually do that.

And the problem: This approach does not apply to calculated values such as \freq, \amp and \sustain. They are calculated in \note and then stuffed back into the event at the top level: ~freq = freqs; – overriding the synth defaults. (Though these could be stuffed into the proto instead of the top level… perhaps.)

SynthDescLib.at(\default).controlDict.at(\freq).defaultValue
-> 440.0

// middle C because of degree, octave and root
(instrument: \default, amp: 0.8).play;

// *still* middle C, not A as specified in the SynthDef
(instrument: \default, useDefaultControls: true, amp: 0.8).play;

… whereupon I only just noticed that this means the proposed change doesn’t fix the initial \kick reproducer. It still plays middle C.

When I read the problem, I had a feeling that a deeper redesign would be needed (or at least, some degree of skepticism about putting a patch over the issue). At the moment I don’t have a concrete idea how to redesign it, though all I can think of is a sketch: if the requirement is to have four levels of data (event defaults, SynthDef defaults, proto and main event), one way would be to model the four levels (where we currently have 3) and switch up the primitives so that the lookup order can be configured. Instead of squeezing the requirements into the existing hole, we could change the hole.

I agree with this: Make it configurable and explain how to configure it; do not force a breaking change on everybody.

hjh

I’m split on the name, but I agree with all your other suggestions.

I can’t find any issue with \amp and \sustain (it uses the default controls as expected). I think the reason \freq reverts to the parent event is because ~getMsgFunc.valueEnvir is evaluated after the freqs calculation (freqs = ~detunedFreq.value). If you switch the order of those lines, it seems to work fine. And I suppose this reordering would have to be applied in every event type that uses ~getMsgFunc, that is if we want this feature available for other event types besides \note.