Pkey and multichannel expansion kills server

My intention is to derive values from Ptuples in a different key

Given the following code

SynthDef(\b, { |out, freq=440, startFreq=440, sustain=0.2, amp=0.1| Out.ar(out, Blip.ar(Line.kr(startFreq, freq, 1), 15) * Line.kr(amp, 0, sustain, doneAction:2)) }).add;

this works

Pdef(\a, Pbind(
	\instrument, \b,
	\freq, Ptuple([100, 200]),
	\startFreq, Pkey(\freq) * 0.5
)).play

Pdef(\a, Pbind(
	\instrument, \b,
	\note, Ptuple([12, 40]),
)).play

Whereas this kills server.

Pdef(\a, Pbind(
	\instrument, \b,
	\note, Ptuple([12, 40]),
	\startFreq, Pkey(\freq) * 0.5
)).play

// Server 'localhost' exited with exit code 0.
// server 'localhost' disconnected shared memory interface

Does anyone have a hint for me on what I might do wrong here?

cheers and thanks!

just a quick note: you are setting the \note key to Pkey(\freq) - but since the \freq key is not set Pkey(\freq) is nil. So this wouldn’t work in any case.

…but it shouldn’t crash! And I can verify - on Mac this crashes:

Pbind( *[
	note: Pkey(\freq),
	amp: 0.1
] ).play

Thanks for confirming!

Actually, if doing this

Pdef(\a, Pbind(
	\instrument, \b,
	\note, Pseq([40, 29], inf),
	\startFreq, Pkey(\freq) * 0.5
)).play

(i.e. not explicitly setting freq), I get the correct behaviour…, i.e. freq is calculated for me.

The TL;DR here is: Don’t use Pkey for keys that have not been explicitly populated in the pattern. Pkey evaluates at pattern time, not event time. Using Pkey to “access” values that will be calculated later in the event is at best undefined behavior.

This should be okay:

(
p = Pbind(
	\instrument, \b,
	\note, Ptuple([12, 40]),
	\freq, Pfunc { |ev| ev.use { ~freq.value } },
	\startFreq, Pkey(\freq) * 0.5
).asStream;
)

So what’s happening?

\freq is defined in the default event prototype as a function. Pkey(\freq), when evaluated in Pbind, returns this function. Pkey(\freq) * 0.5 then becomes a BinaryOpFunction, and this BinaryOpFunction is what gets put into the event for \startFreq.

(
p = Pbind(
	\instrument, \b,
	\note, Ptuple([12, 40]),
	\startFreq, Pkey(\freq) * 0.5
).asStream;
)

e = p.next(Event.default);

-> ('instrument': b, 'note': [12, 40], 'startFreq': a BinaryOpFunction)

Then, during event processing, \freq gets calculated from \note as an array, and, because \freq is a SynthDef argument, this array gets put into the the synth argument list.

Then, multichannel expansion causes the single argument list [freq: [a, b], startFreq: a BinaryOpFunction] to split into two argument lists: [[freq: a, startFreq: a BinaryOpFunction], [freq: b, startFreq: a BinaryOpFunction]].

A final pass does asControlInput on all the values. This evaluates the BinaryOpFunction. That function refers back to the original event, where frequency is an array. So startFreq becomes the same array for both synths – a true array, not SC’s slightly funny way of representing arrays in arg lists. The server doesn’t understand this true array and kaboom.

e.asOSC;

-> [[0.0, [9, b, 1000, 0, 1, out, 0, freq, 523.2511306012, 

// NO no no, we don't want this
startFreq, [261.6255653006, 1318.5102276515], 

sustain, 0.8, amp, 0.1], [9, b, 1001, 0, 1, out, 0, freq, 2637.020455303, startFreq, [261.6255653006, 1318.5102276515], sustain, 0.8, amp, 0.1]]]

When \note is a single value, then \startFreq is also a single value, and, no problem.

hjh

yeah Pkey is utterly wretched

But all that said, surely

Pbind(*[note: Pkey(\freq)]).play

shouldn’t hang? Shouldn’t an error be thrown?

It’s not.

That’s a different issue. This is creating an infinite dependency loop: Event.default[\freq] depends on \midinote, which depends on \note. If you replace the \note function with the \freq function, then you have a situation where \note depends on \midinote, which depends on \note.

The thing is, how can you detect the situation?

Pbind(\x, Pfunc { |ev| ev[\freq].debug("freq"); 0 })
.asStream.next(Event.default)

We can’t (and shouldn’t) forbid users from setting an event key to a function – there are legitimate uses for that – and we can’t inspect inside the function to see its dependencies.

I think it’s not a bad thing to improve SC’s safety, where it’s reasonable and not too intrusive. But I’m not sure that SC needs to adopt a general policy of preventing users from shooting themselves in the foot.

hjh

Detecting recursion before it’s… recursing… is very expensive. I think what could be very useful here - and this would be useful for other cases as well - is a “debug” event type (actually, this would need to modify an existing event type, so it would need to look more like Pbind(\type, \note.debugEventType), where it creates a modified version of the underlying event type). One thing a debug event type could do is walk through all it’s values before play is called and wrap them in recursion detection:

currentEnvioronment.collectInPlace({
  |val|
  if (val.isKindOf(AbstractFunction)) {
    var recursion = false;
    {
      var result;
      if (recursion) {
        Error("Recursion detected at key %".format(key)).throw
      } {
        recursion = true;
      };
      protect(val, { recursion = false });
    }  
  } { val }
})

This would detect recursion and throw when you were using the debug event type. There are a bunch of debug / sanity check things that could be done in a debug event type, that would help solve common event related problems, but things you wouldn’t want to run during a performance.

1 Like

Thank you for the deep analysis of my example!
And I am happy that there is a solution on how to get this to work.

I am looking at this from a n00b user’s perspective (i.e. didactical):

Explaining multichannel-expansion to people with a music background is relatively easy, however, introducing Pfunc as in your example

makes things much more complicated. I have to think about this for a bit.

Especially, if it looks like calling value on freq itself… But I assume this is the parent Event’s freq

// from pitchEvent
#{
   (~midinote.value + ~ctranspose).midicps * ~harmonic;
}

that gets evaluated here, right?

I occasionally shoot myself in the foot in SC (although it gets less) but the attempt I originally posted is a nested shoot, hence difficult to see beforehand :slight_smile:

The approach in these two posts also detects circular references:

(Though it does break one uncommon usage, noted a bit later in that thread.)

hjh

That’s correct. There’s no concrete freq value that Pbind is putting into the Event, but there is a freq later, coming from the default Event’s automatic conversion functions.

There’s plenty of room to improve the programming interface, such as the following (entirely untested! no time…)

Pevaluate {
	var <>key;
	*new { |key|
		^super.newCopyArgs(key)
	}
	embedInStream { |inval|
		var keyStream = key.asStream;
		var k, val;
		while {
			k = keyStream.next(inval);
			k.notNil
		} {
			inval.use {
				val = k.envirGet.valueEnvir;
			};
			if(val.isNil) { ^inval };
			inval = val.yield;
		}
		^inval
	}
}

Then you could write:

(
p = Pbind(
	\instrument, \b,
	\note, Ptuple([12, 40]),
	\startFreq, Pevaluate(\freq) * 0.5
).asStream;
)

… just as a simple sketch.

What can’t go away is awareness of the fact that freq is a “later” value, not a “now” value.

hjh

1 Like

I’ve been prototyping a way to unify Pkey and “function to be unwrapped at Event play time”, because I use both of these concepts a lot but the differences between how they are written and some subtle behavior things is frustrating. Something like this (using a class name Pref :woman_shrugging:)

Pbind(
  \combFreq, Pref(\freq, context:\play), // equivalent to Pref({ ~freq }, \play)
  \degree,   Pref(\degree, context:\bind) + 4
)

I might try to start using this and see how it feels… Probably, I’d implement this with an unwrapping pass like:

Pref {
  *unwrap { 
    |e, context| 
    ^e.collectInPlace { |value| if (value.context === context) { value.value } { value } } 
  }
}
// then we do Pref.unwrap(event, \play) before we play the event

This also implies that there could be other contexts to unwrap values besides \play and \bind, but I haven’t really decided if that’s useful.

1 Like