Player Keys

Introduction

Player Keys are the easiest way to create relationships between your Player objects. They let you share data in a reactive and dynamic way and are especially useful if you are collaborating when using the Troop interface. They are accessed as you would any Python object attribute, by typing the name of the object, a dot, then the name of the attribute e.g. p1.pitch. The neat thing about Player Keys is that, if the original Player is updated, so is the Player Key, so you don’t need to worry about updating any values.

These work for any attribute of a Player e.g. degree, amp, sus, pan or anything else. It will even work for custom attributes on your own SynthDefs. It isn’t quite the same as copying and pasting the Pattern value, but will always use the most up-to-date value held by that attribute. Let’s look at an example:

p1 >> pads([0, 4, 5, 3], dur=4)
p2 >> pluck(p1.degree, dur=1/2)

The Pattern used by p1 is P[0, 4, 5, 3] but the duration is 4 beats for each note. After using p1.degree, the p2 player will play the same pitch, only changing the value when the pitch of p1 changes. Essentially, p2 will play each pitch 8 times. To access the Pattern directly, you need to read the data from the attr dictionary like so:

p1 >> pads([0, 4, 5, 3], dur=4)
p2 >> pluck(p1["degree"], dur=1/2)

Note: if p1 is updated, then the pitch of p2 will not be updated until the line of code is re-run when using the attr dictionary.

Aliases

The Player Key uses two aliases for retrieving a player’s degree value. You can use .pitch for synths and .char for the play synth, which makes it easier to understand what value you are actually getting.

Maths and Logic

You can perform any mathematical operation on a Player Key that is available in Python:

SymbolNameDescription
+Addition
Subtraction
/Division
*Multiplication
**Power
^Power
//Floor divisionReturns the integer result of the division
%Modulo divisionReturns the remainder part of the division
==Equal toTests if the Player Key is equal to the given value, returns 1 if True and 0 if False
!=Not equal to Tests if the Player Key is not equal to the given value, returns 1 if True and 0 if False
>Greater than Tests if the Player Key is greater than the given value, returns 1 if True and 0 if False
<Less thanTests if the Player Key is less than the given value, returns 1 if True and 0 if False
>=Greater than or equal toTests if the Player Key is greater than or equal to the given value, returns 1 if True and 0 if False
<=Less than or equal toTests if the Player Key is less than or equal to the given value, returns 1 if True and 0 if False

Methods

PlayerKey.transform(func)

The behaviour of a Player Key is similar to that of the TimeVar. It is essentially a number/string that changes over time but any transformation made to it, such as multiplying it by two, will also hold true whenever the value changes.

>>> p1 >> pads([0, 4, 5, 3], dur=4)
>>> a, b = p1.pitch, p1.pitch * 2
>>> print(a, b)
4, 8
>>> print(a, b)
5, 10

As the time changes, so does the player’s pitch and performing any arithmetic operation on it will create a new Player Key that holds information about the original and the transformation. That way it always returns the transformation of the correct value.

Things get tricky when we want to use a custom function on our player key. Let’s say we want to want transform our Player Key into a 5 when odd and a 3 when even. Here’s a function that will do that you:

>>> def odd_test(num):
...    return 5 if num % 2 == 1 else 3

>>> p1 >> pads([0, 1, 2, 3], dur=4) 
>>> a, b = p1.pitch, odd_test(p1.pitch)
>>> print(a, b)
0, 3
>>> print(a, b)
1, 3
>>> print(a, b)
2, 3

Notice anything? We always got 3, even if the Player Key’s value was odd. To apply a function (as opposed to a mathematical operation) you need to use the transform method and supply the method with the function to transform it. It should take only one input argument:

>>> p1 >> pads([0, 1, 2, 3], dur=4)
>>> p2 >> pluck(p1.pitch.transform(odd_test), dur=1/2)
>>> print(p1.pitch, p2.pitch)
0, 5
>>> print(p1.pitch, p2.pitch)
1, 3
.map(mapping_dict, default=0)

Instead of defining a function to return certain values as above, we can provide a mapping in the form of a Python dictionary. The mappings can be one-to-one values or functions. If a Player Key’s value is not in the dictionary, then the default value is returned.

>>> p1 >> pads([0, 4, 5, 3], dur=4)
>>> p2 >> pluck(p1.pitch.map({4: 1, 3: 0}, default=2))
>>> print(p1.pitch, p2.pitch)
0, 2
>>> print(p1.pitch, p2.pitch)
4, 1
>>> print(p1.pitch, p2.pitch)
5, 2
>>> print(p1.pitch, p2.pitch)
3, 0

Here’s how you can implement the odd_test transformation using map and a lambda function:

p1 >> pads([0, 4, 5, 3], dur=4)
p2 >> pluck(p1.pitch.map({lambda x: x % 2 == 1: 5}, default=3)
.accompany(rel=[0, 2, 4])

Returns a Player Key that, when the source Player Key changes value, moves to the closest value that is +/- the values in rel i.e. [0, 2, 4]. When used with .pitch this will move to the nearest pitch value that completes the third or fifth above or below the note.

p1 >> pads([0, 4, 5, 3], dur=4)
p2 >> pluck(p1.pitch.accompany())

Useful Examples

Create a chord sequence based on the pitch of a player:

p1 >> bass([0, 4, 5, 3], dur=4)
p2 >> pluck(p1.pitch + (0, 2, 4), dur=1/2)

Use the pitch of a player within a sequence:

p1 >> bass([0, 4, 5, 3], dur=4)
p2 >> pluck([p1.pitch, 7, 6, 7], dur=1/2)

Invert the amplitude of a player:

p1 >> play("x-o-", amp=[1,1,0,1,0,1,0], dur=1/4)
p2 >> play("*", amp=p1.amp != 1, dur=1/4)

Harmonise and pan two players to opposing channels:

p1 >> pluck([0, 2, 6, 3, 2, 4, 1, -2], dur=1/2, pan=[-1, 0.5, 0.25, -0.5, 0])
p2 >> blip(p1.pitch + 2, dur=1/2, pan=p1.pan * -1)

Use indexing to accompany the root note of a chord sequence:

p1 >> pluck([0, 4, 5, 3], dur=4) + (0, 2, 4)
p2 >> blip(p1.pitch[0].accompany())