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 rotate, reverse,
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)]
|