By Jacob Joaquin <jacobjoaquin@gmail.com>
Updated April 10th, 2013
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.
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/
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.
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.
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.
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.
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.
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.
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
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
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
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
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.
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.
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
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
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
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
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.
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
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
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
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
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
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 |