Layer chords of infinite duration using Pbindef

Hi,

In order to change the chords of a layer with quantization, I was wondering wether the equivalent of PmonoArtic exists with the functionnalities of Pbindef.

This question has been partially discussed in this topic : Stopping an event with infinite duration since maybe one could also see this issue as stoping an event of infinite duration, changing the chords parameter and playing a new infinite event ?

Concretely, I would like to be able to change the chords at any first beat of a bar or to come back to chords of finite duration.

Here is the solution I found using Pbindef :

SynthDef(\layer, {arg chord = #[0, 2, 4], scale = Scale.major, out = 0, gate = 0;
   var sig, freq;

   freq = scale.degreeToFreq(chord, 60.midicps, 1); // issue with chord arg
   sig = Mix(SinOsc.ar(200, 0, 0.1));
   sig = sig * EnvGen.kr(Env.asr(1, 1, 1), gate, doneAction: 2);
   Out.ar(out, sig!2);
}).add;

a = Synth(\layer);

Pbindef(\testLayer,
    \type, \set,
    \id, a.nodeID,
    \args,#[\chord, \gate],
    \chord,[8, 12, 15],
    \gate, 1,
    \dur, 1,
    \amp, 0.6
).play(quant: 1);

Pbindef(\testLayer,\chord, [[3, 5, 7]]).play(quant: 1);
Pbindef(\testLayer).stop;
a.set(\release,2);
a.set(\gate,0);

It is quite working, however I have two issues. First, I can’t manage to solve the “degreeToFreq” conversion in the SynthDef, it seems that the chord argument is not understood, although the following code works properly :

Scale.major.degreeToFreq(#[2, 4, 6], 60.midicps, 1); 

Any idea of where this error comes from ?

The second issue is that this method requires to set the number of channels in the Synthdef before creating the pattern, which means I can’t use this SynthDef if I want to play a 4 notes chord for instance. Pmono (and PmonoArtic) manages the number of synths and looks more flexible form this aspect. But I would like to keep Pbindef objects for their concise way to set arguments (a Pmono encapsulated in a Pdef needs to be evaluated in full to change one argument).

So is there a better solution, like a "PmonoArtic mixed with a Pbindef " ?!

Thanks !

mic

First, I can’t manage to solve the “degreeToFreq” conversion in the SynthDef, it seems that the chord argument is not understood…

That’s correct. Why would it be? “chord” has a meaning in English, but that doesn’t mean it has a meaning in the default Event prototype.

http://doc.sccode.org/Tutorials/A-Practical-Guide/PG_07_Value_Conversions.html

Pitch keys subject to automatic conversion are \degree, \note, \midinote (and \freq, but it’s already Hz).

\chord is not one of these, so by definition, it’s passed to the synth without any further operations being done.

	\freq, Pfunc { |ev| ev[\scale].degreeToFreq(ev[\chord], 60.midicps, 1) }

Pmono (and PmonoArtic) manages the number of synths and looks more flexible form this aspect. But I would like to keep Pbindef objects for their concise way to set arguments.

There’s a somewhat hidden trick for this. You might think that Pmono(Artic) requires all the values to be set within that pattern – but in fact, the values can be changed from anywhere. The only things that matter are the content of the final event and that Pmono is the last thing to touch the event. So you can write the event in a Pbindef, and at the last second convert it to mono by Pchain-ing it.

(
Pbindef(\chords,
	\top, Pn(
		Pseries(
			0,
			Pconst(10, Pwrand([-2, -1, 1, 2], [0.05, 0.2, 0.55, 0.2], inf)),
			inf
		),
		inf
	),
	\degree, (Pkey(\top) +.x Pseries(0, Pwrand([-1, -2, -3], [0.1, 0.6, 0.3], inf), 4)).clump(4),
	\dur, Pwhite(1, 3, inf) * 0.5,
	\legato, Pwrand([0.8, 1.01], [0.2, 0.8], inf)
);

Pdef(\chordSeq, Pchain(
	PmonoArtic(\default, \dummy, 0),
	Pbindef(\chords)
)).play;
)

Pbindef(\chords, \dur, Pwhite(1, 3, inf) * 0.125);

Pdef(\chordSeq).stop;

hjh

1 Like

Many thanks for your quick reply.

I didn’t know that property, and indeed, that’s solving my initial problem, thanks !

I must admit that I am a bit struggling to understand what is going on with arguments. I will try to express my misunderstanding through different examples.

  1. I defined a “chord” argument as an array of integers ( I voluntarily didn’t call it “degree”). As the method .degreeToFreq can be applied to an integer or an array of integers, I was expecting it to be evaluated inside the Pbindef. As a comparison, if I replace the “freq” argument by “frequency”, the following code is ok :
(
SynthDef(\layer, {arg frequency = 100, out = 0, gate = 0;
   var sig;

   sig = Mix(SinOsc.ar(frequency, 0, 0.1));
   sig = sig * EnvGen.kr(Env.asr(1, 1, 1), gate, doneAction: 2);
   Out.ar(out, sig!2);
}).add;
)
a = Synth(\layer);

(
Pbindef(\testLayer,
    \type, \set,
    \id, a.nodeID,
    \args,#[\frequency, \gate],
	\frequency, 190,
    \gate, 1,
    \dur, 1,
    \amp, 0.6
).play(quant: 1);
)

Pbindef(\testLayer,\frequency, 200).play(quant: 1);

So I can access the “frequency” arg through the Pbindef and it is accepted by SinOsc.ar

Now I realised that if the freq arq is an array, when accessing it with Pbindef, not all the elements of the array are modified :

(
SynthDef(\layer, {arg frequency = #[100, 222, 444], out = 0, gate = 0;
   var sig;

   sig = Mix(SinOsc.ar(frequency, 0, 0.1));
   sig = sig * EnvGen.kr(Env.asr(1, 1, 1), gate, doneAction: 2);
   Out.ar(out, sig!2);
}).add;
)
a = Synth(\layer);

(
Pbindef(\testLayer,
    \type, \set,
    \id, a.nodeID,
    \args,#[\frequency, \gate],
	\frequency, #[90, 200, 300],
    \gate, 1,
    \dur, 1,
    \amp, 0.6
).play(quant: 1);
)

Pbindef(\testLayer,\frequency, #[80, 190, 290]).play(quant: 1);

Again, I might have missed something.

  1. Following your comment, I tried to adapt the code with

So I thought it would look like this :

SynthDef(\layer2, {arg freq = #[100, 222, 444], out = 0, gate = 0;
   var sig;

   sig = Mix(SinOsc.ar(freq, 0, 0.1));
   sig = sig * EnvGen.kr(Env.asr(1, 1, 1), gate, doneAction: 2);
   Out.ar(out, sig!2);
}).add;

a = Synth(\layer2);

Pbindef(\testLayer2,
    \type, \set,
    \id, a.nodeID,
    \args,#[\freq, \gate],
    \chord,#[0,2,4],
	\scale, Scale.major,
	\freq, Pfunc ({ |ev| ev[\scale].degreeToFreq(ev[\chord], 40.midicps, 1) }),
	// \freq, Pkey(\scale).degreeToFreq(Pkey(\chord), 60.midicps, 1) }), doesn't work
    \gate, 1,
    \dur, 1,
    \amp, 0.6
).play(quant: 1);


Pbindef(\testLayer2,\chord, #[1, 3, 5]).play(quant: 1);

By curiosity I tried using Pkey instead of Pfunc for the \freq arg, but it doesn’t seem appropriate (not linked to the current Event as Pfunc does ?). Here again, only the last element of the chord array is modified.

mic

I must admit that I am a bit struggling to understand what is going on with arguments.

Sure. One tricky thing about patterns/events is that there are invisible calculations going on, and it isn’t immediately clear where those are happening.

The Pbind(ef) does only the operations written within it. There are no automatic conversions within Pbind.

p = Pbind(\degree, Pseries(0, 1, inf), \dur, 0.5).asStream;

e = p.next(Event.default);
-> ( 'degree': 0, 'dur': 0.5 )

After evaluation of the Pbind, the resulting event has not done anything to convert the scale degree into a frequency.

When you play an event based on the default event prototype, certain keys (e.g. \freq) are automatically calculated based on other keys.

e.play;

-> ( 'instrument': default, 'degree': 0, 'dur': 0.5, 'amp': 0.1, 
  'server': localhost, 'sustain': 0.4, 'isPlaying': true, 'hasGate': true, 'id': [ 1000 ], 
  'msgFunc': a Function, 'freq': 261.6255653006 )

Now there is a frequency (and sustain and amp). These automatic conversions are described in the help file I posted earlier.

So when you said “I was expecting it to be evaluated inside the Pbindef” – that’s a minor misconception to correct. Automatic frequency conversions are never part of a Pbind’s processing. (You might write them explicitly, as in my Pfunc, but then they are not automatic :wink: .)

I defined a “chord” argument as an array of integers ( I voluntarily didn’t call it “degree”). As the method .degreeToFreq can be applied to an integer or an array of integers.

Let me take a Socratic approach for a minute.

According to this logic, then, degreeToFreq should be applied to any data types present in the event that understand it.

What about \dur? If I write \dur, 0.1, should it then convert this value to a tonic note raised by a semitone?

Of course not. \dur is not relevant to pitch in any way.

Now, go back and read the help file about automatic conversions. The relevant event keys for pitch are \degree, \note, \midinote, and \freq. The event defines these specific names to be associated with pitch.

\chord is not one of these. So, from the event’s point of view, \chord is exactly as relevant to pitch as \dur is – i.e., not relevant in the slightest. So the event does the same thing that it does with any keys that are not defined in the event to have a special meaning: it passes them to the synth without any other calculations being done.

I think your confusion is that in English, \chord is likely to be about pitch. But the event doesn’t speak English. It only understands the terms defined within the event prototype. So it doesn’t matter what you think \chord should mean. (Well… if you want \chord to mean something, you can define another event type or an alternate event prototype. The point is that the default event prototype can’t possibly account for every user’s assumptions.)

Now I realised that if the freq arq is an array, when accessing it with Pbindef, not all the elements of the array are modified :

This is an extremely common point of confusion with events and arrays.

If you write (frequency: [a, b, c]), it does not mean to pass the entire array to frequency. The real meaning is to produce three separate synths, one for each given frequency. This is the common use case: each synth produces only one note, so multiple parameters should produce multiple notes.

If you want a single synth to produce multiple notes (a less common case), then the syntax must distinguish this case from the other case. (Computer languages generally don’t like it when you write two different meanings exactly the same way.) SC events tell the difference by wrapping the array in another level of array: \frequency, #[[90, 200, 300]]. The outer level of array means “only one synth.” The inner level of array means “this is what to pass to the argument.”

(
SynthDef(\testArray, { |array = #[0, 0, 0], t_trig = 0, gate = 1|
	Poll.kr(t_trig, array);
	FreeSelf.kr(gate <= 0);
}).add;
)

e = (type: \on, instrument: \testArray, array: [1, 2, 3], trig: 1).play;
UGen(OutputProxy): 3
UGen(OutputProxy): 0
UGen(OutputProxy): 0
UGen(OutputProxy): 2
UGen(OutputProxy): 0
UGen(OutputProxy): 0
UGen(OutputProxy): 1
UGen(OutputProxy): 0
UGen(OutputProxy): 0

// ^^ crikey, that didn't work

e.put(\type, \off).play;

e = (type: \on, instrument: \testArray, array: [[1, 2, 3]], trig: 1).play;
UGen(OutputProxy): 1
UGen(OutputProxy): 2
UGen(OutputProxy): 3

e.putAll((type: \set, args: #[array, t_trig], array: [[10, 20, 30]], t_trig: 1)).play;
UGen(OutputProxy): 10
UGen(OutputProxy): 20
UGen(OutputProxy): 30

e.put(\type, \off).play;

So…

Pbindef(\testLayer2,
    \type, \set,
    \id, a.nodeID,
    \args, #[\freq, \gate],
    \chord, #[[0,2,4]],
	\scale, Scale.major,
	\freq, Pfunc ({ |ev| ev[\scale].degreeToFreq(ev[\chord], 40.midicps, 1) }),
	// \freq, Pkey(\scale).degreeToFreq(Pkey(\chord), 60.midicps, 1) }), doesn't work
    \gate, 1,
    \dur, 1,
    \amp, 0.6
).play(quant: 1);


Pbindef(\testLayer2,\chord, #[[1, 3, 5]]).play(quant: 1);

hjh

Oh, and…

By curiosity I tried using Pkey instead of Pfunc for the \freq arg, but it doesn’t seem appropriate (not linked to the current Event as Pfunc does ?)

Pkey is implicitly linked to the current event.

(
p = Pbind(
	\source, Pseries(0, 1, inf),
	\calc, Pkey(\source) * 2
).trace.asStream;
)

p.next(Event.default);
( 'calc': 0, 'source': 0 )
-> ( 'calc': 0, 'source': 0 )
( 'calc': 2, 'source': 1 )
-> ( 'calc': 2, 'source': 1 )  ... etc.

hjh

Thanks for your patience and clarity.

Actually, I’m afraid my misconception is more than minor ! Keeping in mind my first try, I wanted to write in the last post “inside the SynthDef” instead of the “Pbindef”. So far I never faced this case where I want to compile a method different from a Ugen inside a SynthDef. Now I just realised it’s something obvious:man_facepalming: : SynthDefs compile only Ugens !

Well not really, I didn’t expect SC to understand the meaning of this particular word, my idea was to create a new arg in a SynthDef and to use it in a Pbind acessing the synth node. Hence my test exemple with frequency (or whatever word) in place of “freq”.

Thanks for that reminder, I didn’t check the array dimension. I should have noticed it was a notes sequence and not a chord.

Ok, now it works great, I just made some modifications about the braces [ ], as the 2D Array should come out the Pfunc :

(
Pbindef(\testLayer2,
    \type, \set,
    \id, a.nodeID,
    \args, #[\freq, \gate],
    \chord, [1,3,5],
	\scale, Scale.major,
	\freq, Pfunc({ |ev| [ev[\scale].degreeToFreq(ev[\chord], 40.midicps, 1)] }), // added [ ]
	\gate, 1,
    \dur, 1,
    \amp, 0.6
).play(quant: 1);
)
	
Pbindef(\testLayer2, \chord, [0,2,4]);

Ok, so the only reason why I can’t use Pkey instead of Pfunc (Pkey(\scale).degreeToFreq…) is that Pkey can’t be the receiver of a method, right ?

Thanks again for your explanations, that was very helpful !

mic

Sure it can!

(
p = Pbind(
	\midinote, Pwhite(0, 7, inf).degreeToKey(Pkey(\scale), Pkey(\stepsPerOctave))
).asStream;
)

p.next(Event.default);
-> ( 'midinote': 4.0 )
-> ( 'midinote': 11.0 )
-> ( 'midinote': 4.0 )

It’s calculating just fine. What was the problem?

Now I just realised it’s something obvious :man_facepalming: : SynthDefs compile only Ugens !

There is actually a DegreeToKey UGen. (I do know that it takes some time to learn your way around new software’s documentation. In this case, you could have found this UGen by typing “degree” into the search page of SC’s help browser.)

hjh

Actually if I’m going to say something about Pkey, the example should actually use Pkey :grin:

(
p = Pbind(
	\degree, Pwhite(0, 7, inf),
	\midinote, Pkey(\degree).degreeToKey(Pkey(\scale), Pkey(\stepsPerOctave))
).asStream;
)

p.next(Event.default);
-> ( 'degree': 5, 'midinote': 9.0 )
-> ( 'degree': 0, 'midinote': 0.0 )
-> ( 'degree': 6, 'midinote': 11.0 )
-> ( 'degree': 2, 'midinote': 4.0 )

hjh

So my guess was wrong ! However I can’t figure out what is the issue in the .degreeToFreq case.
Here are the errors :

  1. If I use Pkey as a \scale reference :
(
p = Pbind(
	\scale, Scale.major,
	\freq, Pkey(\scale).degreeToFreq([0,2,4], 60.midicps,1),
).play;
)

->ERROR: Message 'degreeToFreq' not understood.
RECEIVER: a Pkey
  1. If I use Pkey as a \degree reference :
(
p = Pbind(
	\degree, Pwhite(0, 7, inf),
	\freq, (Scale.major).degreeToFreq(Pkey(\degree), 60.midicps,1),
).play;
)

->ERROR: Primitive '_BasicWrapAt' failed.
Index not an Integer

In the last case Pkey(\degree) doesn’t seem to return an integer.

Maybe the answer is in .degreeToKey’s and .degreeToFreq’s origin as that the first comes from Thunk class and the second from Scale class ?

Good to know !

It seems the issue here is that degreeToKey is a math operator, to be applied to numbers (or collections or streams of numbers), while degreeToFreq is a one-off method implemented only on Scale.

Unfortunately degreeToFreq has a superficial name resemblance to the better-supported math operator, but that doesn’t imply broad compatibility.

In general, if there is some method that does not apply to patterns, but it applies to the values being produced by patterns, use collect:

(
p = Pbind(
	\scale, Scale.major,
	\degree, Pwhite(0, 7, inf) + [0, 2, 4],
	// note: Pkey(\scale) is a pattern object
	// upon which `.collect` is called
	// that's important in this context!
	\freq, Pkey(\scale).collect { |scale, event|
		scale.degreeToFreq(event[\degree], 60.midicps, 1)
	}
).play;
)

hjh

Another good tip ! Thanks

mic

This trick is common and useful enough that it should probably be mentioned in the help file of Pbindef… which otherwise clobbers the mono stream player if one does the more obvious thing and Pbindef’s over a Pdef that is set to a Pmono or PmonoArtic, e.g. Pbindef(\chordsSeq) (instead of Pbindef(\chords)) in James’ example would cause that problem.

Frankly I wonder if automatic Pchain wrapping of Pmono[Artic] is not desirable in PbindProxy, since it does already have a case-by-case check for its src, i.e. it does:

	    if(src.class === PbindProxy) {
			src.set(*pairs);
			pat.wakeUp;
		} {
			if(src.isKindOf(Pbind)) { // ...
            }
		    src = PbindProxy.new(*pairs).quant_(pat.quant);
        }

That could be a legit feature request on github – provide a minimal example of a Pmono getting clobbered (default SynthDef – don’t include the array or \chord stuff in that example, to focus on the Pmono behavior) and explain the desired behavior.

hjh