Amen Break PythonScore Tutorial

By Jacob Joaquin <jacobjoaquin@gmail.com>

Updated April 10th, 2013

Introduction

PythonScore is a fully modular score environment for the Csound unified CSD file. Unlike other alternative text-based score environments, PythonScore is non-imposing and does its best to get out of the way rather than to impose a strict set of rules for composition. Classical Csound score is fully supported, all of Python 2.7 is available, and composers can pick and choose their score tools à la carte.

The purpose of this tutorial is to try and prove the merits of using the Python language in favor over the classical Csound score. The classical score is a fixed system while Python is a very mature and highly extensible language with a vast collection of first and third party modules. Despite being a general purpose language that wasn’t designed for music, Python is particularly well suited for composition.

PythonScore is not meant as a full replacement of the classical Csound score, but as an enhancement. Any and all existing classical score commands still work, and PythonScore itself outputs classical score code. Perhaps PythonScore should be thought as a user-friendly higher level abstraction of the classic score.

PythonScore is still early in development, though the library of use cases and examples is rapidly growing. And all from a single developer, which will hopefully change soon. The secondary purpose of this tutorial is to attract a small group of people to start experimenting with this alternative Csound score environment, to find out what works, what’s easy and what’s not, and to collect new ideas for future upgrades. It is also the belief of the author that once people have some hands-on experience, many of the advantages of working with PythonScore will become obvious. Until then, it’s just the ramblings of one person’s perspective.

Since PythonScore is under development, consider this tutorial a living document, as it will be continually updated to keep up with any future changes. Send questions, comments, and bugs to jacobjoaquin@gmail.com, or write to the Csound Mailing List.

Files

The Csound CSD files and the amen_break.wav sample ship with the csd module and can be found in the following folder:

/examples/tutorials/amen/

The Orchestra

In order to bring focus to the enhanced set of features and compositional techniques Python brings to the score environment, every example uses the exact same orchestra throughout the entirety of this tutorial. Let’s take a look:

sr = 44100
kr = 44100
ksmps = 1
nchnls = 2
0dbfs = 1.0

gS_loop = "amen_break.wav"
gisr filesr gS_loop
gilength filelen gS_loop
gibeats = 16 
gi_sampleleft ftgen 1, 0, 0, 1, gS_loop, 0, 4, 0

instr 1
    idur = p3
    iamp = p4
    ibeat = p5
    itune = p6
    ipos = ibeat / gibeats * gilength * gisr

    aenv linseg iamp, idur - 0.01, iamp, 0.01, 0
    a1, a2 loscilx aenv, itune, 1, 0, 1, ipos, 0
    outs a1, a2
endin

Source: amen_classic.csd

The orchestra contains only a single instrument, a basic sampler with the Amen Break as its only audio source. The instrument includes support for three custom pfield inputs for amplitude, the beat positional sample offset, and for tuning the sample. The fact that this is only a very basic instrument is purposeful as techniques discussed in this tutorial will demonstrate that even simple instruments can be greatly enhanced with using some of the new techniques afforded by Python.

The output of this sampler has a very trashy quality to it, and sounds like it was produced with a tracker such as Fast Tracker II.

The use of a drum loop is chosen for a very specific reason. The ability to express events in time is a defining attribute of PythonScore, and rhythm-based music is particullary well suited for this purpose. Do not leave with the wrong impression that PythonScore caters to 4/4 drum music, as this is far from the truth.

The Python Interpreter

Before diving into the tutorial, an introduction the the Python Interpreter is in order as it a wonderful tool for quickly testing various aspects of the Python language. This tutorial will occasionally embed examples using it as well.

Typically, the Python interpreter is used in the terminal, which is invoked by typing “python”:

Quorra ~ $ python

This starts an interactive Python session in which users can begin entering various statements:

>>> print 'hello world'
hello world
>>> 440 * 2 ** (-9 / 12.0)
261.6255653005986
>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

The CsoundQt graphical front end comes equipped with a Python console. There’s also IPython which supercharges the Python Interpreter with a lot of great features for Python beginners and veterans alike.

Classical Score to PythonScore

In this section, we’ll walk you through some of the basic features of Python and PythonScore. The tutorial starts with a classical Csound score, and adds new PythonScore idioms one by one until the score as been fully translated. The end result is the new Python score.

t 0 170

; Measure 1
i 1 0.0 0.5 0.707 0 1
i 1 1.0 0.5 0.707 1 1
i 1 2.5 0.5 0.707 0 1
i 1 3.0 0.5 0.707 1 1

; Measure 2
i 1 4.0 0.5 0.707 0 1
i 1 5.0 0.5 0.707 1 1
i 1 6.5 0.5 0.707 0 1
i 1 7.0 0.5 0.707 1 1

; Measure 3
i 1 8.0  0.5 0.707 0 1
i 1 9.0  0.5 0.707 1 1
i 1 10.5 0.5 0.707 0 1
i 1 11.0 0.5 0.707 1 1

; Measure 4
i 1 12.0 0.5 0.707 0 1
i 1 13.0 0.5 0.707 1 1
i 1 14.5 0.5 0.707 0 1
i 1 15.0 0.5 0.707 1 1

Source: amen_classic.csd

The score code produces four measures of the same drum pattern. The pattern itself is the classical drum ‘n’ bass.

Upgrade

Porting a classical score into Python requires a few lines of code to set up. First, set the argument of the CsScore bin utility to “python”. Then import PythonScoreBin:

from pysco import PythonScoreBin

PythonScoreBin is a subclass of PythonScore specifically tailored for use inside a Csound Unified CSD file.

An object called score is instantiated from the PythonScoreBin() class. Additionally, variable cue is created and points to the score.cue context manager. We’ll go into details later as to why. Classical Csound source code is entered into the score() object via the write() method. The result is this new score:

<CsScore bin="python">

from csd.pysco import PythonScoreBin
from random import choice

score = PythonScoreBin()
cue = score.cue

score.write('''
t 0 170

; Measure 1
i 1 0.0 0.5 0.707 0 1
i 1 1.0 0.5 0.707 1 1
i 1 2.5 0.5 0.707 0 1
i 1 3.0 0.5 0.707 1 1

; Measure 2
i 1 4.0 0.5 0.707 0 1
i 1 5.0 0.5 0.707 1 1
i 1 6.5 0.5 0.707 0 1
i 1 7.0 0.5 0.707 1 1

; Measure 3
i 1 8.0  0.5 0.707 0 1
i 1 9.0  0.5 0.707 1 1
i 1 10.5 0.5 0.707 0 1
i 1 11.0 0.5 0.707 1 1

; Measure 4
i 1 12.0 0.5 0.707 0 1
i 1 13.0 0.5 0.707 1 1
i 1 14.5 0.5 0.707 0 1
i 1 15.0 0.5 0.707 1 1
''')
 
</CsScore>
</CsoundSynthesizer>

Source: amen_upgrade.csd

If you are worried that you’ll be required to throw out everything you know about the classic Csound score, fear not, as you can still use it with the score() object virtually untouched, or in combination with other Python features as you learn them.

Tempo

The first item we extract from the block of classical score code is the tempo statement, and rewrite it like this:

score.t(170)

This sets the tempo of the score to 170 beats-per-minute.The updated score now looks like this:

<CsScore bin="python">

from csd.pysco import PythonScoreBin
from random import choice

score = PythonScoreBin()
cue = score.cue

score.t(170)

score.write('''
; Measure 1
i 1 0.0 0.5 0.707 0 1
i 1 1.0 0.5 0.707 1 1
i 1 2.5 0.5 0.707 0 1
i 1 3.0 0.5 0.707 1 1

; Measure 2
i 1 4.0 0.5 0.707 0 1
i 1 5.0 0.5 0.707 1 1
i 1 6.5 0.5 0.707 0 1
i 1 7.0 0.5 0.707 1 1

; Measure 3
i 1 8.0  0.5 0.707 0 1
i 1 9.0  0.5 0.707 1 1
i 1 10.5 0.5 0.707 0 1
i 1 11.0 0.5 0.707 1 1

; Measure 4
i 1 12.0 0.5 0.707 0 1
i 1 13.0 0.5 0.707 1 1
i 1 14.5 0.5 0.707 0 1
i 1 15.0 0.5 0.707 1 1
''')
 
</CsScore>
</CsoundSynthesizer>

Source: amen_tempo.csd

The t() method takes additional arguments and works exactly the same as the t-statement in Csound with one exception. In the classical Csound score, the t-statement requires a zero as the first argument. This has been excluded from the PythonScore version to reduce redundancy.

Time is Relative

For the most part, when events are entered into a classical Csound score, the start times are in global beats. PythonScore changes this with the introduction of the cue() object, a Python context manager for moving the current position time in the score, like a needle on a record player. All events entered into PythonScore are relative the current positional value of cue().

with cue(12):
    score('i 1 0 0.5 0.707 0 0')

While the start time value in pfield-2 is 0, this event will start at 12 beats into the piece; Start times are translated to reflect the current position of the cue(). The output looks like this:

i 1 12 0.5 0.707 0 0

Remember when we set cue = score.cue earlier? This was done to create a shorthand version to eliminate having to specify the full score object and method.

As for translating the drum ‘n’ bass parts, each measure has been split into its own chuck of events. The first event in measure 2 has been changed to 0.0, and the same goes for measures 3 and 4. The cue() object allows the composer locally to the current measure, rather than manually calculating the global time at all times.

from csd.pysco import PythonScoreBin
from random import choice

score = PythonScoreBin()
cue = score.cue

score.t(170)

with cue(0):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

with cue(4):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

with cue(8):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

with cue(12):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

Source: amen_cue.csd

There are of course many more benefits. For example, moving entire sections of code is a fairly trivial process in PythonScore. Doing the same in the classical score could require translating start times down a long pfield-2 column. It is the assertion of the author that this feature, and this feature alone, makes transitioning to PythonScore more than worthwhile.

Measures

The drum ‘n’ bass score is in 4/4. While cue() allows us to treat beats local to the current measure, the arguments for the cue() are still in absolute global time. Out of the box, PythonScore does not support measures. However, Python is highly extensible and lets you roll your own. Implementing 4/4 measures is done by defining a new function:

def measure(t):
    return cue((t - 1) * 4.0)

This two line custom function takes the measure number as its sole argument and translates measure time to beat time, and returns an instance of cue().

Let’s focus little on the math. The global score starts at 0, but measures start at 1. The is overcome with an offset of -1. In 4/4, there are four beats per measure, which requires the offset value to be multiplied by 4. Testing the math in the Python Interpreter with inputting 4:

>>> def test_measure(t):
...     return (t - 1) * 4.0
...
>>> test_measure(4)
12.0

Inputting 4 into our test the value returned is 12, which is the starting time of the first event in measure 4 in the original classical score. Applying measure() to the PythonScore, were left with the following. Notice that the comments have been factored out now that the code itself clearly indicates what a measure is:

from csd.pysco import PythonScoreBin
from random import choice

def measure(t):
    return cue((t - 1) * 4.0)

score = PythonScoreBin()
cue = score.cue

score.t(170)

with measure(1):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

with measure(2):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

with measure(3):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

with measure(4):
    score.write('''
    i 1 0.0 0.5 0.707 0 1
    i 1 1.0 0.5 0.707 1 1
    i 1 2.5 0.5 0.707 0 1
    i 1 3.0 0.5 0.707 1 1
    ''')

Source: amen_measure.csd

Instrument Event Function

The score.i() method is another way for entering in data into the score. Instead of a string, it accepts any number of arguments and constructs a valid classical score event string. For example:

score.i(1, 0, 1, 0.707, 8.00)

Outputs:

i 1 0 1 0.707 8.00

Replacing the calls to score.write() with the i() method in the Amen score, the updated code looks like this:

from csd.pysco import PythonScoreBin
from random import choice

def measure(t):
    return cue((t - 1) * 4.0)

score = PythonScoreBin()
cue = score.cue

score.t(170)

with measure(1):
    score.i(1, 0.0, 0.5, 0.707, 0, 1)
    score.i(1, 1.0, 0.5, 0.707, 1, 1)
    score.i(1, 2.5, 0.5, 0.707, 0, 1)
    score.i(1, 3.0, 0.5, 0.707, 1, 1)

with measure(2):
    score.i(1, 0.0, 0.5, 0.707, 0, 1)
    score.i(1, 1.0, 0.5, 0.707, 1, 1)
    score.i(1, 2.5, 0.5, 0.707, 0, 1)
    score.i(1, 3.0, 0.5, 0.707, 1, 1)

with measure(3):
    score.i(1, 0.0, 0.5, 0.707, 0, 1)
    score.i(1, 1.0, 0.5, 0.707, 1, 1)
    score.i(1, 2.5, 0.5, 0.707, 0, 1)
    score.i(1, 3.0, 0.5, 0.707, 1, 1)

with measure(4):
    score.i(1, 0.0, 0.5, 0.707, 0, 1)
    score.i(1, 1.0, 0.5, 0.707, 1, 1)
    score.i(1, 2.5, 0.5, 0.707, 0, 1)
    score.i(1, 3.0, 0.5, 0.707, 1, 1)

Source: amen_i.csd

Nesting Time

A with cue() statement can be placed inside a hierarchy of other with cue() statements. This allows composers to reset the relative positional time to zero regardless of the current scope of time by writing another with cue() statement: For example, this is possible:

with cue(32):
    with cue(4):
        with cue(1):
            score.i(1, 0, 1, 0.707, 0, 1)

Though the start time of the event is written as 0, the actual start time of the event is 37, which is the accumulation of all the args in the cue nesting hierarchy plus pfield-2: 32 + 4 + 1 + 0

The greater implication is that time can be completely decoupled from events. In this version of the score the start times are moved from the pfield-2 column to the the cue(), and then set to 0.

from csd.pysco import PythonScoreBin
from random import choice

def measure(t):
    return cue((t - 1) * 4.0)

score = PythonScoreBin()
cue = score.cue

score.t(170)

with measure(1):
    with cue(0.0): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(1.0): score.i(1, 0, 0.5, 0.707, 1, 1)
    with cue(2.5): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(3.0): score.i(1, 0, 0.5, 0.707, 1, 1)

with measure(2):
    with cue(0.0): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(1.0): score.i(1, 0, 0.5, 0.707, 1, 1)
    with cue(2.5): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(3.0): score.i(1, 0, 0.5, 0.707, 1, 1)

with measure(3):
    with cue(0.0): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(1.0): score.i(1, 0, 0.5, 0.707, 1, 1)
    with cue(2.5): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(3.0): score.i(1, 0, 0.5, 0.707, 1, 1)

with measure(4):
    with cue(0.0): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(1.0): score.i(1, 0, 0.5, 0.707, 1, 1)
    with cue(2.5): score.i(1, 0, 0.5, 0.707, 0, 1)
    with cue(3.0): score.i(1, 0, 0.5, 0.707, 1, 1)
 

Source: amen_nest.csd

Score Instruments

Composers can create named Python functions that call their corresponding orchestra instruments. Furthermore, multiple functions can be created for the same instrument, but with more specific intent as to what the resultant behavior of the instrument is, like a patch.

The sampler instrument in the orchestra is fairly simple and generic in design. While the Amen Break loop is fixed, the instrument itself has no knowledge of kicks or snares. It only accesses the part of the sample denoted by the beat positional offset passed into pfield-5.

Looking at the PythonScore code from the last example, there are only two unique calls to score.i(), and even then all the arguments are identical except for pfield-5:

score.i(1, 0, 0.5, 0.707, 0, 1)
score.i(1, 0, 0.5, 0.707, 1, 1)

The former plays a kick drum while the latter plays a snare. Though looking at the code who knows which is which and who is who. [1] This is because the classic Csound events are more or less just data, and don’t naturally signify what’s behind the data.

These statements are also unnecessarily long in Python, which makes them ripe to for consolidation. Defining functions for the kick and snare avoids the extra overhead by factoring out all the args. Something is made possible by the cue() object.

def kick():
    score.i(1, 0, 0.5, 0.707, 0, 1)

def snare():
    score.i(1, 0, 0.5, 0.707, 1, 1)

An equally important benefit to this approach is that with proper names, this technique creates code that is self documenting and easier to read. If you type kick() it plays a kick. Type snare() it plays a snare. Type snozberry() it plays a snozberry(). [2]

from csd.pysco import PythonScoreBin
from random import choice

def measure(t):
    return cue((t - 1) * 4.0)

def kick():
    score.i(1, 0, 0.5, 0.707, 0, 1)

def snare():
    score.i(1, 0, 0.5, 0.707, 1, 1)

score = PythonScoreBin()
cue = score.cue

score.t(170)

with measure(1):
    with cue(0.0): kick()
    with cue(1.0): snare()
    with cue(2.5): kick()
    with cue(3.0): snare()

with measure(2):
    with cue(0.0): kick()
    with cue(1.0): snare()
    with cue(2.5): kick()
    with cue(3.0): snare()

with measure(3):
    with cue(0.0): kick()
    with cue(1.0): snare()
    with cue(2.5): kick()
    with cue(3.0): snare()

with measure(4):
    with cue(0.0): kick()
    with cue(1.0): snare()
    with cue(2.5): kick()
    with cue(3.0): snare()

Source: amen_instr.csd

Drum Pattern

The score contains four measures that contain identical content. Functions can store musical phrases, everything from a single note to complete works of Beethoven if necessary. In this case, we have a simple four notes drum ‘n’ bass pattern.

def drum_pattern():
    with cue(0.0): kick()
    with cue(1.0): snare()
    with cue(2.5): kick()
    with cue(3.0): snare()

These events will not be generated until the function is called. For each measure, four lines of score instruments is replaced with drum_pattern().

from csd.pysco import PythonScoreBin
from random import choice

def measure(t):
    return cue((t - 1) * 4.0)

def kick():
    score.i(1, 0, 0.5, 0.707, 0, 1)

def snare():
    score.i(1, 0, 0.5, 0.707, 1, 1)

def drum_pattern():
    with cue(0.0): kick()
    with cue(1.0): snare()
    with cue(2.5): kick()
    with cue(3.0): snare()

score = PythonScoreBin()
cue = score.cue

score.t(170)

with measure(1): drum_pattern()
with measure(2): drum_pattern()
with measure(3): drum_pattern()
with measure(4): drum_pattern()

Source: amen_pattern.csd

Functions aren’t limited to storing measures of data. Notes, licks, phrases, bars, sections, scores, can all be placed inside a function. Later when it’s time to start arranging, musical patterns like these can be more or less dropped into measure() blocks.

Interlude

The score we’re left with is fundamentally more expressive and more readable than the original Csound score. Even if all the intricacies of Python may still a mystery, reading through the code you should be able to pick up on the instrumentation as well as seeing that the score is divided into measures.

In the examples to come, new features and musical phrases are built on top of the existing work covered in the previous examples. The orchestra will remain unchanged, though the music will. Some of these new idioms are more complex, so it worth playing with what has be presented thus far. Though do read on even if you just skim that material to get a sense as to what other compositional advantages that Python brings to composing with code.

Looping Through Lists

A Python list is a container for storing various data, which may include numbers, strings, other lists, functions, etc. And they make a wonderful additional to the score environment. They’re highly versatile and time saving. This example is a list of integers:

[1, 2, 3, 4]

A loop is created from a list with the for statement, as it will iterate through each value. In the score as the for loop iterates through the list, the m variable assumes the the value of the current list item. This is shorthand for calling drum_pattern() for measures 1 through 4:

for m in [1, 2, 3, 4]:
    with measure(m): drum_pattern()

Source: amen_list.csd

Range

The Python range() generates lists of integers automatically and is useful in a range of different contexts. Here are three examples of lists generated with range():

>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> range(4, 8)
[4, 5, 6, 7]
>>> range(0, 32, 4)
[0, 4, 8, 12, 16, 20, 24, 28]

Substituting that manually created list with range() leaves us with this score update.

for m in range(1, 5):
    with measure(m): drum_pattern()

Source: amen_range.csd

Choice Variation

Python comes with the random module which includes the choice() function that returns a random value from a list. Prior to this example, all kick and snare events use the same sample from the Amen break. Applied to the kick() and snare() score instruments, these functions randomly choose samples from a fixed list of beat positional offsets in the drum loop.

The kick() function chooses from positions 0, 0.5, 4, and 4.5:

def kick():
    sample = choice([0, 0.5, 4, 4.5])
    score.i(1, 0, 0.5, 0.707, sample, 1)

The snare() functions chooses from positions 1, 3, 5, and 7:

def snare():
    sample = choice([1, 3, 5, 7])
    score.i(1, 0, 0.5, 0.707, sample, 1)

Source: amen_choice.csd

Hats

Once again, a new score instrument is created from a function by re purposing the code used in kick() and snare() for a hihat:

def hat():
    sample = choice([1.5, 2, 2.5, 6.5, 7.5])
    score.i(1, 0, 0.35, 0.707, sample, 1)

A second drum pattern called drum_pattern_8th_hats() is created, a variation on the original drum pattern. In fact, drum_pattern() is called right in this new function.

def drum_pattern_8th_hats():
    drum_pattern()

    for t in range(8):
        with cue(t / 2.0): hat()

Both patterns are played side by side in the updated score:

with measure(1): drum_pattern()
with measure(2): drum_pattern_8th_hats()
with measure(3): drum_pattern()
with measure(4): drum_pattern_8th_hats()

Source: amen_hats.csd

Post-Processing Score Data

Any data entered into the score() object via the PythonScore write() or i() methods can be treated as data and post-processed with pmap(). If you ran the last example multiple times, you might have noticed the “samples out of range” warning. This can be solved in fell swoop. The pmap() method takes the following input:

pmap(event_type, instr, pfield, function, *args, **kwargs)

To avoid samples being played out of range, all the amplitudes in the score are lowered. Pfield-4 is designated for amplitude, and each value in the pfield-4 column multiplied by a constant value. The process uses a custom definition for returning the product of two numbers:

def multiply(x, y):
    return x * y

The value of the pfield is passed in as the first arg x and 0.707 as y. Pfield-4 is replaced with the value returned by the multiply() function

score.t(170)

with measure(1): drum_pattern()
with measure(2): drum_pattern_8th_hats()
with measure(3): drum_pattern()
with measure(4): drum_pattern_8th_hats()

score.pmap('i', 1, 4, multiply, 0.707)

Source: amen_postprocess.csd

The pmap() function only works on data already in the score. For this reason, the best place for most cases will be either after the last event is generated and/or just before score.end() is called.

Pre-Processing Score Data

The p_callback() method is the pre-processing equivalent of pmap(). The p_callback() method registers a function to use with a specific pfield for a specific instrument, and manipulates data as it is entered into the score.

As an example, two related set of p_callback() functions are created. The first tunes all the drum events up a perfect 5th by altering the pfield-6 column used for tuning the samples. Since changing the pitch also affects the length of the sample, which in turn can cause the more of the sample to bleed into the next sample in the audio file, a second p_callback() modifies the duration by the inverse of a perfect 5th.

perfect_5th = 2 ** (7 / 12.0)
score.p_callback('i', 1, 6, multiply, perfect_5th)
score.p_callback('i', 1, 3, multiply, 1 / perfect_5th)

Where these callbacks are placed matter. Typically it is good practice to place p_callback() functions before any events are placed in the score. In this example, these are placed just prior to the first write to the score().

perfect_5th = 2 ** (7 / 12.0)
score.p_callback('i', 1, 6, multiply, perfect_5th)
score.p_callback('i', 1, 3, multiply, 1 / perfect_5th)

score.t(170)

with measure(1): drum_pattern()
with measure(2): drum_pattern_8th_hats()
with measure(3): drum_pattern()
with measure(4): drum_pattern_8th_hats()

score.pmap('i', 1, 4, multiply, 0.707)

Source: amen_preprocess.csd

Transpose

The perfect 5th ratio was hard coded into the last example, but transposing data is something that is useful enough that it’s worth taking the time to consolidate it into a reusable function.

The transpose() function accepts two args, one which is required and a second optional default argument. The first arg is the value in half steps in which to transpose. Without a second arg, it outputs the ratio of transposition. If the second arg is supplied, then it will apply the transposition ratio to this before returning the output.

def transpose(halfstep, value=1):
    return value * 2 ** (halfstep / 12.0)

The score.p_callback() calls are refactored using this new function.

score.p_callback('i', 1, 6, multiply, transpose(7))
score.p_callback('i', 1, 3, multiply, 1 / transpose(7))

Source: amen_transpose.csd

Default Arguments

Earlier in the tutorial when kick(), snare(), and hat() were created, the instr number, start time, duration, amplitude, beat positional offset, and tuning were factored out of the interface. Three of these are put back in as optional arguments: duration, amplitude, and tuning. Since these are optional arguments, we can continue using the existing short form. Here are the updated score instruments:

def kick(dur=1, amp=1, tune=1):
    dur = dur * 0.5
    amp = amp * 0.707
    dur = dur * (1.0 / tune)

    sample = choice([0, 0.5, 4, 4.5])
    score.i(1, 0, dur, amp, sample, tune)

def snare(dur=1, amp=1, tune=1):
    dur = dur * 0.5
    amp = amp * 0.707
    dur = dur * (1.0 / tune)

    sample = choice([1, 3, 5, 7])
    score.i(1, 0, dur, amp, sample, tune)

def hat(dur=1, amp=1, tune=1):
    dur = dur * 0.35
    amp = amp * 0.707
    dur = dur * (1.0 / tune)

    sample = choice([1.5, 2, 2.5, 6.5, 7.5])
    score.i(1, 0, dur, amp, sample, tune)

A new pattern is created that plays four kicks on the beat transposed down a perfect fourth.

def drum_pattern_2():
    for t in range(4):
        with cue(t): kick(tune=transpose(-5))

Here is drum_pattern_2() tested in context with two other patterns:

with measure(1): drum_pattern()
with measure(2): drum_pattern_8th_hats()
with measure(3): drum_pattern_2()
with measure(4): drum_pattern_8th_hats()

Source: amen_default_args.csd

Player Instruments

Python functions can accept other functions as arguments. This is taken advantage of in this example as we create a generic player instrument that accepts either kick, snare, or hat as the first argument, then plays the passed score instrument based on the the proceeding arguments. This is possible since our score instruments have identical interfaces/signatures.

The player instrument is named swell() as it was originally envisioned as way to play instruments with ramped amplitudes. Here is a description of the rest of the arguments in order in which they appear:

  • The duration of that phrase in beats.
  • The duration of the individual instrument events.
  • The number of beats to play through the lifespan of the player.
  • The amplitude of the first note.
  • The amplitude of the target last note.
  • Optional arg for tuning.
def swell(instr, phrase_dur, note_dur, n_beats, start_amp, end_amp, tune=1):
    inc = (end_amp - start_amp) / (n_beats + 1.0)

    for t in xrange(n_beats):
        with cue(t / float(n_beats) * phrase_dur):
            instr(note_dur, start_amp + t * inc, tune)

Here is a new pattern that utilizes the new player instrument:

def intro():
    with measure(1):
        swell(kick, 4, 1, 4, 0.3, 1, transpose(-3))

    with measure(2):
        swell(kick, 4, 1, 4, 0.3, 1, transpose(-3))
        swell(hat, 2, 1, 4, 1.0, 0.5)
        with cue(2.0): swell(hat, 2, 1, 4, 0.5, 1.0)

    with measure(3):
        swell(kick, 4, 1, 4, 0.3, 1, transpose(-3))
        swell(hat, 2, 1, 4, 1.0, 0.5)
        with cue(2.0): swell(hat, 2, 1, 4, 0.5, 1.0)
        swell(snare, 4, 0.5, 16, 0.4, 0.05, transpose(7))

    with measure(4):
        swell(kick, 4, 1, 8, 0.3, 1)
        swell(hat, 2, 1, 4, 1.0, 0.5)
        with cue(2.0): swell(hat, 2, 1, 4, 0.5, 1.0)
        swell(snare, 4, 1, 16, 0.05, 0.7, transpose(7))

Source: amen_player.csd

Algorithmic Flair

For the last drum pattern of this tutorial, we’re going to create one that has a bit of an algorithmic flair for randomly generating kicks, snares, and hats. For this pattern, we’re going to use the random function from Python’s random module, which is imported near the top of the score:

from random import random

The new pattern is called drum_pattern_flair() and looks like this:

def drum_pattern_flair(r=0):
    drum_pattern()

    times = [0.5, 0.75, 1.5, 2, 2.75, 3.5, 3.75]
    instrs = [kick, snare, hat, hat, hat]

    for time in times:
        if random() < r:
            with cue(time):
                instr = choice(instrs)
                instr(amp=random() * 0.75 + 0.125)

The pattern excepts an optional argument r that determines the odds of an event being randomly generated. If r is 0, then there is no chance that extra notes will be played. When r is set to 1, it’ll generate an event every chance it gets.

The pattern uses the original drum_pattern() created earlier, and then continues with some programming logic to add a bit of algorithmic flair to it. A list is created with eight different start times (in beats). A second list containing a list of score instruments is created; Only one kick and snare are added, but includes hat three times to increase the odds that this will be chosen.

Using the for loop, the program iterates through each value in list times. It does a comparison to a randomly generated number for each. If the random number is less than r, then a score instrument is generated for the current value of time. The cue() is set to the current value of variable time. The choice() functions then chooses either kick, snare, hat, hat, or hat. Then the event is created, with a random amplitude in the range of 0.125 and 0.875.

The end result is the original drum pattern plus some extra random notes, maybe.

for m in range(1, 17, 4):
    with measure(m):
        with measure(1): drum_pattern()
        with measure(2): drum_pattern_flair(0.25)
        with measure(3): drum_pattern_8th_hats()
        with measure(4): drum_pattern_flair(1)

Source: amen_flair.csd

Form With Functions

In this final example, rather adding a feature we’re going to make a point about readability. Look at this snippet of code from the final score and see if you can figure out the form?

with measure(1): intro()
with measure(5): section_a()
with measure(21): section_b()
with measure(37): section_a()
with measure(53): section_c()
with measure(61): section_a()
with measure(77): section_b()
with measure(93): outro()

Source: amen_form.csd

Granted, not all scores are built the same, and Python definitely allows the creation of some horrendous code. Though the code can also be used to bring greater clarity to the score.

Footnotes

[1]Pink Floyd, “Us and Them”, The Dark Side of the Moon, 1973
[2]Willy Wonka and the Chocolate Factory, movie, 1971