Protoevent (to be used with a sampler) issue

Hello everyone,
I am submitting here a problem that I am unable to solve related to the protevent concept.

It all starts with the sampler project which, for a few years now, I have been implementing piece by piece and using in various jobs.

The post is quite long, but I hope I can be concise and clear enough for you to understand everything.

Premise

The goal is to place as much reusable code as possible within a protoevent to then use it as a starting point for multiple Pbindefs, within which only the key-value matches you want to change are defined.

The context is to use a sample bank (in this example we will use the SSO) and a sample loading function that basically prepares a dictionary of buffers and integers to represent their corresponding midi notes.

The task of the Pbindef is, given a scale, octave and degree, to retrieve the most ‘suitable’ sample to play the desired note and calculate the correct rate at which to play it.

in use

I typically use the following SynthDef:

(
SynthDef(\hybrid_player, {
	|
	out=0, gate=1, amp=0.9, buf, rate=1, pan=0.0,
	atk=5, dcy=0.1, sus=0.7, rel=5
	|
	var sig, sig1, sig2, env;

	var density = LFNoise0.kr(25).range(1, 5);
	var trigger = Impulse.kr( density );
	var pos = 0.5 + TRand.kr(trigger, -0.35, 0.35);
	var length = 1 + TRand.kr(trigger, 0.25, 0.35);

	env = EnvGen.kr(Env.adsr(atk, dcy, sus, rel), gate, doneAction:2);

	sig1 = PlayBuf.ar(1, buf, BufRateScale.ir(buf)*rate, 1, doneAction:0);

	sig2 = Mix.ar(GrainBuf.ar(
		1,
		trigger,
		length,
		buf,
		rate,
		pos,
		2,
		pan: 0)
	);

	// TODO: sometimes the Line time create some glitch: why?
	sig = SelectX.ar(Line.kr(0, 1.0, 0.3), [sig1, sig2]);
	sig = LeakDC.ar(sig);
	sig = Normalizer.ar(sig, 0.4);

	sig = sig * env * amp;

	Out.ar(out, Pan2.ar(sig, pan));
}).add;
);

and, after defining a dictionary and the load function (see this post ), I load the samples by evaluating the row:

~func_load_samplebank.(\sso_flute, "Flute", $-, $#, [\flute], ~my_samples, basepath:~path_ssoBasepath);

I can now play a musical phrase by evaluating this Pbindef

(
Pbindef(\flutes_pbindef,
	\instrument, \hybrid_player,
	\samples_dict, ~my_samples[\sso_flute],

	\scale, Scale.major,
	\root, 0,
	\octave, 5,
	\degree, Pseq([\rest, [0,4], [0,5]], 1),
	\dur, 4,

	\amp, 0.4,
	\atk, 1,
	\dcy, 0.5,
	\sus, 0.7,
	\rel, 5,
	\pan, 0.0,
	\out, 0,

	\index, Pfunc({
		|e|
		e.use({
			if( ~degree.() != \rest, {
				~midinote.().asArray.collect { |note| ~samples_dict[\midinotes].indexIn(note); }
			}, {
				0;
			});
		});
	}),

	\buf, Pfunc({
		|e|
		e.use({
			~samples_dict[\buffers][ ~index.() ];
		});
	}),

	\rate, Pfunc({
		|e|
		e.use({
			(~midinote.() - ~samples_dict[\midinotes][ ~index.() ]).midiratio;
		});
	}),


	\callback, {
		|e|
		"\nCallback".postln;
		e.asSortedArray.do({
			|item|
			postf("%\t%\n",item[0], item[1]);
		});
	}

).quant_([4]).play;
)

I associated a debugging function with the callback key in order to analyse the characteristics of the events generated by the pattern and to better compare them with the result generated by the following examples.

You can see how:

  • first I create the association between the custom key \samples_dict with the dictionary of samples/midinotes created earlier;
  • search, on the basis of the midinote produced by the pattern (and already present in the current environment) for the nearest index for the midi note and store that value in the custom key \index;
  • use this index to retrieve the buffer from the samples dictionary;
  • derive the rate at which to play the sample by means of a quick calculation associated with the \rate key;

The problem

At this point, I would like to move all the index, buf and rate associations to an eventPrototype so that I can write other Pbindefs more quickly and legibly. These will have the same basic behaviour but will perhaps use other samples to reproduce the sound of other musical instruments.

To do this, I proceeded as described below. I create the protoevent:


(
~myProtoEvent = (
	\index: Pfunc({
		|e|
		e.use({
			if( ~degree.() != \rest, {
				~midinote.().asArray.collect { |note| ~samples_dict[\midinotes].indexIn(note); }
			}, {
				0;
			});
		});
	}),

	\buf: Pfunc({
		|e|
		e.use({
			~samples_dict[\buffers][ ~index.() ];
		});
	}),

	\rate: Pfunc({
		|e|
		e.use({
			(~midinote.() - ~samples_dict[\midinotes][ ~index.() ]).midiratio;
		});
	})
);
)

and then I try to use it inside the pattern:


(
Pbindef(\flutes_pbindef_w_protoevent,
	\instrument, \hybrid_player,
	\samples_dict, ~my_samples[\sso_flute],

	\scale, Scale.major,
	\root, 0,
	\octave, 5,
	\degree, Pseq([\rest, [0,4], [0,5]], 1),
	\dur, 4,

	\amp, 0.4,
	\atk, 1,
	\dcy, 0.5,
	\sus, 0.7,
	\rel, 5,
	\pan, 0.0,
	\out, 0,


	\callback, {
		|e|
		"\nCallback".postln;
		e.asSortedArray.do({
			|item|
			postf("%\t%\n",item[0], item[1]);
		});
	}

).quant_([4]).play(TempoClock.default, protoEvent:~myProtoEvent);
)

The interesting thing is that, although this pattern is sounding, it has a completely different behaviour to the starting pattern, and if we go to examine the output generated by the callback function, I realise that there are substantial differences between corresponding associations between the events of the first and second patterns.

Listing only the associations that seem to change between the first and second patterns

Istantanea_2022-11-16_20-50-10

I see how:

  1. in the pattern without the protoevent, for buffer, index and rate (the keys we propose to calculate) the correct corresponding data is returned, while for the pattern that uses the protoevent a Pfunc is returned instead in all three cases. why?

I mean, it would also seem logical to me given that in the protoevent it is a function (which will probably be wrapped inside a Pfunc, when it is used later in a pattern), but why is it not evaluated to return what I need?

  1. I notice how, for both patterns, the \samples_dict key is associated with a dictionary that differs from the one I initially associated with it.

To be more precise, the starting sample dictionary seems to have been enriched with other key-value associations that come from the event environment instead. Why?

Shown here are the values associated with the \samples_dict key for the initial pattern

( 'instrument': hybrid_player, 'midinotes': [ 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81 ], 'buffers': [ Buffer(5, 265447, 1, 44100.0, /home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Flute/flute-d#3.wav), Buffer(8, 272961, 1, 44100.0, /home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Flute/flute-f#3.wav), Buffer(0, 273052, 1, 44100.0, /home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Flute/flute-a3.wav), Buffer(3, 272896, 1, 44100.0, /home/nicola/Musica/samples...etc...

and for the second one:

( 'instrument': hybrid_player, 'buf': a Pfunc, 'rate': a Pfunc, 'midinotes': [ 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81 ], 'index': a Pfunc, 'buffers': [ Buffer(5, 265447, 1, 44100.0, /home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Flute/flute-d#3.wav), Buffer(8, 272961, 1, 44100.0, /home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Flute/flute-f#3.wav), Buffer(0, 273052, 1, 44100.0, /home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Flute/flute-a3.wav), Buf...etc...

Note that the default event prototype contains many functions, but no Pfuncs anywhere.

You’ll need to convert the Pfuncs into just plain old regular functions.

hjh

1 Like

Thank you @jamshark70 for your reply.
I’ve made an experiment, now my protoevent have its Pfuncs replaced by functions:

(
~myProtoEvent = (
	\index: #{
		if( ~degree.() != \rest, {
			~midinote.().asArray.collect { |note| ~samples_dict[\midinotes].indexIn(note); }
		}, {
			0;
		});
	},

	\buf: #{
		~samples_dict[\buffers][ ~index.() ];
	},

	\rate: #{
		(~midinote.() - ~samples_dict[\midinotes][ ~index.() ]).midiratio;
	}
);
)

when I evaluate the pattern below my server crashes. Why?

(
Pbindef(\flutes_pbindef_w_protoevent,
	\instrument, \hybrid_player,
	\samples_dict, ~my_samples[\sso_flute],

	\scale, Scale.major,
	\root, 0,
	\octave, 5,
	\degree, Pseq([\rest, [0,4], [0,5]], 1),
	\dur, 4,

	\amp, 0.4,
	\atk, 1,
	\dcy, 0.5,
	\sus, 0.7,
	\rel, 5,
	\pan, 0.0,
	\out, 0,


	\callback, {
		|e|
		"\nCallback".postln;
		e.asSortedArray.do({
			|item|
			postf("%\t%\n",item[0], item[1]);
		});
	}

).quant_([4]).play(TempoClock.default, protoEvent:~myProtoEvent);
)

Troubleshooting this was slowed down by the fact that I had to construct my own samples dict.

Anyway… taking a look at the OSC being sent:

latency 0.2	SysClock logical time 760.126408032	thisThread's logical time 760.126408032
	[ 9, 'hybrid_player', 1001, 0, 1, 'out', 0, 'amp', 0.4, 'buf', [ Buffer.new, Buffer.new ], 'rate', [ 0.8408964152543, 0.89089871814075 ], 'pan', 0.0, 'atk', 1, 'dcy', 0.5, 'sus', 0.7, 'rel', 5 ]

[ Buffer.new, Buffer.new ] causes the crash.

With the standard event prototype, if there is a buffer, or even an array of buffers, in the Event, it’s converted to a buffer number at the time of generating OSC (Event.sc, line 559, msg.asOSCArgArray).

The problem here is that you are already relying on asOSCArgArray to evaluate the functions in the event. asOSCArgArray is not done recursively. It runs the one time, and this happens to get the values out of the functions. Those values happen to include Buffer objects – but it’s too late – that process already ran – so there’s no automatic buffer number conversion.

So you change it like this:

	\buf: #{
		~samples_dict[\buffers][ ~index.() ].collect(_.bufnum);
	},

… and then you find that a single note is getting both buffer numbers, and both rates (and this also crashes). The reason for this is that the OSC messages are flop-ped before asOSCArgArray. If any functions evaluated during asOSCArgArray return arrays, then it’s too late – their values won’t be distributed among multiple messages.

But if you use finish, you can do stuff before message generation.

(
~myProtoEvent = (
	// btw you do not need backslashes here
	index: #{
		if( ~degree.() != \rest, {
			~midinote.().asArray.collect { |note| ~samples_dict[\midinotes].indexIn(note); }
		}, {
			0;
		});
	},

	buf: #{
		~samples_dict[\buffers][ ~index.() ].collect(_.bufnum);
	},

	rate: #{
		(~midinote.() - ~samples_dict[\midinotes][ ~index.() ]).midiratio;
	},
	
	finish: #{
		~index = ~index.value;
		~buf = ~buf.value;
		~rate = ~rate.value;
	}
);
)

hjh

Thank you so much @jamshark70 ,
as always, each of your answers is a great source of knowledge for me.

Your solution works great, I just had to make a slight modification by converting to Array the buffers before applying the .collect method and now it works perfectly.

buf: #{
		~samples_dict[\buffers][ ~index.() ].asArray.collect(_.bufnum);
	},

:metal: