Writing an Event prototype using/referencing Event.default as its own prototype/parent

Hi,
I’m trying to write an Event prototype that will compute itself some keys under some circumstances and refer to Event.default in the other cases.

In this example, the prototype should return itself the “note” value if the event has a “name” property. Otherwise it should use the Event.default’s “note” mechanism.

This is working fine, until I’m trying to return the event “freq” values, which is neither defined in the event itself nor and the prototype, and is taken from the upper Event.default prototype but that relies on the prototype “note” function.

(
x=(name: "C7"); // an event with a "name"
y=(degree: 5); // an event without a "name"


p=(	\note: { |event| 
	event.debug("event"); // KO: event is nil when called from Event.default.note
	if (event.includesKey(\name).not) {
		event.use{Event.default.note};
	} {
		var tmp;
		//tmp=event.name.asChordEvent; // extract scale, note, gtranspose from the name
		tmp=(note: [ 0, 4, 7, 10 ] );
		tmp.note;
	}
},
).proto_(Event.default);

x.proto_(p);
y.proto_(p);

)

// Event with a name provided
x.name         // --> C7: OK
x.use{x.note}; // --> [ 0, 4, 7, 10 ] : OK
x.use{x.freq}; // --> [ 261.62, 329.62, 391.99, 466.16 ] : KO

// Event with no name provided
y.name         // --> nil: OK
y.use{y.note}; // --> 9 : OK
y.use{y.freq}; // --> 440 : KO

When the prototype “note” function is called from the Event.default “freq” function, it does not receive any event, so it breaks computing the right notes.

How to solve this ?

The catch here is the way that the default event prototype’s conversion functions call the upstream functions: \freq calls ~midinote.value:

  • It doesn’t use an |event| argument. (None of the functions do.)
  • It doesn’t pass the event upstream as an argument. (It doesn’t have to, because it’s assuming that you’ve already used the event as the current environment – which does happen somewhere in the play sequence.)

So you should access keys in the event using environment variable syntax or envirGet / envirPut, and if you need to call methods on the event, use currentEnvironment – e.g. instead of event.includesKey(\name).not, it should be ~name.isNil and so on.

hjh

PS Your last KO is actually OK: 9 semitones up from the default root middle C is 440. Degree 5, which you specified, is the 6th of the scale, which in C major is A-natural.

I rewrote my code following your suggestion, and it is getting better, although I still have some strange effects I’m failling to explain.

(
// The protoype
p=(
	\note: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\note proto (without name)".postln;
			Event.default.note.debug("note returned by proto");
		} {
			var tmp;
			"Into \\note proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.note;
		};

	},
	\scale: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\scale proto (without name)".postln;
			Event.default.scale.debug("scale returned by proto");
		} {
			var tmp;
			"Into \\scale proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.scale;
		}
	},
	\gtranspose: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\gtranspose proto (without name)".postln;
			Event.default.gtranspose.debug("gtranspose returned by proto");
		} {
			var tmp;
			"Into \\gtranspose proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.gtranspose;
		}
	},
	\nature: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\nature proto (without name)".postln;
			Event.default.nature; // non standard, so should return nil
		} {
			var tmp;
			"Into \\nature proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.nature;
		}
	},
	\rootname: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\rootname proto (without name)".postln;
			Event.default.rootname; // non standard, so should return nil
		} {
			var tmp;
			"Into \\rootname proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.rootname;
		}
	},
).proto_(Event.default);
)

// test cases
Pbind(\name, Pseq(["C7add9","G7"])).play; // OK (doesn't play the chords, which is expected)
Pbind(\name, Pseq(["C7addb9","G7"])).play(protoEvent: p); // OK plays the chords correctly

Pbind(\degree, Pseq([2])).collect{|e| e.debug("playing")}.play(); // OK
Pbind(\degree, Pseq([2]), \scale,Scale.minor).collect{|e| e.debug("playing")}.play(); // OK

Pbind(\degree, Pseq([2])).collect{|e|  e.debug("playing")}.play(protoEvent: p); // KO : what is it playing ???
Pbind(\degree, Pseq([2]), \scale,Scale.minor).collect{|e| e.debug("playing")}.play(protoEvent: p); // OK

PS: This ChordAnalyzer class is needed to run the code.

Hmm… I don’t quite have time to install an extension just to try it.

One other problem is that the default Event prototype doesn’t .value all of the keys – only the ones at the trunk of the chain of operations. \midinote does ~note.value but refers only to ~scale – so you can’t drop in a function for scale, or gtranspose. (When one or more arguments are functions, most (all?) of the math operations will compose to larger functions, and these functions might evaluate correctly at the time of building the OSC messages… but they might not, and it’s a bit out of your control. So I think it’s better to avoid that situation.)

I’m starting to think it might be easier for you to use the \finish key to massage the data from your format into the format that the default event prototype expects. (Or use a filter pattern.) That is, your pattern generates data with or without \name and the \finish function converts that into the required \note, \scale etc. values.

hjh

I came to the same conclusion.
My first question was : Why this design ? Why not having \midinote (and \note) referring to \scale.value instead of \scale ? Could Event.default be changed to .value ~scale too (and the other ones) ?

So I made a workaround: in my prototype \note function, I’m forcing the evaluation of the \scale function and store it so the Event.default function don’t need to evaluate \scale anymore.
This is working fine.

I also followed your suggestion with ~finish. It is working fine also.

Now I wonder, which is the most robust approach, and which has the nicest syntax and semantics.

Protoype approach

(
p=(
	\note: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			var note, scale;
			"Into \\note proto (without name)".postln;
			// v-- FIX : Event.defaut.note does not `.value` scale, so the function is never executed.
			// So I force its execution and store the value.
			~scale=~scale.value;
			Event.default.note.debug("note returned by proto");
		} {
			var tmp;
			"Into \\note proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.note;
		};
		
	},
	\scale: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\scale proto (without name)".postln;
			Event.default.scale.debug("scale returned by proto");
			// ~scale=#[0,2,4,5,7,9,11].debug("scale returned by proto");
		} {
			var tmp;
			"Into \\scale proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.scale;
		}
	},
	\gtranspose: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\gtranspose proto (without name)".postln;
			Event.default.gtranspose.debug("gtranspose returned by proto");
			// 0.debug("gtranspose returned by proto");
		} {
			var tmp;
			"Into \\gtranspose proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.gtranspose;
		}
	},
	\nature: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\nature proto (without name)".postln;
			nil; // non standard, so should return nil
		} {
			var tmp;
			"Into \\nature proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.nature;
		}
	},
	\rootname: #{
		// currentEnvironment.debug("environment");
		if (~name.isNil) {
			"Into \\rootname proto (without name)".postln;
			nil; // non standard, so should return nil
		} {
			var tmp;
			"Into \\rootname proto (with name)".postln;
			tmp=~name.asChordEvent;
			currentEnvironment.putAll(tmp);
			tmp.rootname;
		}
	},
)/*.proto_(Event.default)*/;

Pbind(
	//\scale, Scale.minor, \degree, Pn(Pseq([0,2,4]),3,\next),\gtranspose, Pgate(Pseq([0,2,3]),inf,\next),
	\name, Pseq(["C7add9","C#°7"]),
)
.play(protoEvent: p); 
)

~finish approach

(
f={|e|
	// v-- don't work, because finds those keys through Event.default, the prototyp
	// [e.includesKey(\note),e.includesKey(\degree),e.includesKey(\scale),e.includesKey(\name),].postln;
	// if (e.note.isNil.and(e.degree.isNil).and(e.scale.isNil).and(e.name.notNil)) {
	if (e.name.notNil.and(
		e.asKeyValuePairs.select{|item,i| i.even.and([\note,\degree,\scale].includes(item))}.size==0)) {
		var tmp;
		"Expansion required".postln;
		tmp=e.name.asChordEvent.debug("chord event");
		e.putAll(tmp);
	}
	{
		"No expansion required".postln;
	};
}

Pbind(
	// \scale, Scale.minor, \degree, Pn(Pseq([0,2,4]),3,\next), \gtranspose, Pgate(Pseq([0,2,3]),inf,\next),
	\name, Pseq(["C7add9","C#°7"]),
	\finish, {|e|
		f.(e); // expand the "name" into scale, note, gtranspose if necessay (and possible)
		e.debug("finish");
	},
)
.play();
)

For both approach, the protoype and the finish function will be “hidden” in a class.

The best guess I can come up with is that note, midinote, freq and detunedFreq are calculated based on something else, while scale, gtranspose etc are not. The latter parameters contribute to but are not derived from other parameters.

Every function calls costs CPU time, so it’s reasonable for JMc to have decided which parameters can’t be done without function calls and which ones don’t really need them. That is, if every parameter is .valued, then most of them would waste CPU cycles dispatching into Object:value. Also you’d have to be careful of any object that answers value with something other than itself.

hjh

1 Like