Pattern Basics

Why use Patterns?

Player objects use Python lists, often known as arrays in other programming languages, to create sequences of values, such as pitch and durations. However, lists aren’t the most intuitive data structures for transformations. For example, try multiplying a list – what happens?

print([1, 2, 3] * 2)

The result is the same list repeated twice! If you want to manipulate the internal values (e.g. double them) in Python, then here’s how you might do it:

values = [1, 2, 3]
 
# Use a loop
my_list = []
for i in values:
    my_list.append(i * 2)
print(my_list)
 
# List comprehension
print([i*2 for i in values]) 

For both methods, it requires having to loop through all the values and multiply each one individually. Things get even more complicated if you also want to multiply every second value by a different number. This requires quite a lot of work, especially if you don’t know what numbers you’ll be using. This is where the Pattern class comes in.

Patterns act like regular Python lists but any mathematical transformation performed on it is done to each item in the list. The simplest way to create a pattern is just to add an upper-case “P” to the start of a list:

my_list    = [0, 1, 2, 3]
my_pattern = P[0, 1, 2, 3] 

Now when you perform an operation, such as multiplication, you’ll get the transformed pattern:

>>> print(my_pattern * 2)
P[0, 2, 4, 6] 

You can also create a pattern as you would create any other Python object by using the class name followed by brackets with arguments:

my_pattern = Pattern([0, 1, 2, 3]) 

Patterns are also “modulo index-able” which means that no matter what value we use as an index when accessing a Pattern’s data, as long as it’s an integer we get a value returned. If the index is greater than the length of the Pattern, then we just go back to the start of the Pattern and start looking:

>>> pat = P[0, 1, 2]
>>> print(pat[2])
2
>>> print(pat[3])
0 

Transformations

You can perform an operation on a pattern using a list or another pattern to create more complex transformations. For example, adding together the patterns P[0, 1, 2, 3] and P[4, 7] will perform the operation in turn meaning the resulting pattern will be the result P[0 + 4, 1 + 7, 2 + 4, 3 + 7], which is P[4, 8, 6, 10]. Using patterns of lengths with no common divisor will create a new pattern that contains all the combinations values:

>>> P[0, 1, 2, 3] + P[4, 5, -2]
P[4, 6, 0, 7, 5, -1, 6, 8, -2, 5, 7, 1] 

Patterns also have specific methods for transformation such as rotatereverse, and sort that can be used to manipulate order:

>>> P[4, 1, 3, 2].rotate()
P[1, 3, 2, 4]
>>> P[4, 1, 3, 2].reverse()
P[2, 3, 1, 4]
>>> P[4, 1, 3, 2].sort()
P[1, 2, 3, 4] 

You can evaluate help(Pattern) to see more information about the methods.

Pattern Functions

There are a number of functions that return different Patterns. These generate longer Patterns only using a few arguments. To see a list of Pattern functions, you can evaluate help(Patterns.Sequences).

In Python, you can generate a range of integers with the syntax range(start, stop, step). By default, start is 0 and step is 1. You can use PRange(start, stop, step) to create a Pattern object with the equivalent values:

>>> print(list(range(0, 10 2)))
[0, 2, 4, 6, 8]
>>> print(PRange(0, 10, 2))
P[0, 2, 4, 6, 8] 

And because these return instances of Pattern we can treat them as Pattern objects and use Pattern methods and perform arithmetic operations on them like so:

>>> print(PRange(0, 10, 2).reverse() + [1, 2])
P[9, 8, 5, 4, 1, 10, 7, 6, 3, 2] 

Concatenating Pattern

In Python, you would usually concatenate two lists (append one to another) by using the + operator but we’ve already seen that doing this with Patterns would add the values from one pattern to the contents of another. To concatenate two Pattern objects together, you can use the pipe symbol, |, which Linux users might be familiar with – it is used to connect command line programs by sending output from one process as input to another.

>>> print(PRange(4) | [1,7,6])
P[0, 1, 2, 3, 1, 7, 6] 

Pattern lacing and PGroups

What happens when a Pattern contains a nested list like so?

>>> pat = P[0, 2, [3, 5]]
>>> print(pat)
P[0, 2, P[3, 5]] 

First of all, the nested list is converted into a pattern (and any nested lists it might have contained are also converted). If we try and access the nested pattern here is what happens:

>>> print(pat[0])
0
>>> print(pat[1])
2
>>> print(pat[2])
3 

That’s strange…? You’d be forgiven for thinking the last line would return P[3, 5] because that’s the object in the third slot of pat but that’s not how Patterns behave. Patterns are laced which means that the values of the nested Patterns are returned when its parent Pattern is accessed. To access the second value of the nested pattern in the example above we need to loop through the Pattern a second time i.e. use an index value greater than the length of the Pattern:

>>> for i in range(6):
>>>     print(pat[i],)
0, 2, 3, 0, 2, 5 

Due to this, when print the length of a Pattern, you will see the size of the Pattern as if it was expanded as it is above. If you use round brackets and nested a tuple of values, you will find something very different happens:

>>> pat = P[0, 2, (3, 5)]
>>> print(pat)
P[0, 2, P(3, 5)] 

The last item in the pattern is known as a PGroup and is used to keep values within a pattern together i.e. not laced:

>>> print(pat[0])
0
>>> print(pat[1])
2
>>> print(pat[2])
P(3, 5) 

Grouping values together means that they are played at the same time and this comes in really handy when you want to play notes together e.g. chords:

p1 >> pluck([(0, 2, 4), (0, 3, 5)], dur=4) 

You can add tuples/ PGroups to a Pattern to create a new pattern of PGroup items:

>>> pat = P[0, 3, 5, 4]
>>> print(pat + (0, 2))
P[P(0, 2), P(3, 5), P(5, 7), P(4, 6)]
>>> print(pat + [(0, 2), (2, 4)])
P[P(0, 2), P(5, 7), P(5, 7), P(6, 8)]