Introducing “Python Score” for Csound

PYTHON SCORE is a Python script for Csound score generation and processing. With it you will compose music easily and efficiently. No computer should be without one!

This effort has been weeks in the making, if you disregard the 10+ years I’ve spent creating various Csound score utilities I’ve coded up in Perl, Python, C and Java. Much of the focus on this project is directed at keeping the score familiar to Csounders, integrating Python with the Csound score using the CsScore bin feature, and removing as much unnecessary clutter. For example, you won’t need to open and close any files in your programs, Python Score handles it for you. And I’m attempting to keep the point of entry easy as possible for all; You’ll only need to know tiny bit of Python to get started.

Python Score is currently part of the Csound CSD Python library, though it will receive its own repository in the future. Here are the highlights. More information will follow.

The score() function

The first priority of Python Score is to allow traditional Csound score composers to transition into this new score environment in a near seamless fashion. Csounders will be able to use the full breadth of their knowledge of the traditional score they’ve acquired over the years. With the score() function, Csounders accomplish just that. The following example (test.csd) is a complete CsScore section in a Csound csd file:

<CsScore bin="python pysco.py">

score('''
f 1 0 8192 10 1
t 0 189

i 1 0 0.5 0.707 9.02
i 1 + .   .     8.07
i 1 + .   .     8.09
i 1 + .   .     8.11
i 1 + .   .     9.00
i 1 + .   .     8.09
i 1 + .   .     8.11
i 1 + .   .     8.07
''')

</CsScore>

A Csounder has successfully transitioned into this new environment once they’ve learned how to setup the call in the CsScore tag using the bin feature and embed their traditional Csound score code in a score() function. Once this has taken place, the composer is now only a step away from a plethora of new features to include in their arsenal of computer music tools.

The “cue” Object

Where as the score() function makes it easy for Csounders to make the leap into Python Score, the “cue” object is what makes it worthwhile. That and all the goodness Python brings to the mix.

The “cue” object enables composers to utilize nested programming techniques for placing events in time, similar to for-loops and if-then conditional branching. This allows composers to do things such as think-in-time localized to a measure rather than the absolute time of the global score.

The “cue” object works by translating the start values in pfield 2. Consider the following line of code:

score('i 1 0 4 0.707 8.00')

Looking at p-field 2, the event takes place at beat 0. By utilizing the “cue” object using the Python “with” statement, we can move the start time of the event without ever touching p-field 2. The follow block of code plays the same event at beat 64 in the score.

with cue(64):
    score('i 1 0 4 0.707 8.00')

The “cue” is a bit like a flux capacitor, as it makes time travel possible. Or at minimum, it saves a composer time, and lots of it, since they can easily move small and/or large sections of score, in time, without changing each and every value in the p-field 2 column. Notes, licks, phrases, bars, sections, entire compositions, etc… All of these time-based musical concepts benefit from the organizational power of the “cue”.

The following example from test10.csd shows the first three measures of Bach’s Invention 1. The beginning of each measure is designated by the use of a “with cue(t)” statement. Since the native time of the Csound score is in beats, and the fact that the piece is in 4/4, the values used with the “cue” object are multiples of 4.

with cue(0):
    score('''
    i 1 0.5 0.5 0.5 8.00
    i 1 +   .   .   8.02
    i 1 +   .   .   8.04
    i 1 +   .   .   8.05
    i 1 +   .   .   8.02
    i 1 +   .   .   8.04
    i 1 +   .   .   8.00
    ''')

with cue(4):
    score('''
    i 1 0 1    0.5 8.07
    i 1 + .    .   9.00
    i 1 + 0.25 .   8.11
    i 1 + 0.25 .   8.09
    i 1 + 0.5  .   8.11
    i 1 + .    .   9.00

    i 1 0.5 0.5 0.5 7.00
    i 1 +   .   .   7.02
    i 1 +   .   .   7.04
    i 1 +   .   .   7.05
    i 1 +   .   .   7.02
    i 1 +   .   .   7.04
    i 1 +   .   .   7.00
    ''')

with cue(8):
    score('''
    i 1 0 0.5 0.5 9.02
    i 1 + .   .   8.07
    i 1 + .   .   8.09
    i 1 + .   .   8.11
    i 1 + .   .   9.00
    i 1 + .   .   8.09
    i 1 + .   .   8.11
    i 1 + .   .   8.07

    i 1 0 1 0.5 7.07
    i 1 + . 0.5 6.07
    ''')

The “cue” also acts as a stack, meaning you can nest multiple “with cue(t)” statements. The following score event happens at 21.05. That is 16 + 4 + 1 + 0.05.

with cue(16):
    with cue(4):
        with cue(1):
            with cue(0.05):
                score('i 1 0 1 0.707 8.00')

P-field Converters

Csound is loaded with value converters. Though all of these exist in the orchestra side of Csound and there is currently no Csound mechanism to apply value converters to the score. Unless you count macros, but these are limiting. Two functions have been created to allow composers to apply pfield converters, either from an existing library or to create their own. These functions are p_callback() and pmap().

The p_callback() function registers a user-selected function that is applied to a selected pfield for a selected instrument. This registered function is applied when the score() function is called.

The pmap() function works similarly, except that it applies a user-selected function to everything already written to the score. Think of it is a post-score processor, while p_callback() is a pre-score processor.

The example (test2.csd) demonstrates two functions: conv_to_hz() which translates conventional notation into hz, and dB() which translates decibel values into standard amplitude values. Both of these are located in convert.py.

<CsScore bin="./pysco.py">

p_callback('i', 1, 5, conv_to_hz)

score('''
f 1 0 8192 10 1
t 0 120

i 1 0 0.5 -3 D5
i 1 + .   .  G4
i 1 + .   .  A4
i 1 + .   .  B4
i 1 + .   .  C5
i 1 + .   .  A4
i 1 + .   .  B4
i 1 + .   .  G5
''')

pmap('i', 1, 4, dB)

</CsScore>

What else?

The functions presented so far are just the basic mechanisms included in Python Score to help solve specific score related issues. Beyond these there is Python. Having a true mature scripting language at your disposal opens up score creation an processing in ways that Csound alone could never do on it’s own. What Python offers will be the topic of many follow up posts and examples to come.

Score Events

No computer music system is complete without the ability to place notes into a score/timeline. Read and download today’s script here.

The mechanisms that schedule score events and print the results are still a bit wonky. So I’m going to omit the explanation for now, and instead just focus on how they’re used. The following is the __main__ from the script:

if __name__ == "__main__":
    @Instr
    def RingTine(dur, amp, freq_1, freq_2):
        return Sine(amp, freq_1) * Sine(amp, freq_2) * RiseFall(dur, 0)

    s = ScoreEvents()
    s.event(0, 0.25, RingTine(0.25, 1, 440, 44))
    s.event(0.5, 0.125, RingTine(0.125, 0.333, 262, 44))
    s.event(0.75, 0.25, RingTine(0.25, 0.25, 262, 44))
    s.event(0.75, 0.25, RingTine(0.25, 0.5, 440, 44))
    for frame in PrintSamples(s): pass

The first part of __main__ defines an instrument called RingTine that generates a percussive ring-modulated timbre. At least in theory; Still no sound output.

The next step creates a ScoreEvents() object. This is where events are scheduled. This is followed by 4 lines of code that schedule events for @Instr RingTine with the ScoreEvents class method event(). The method takes three arguments:

ScoreEvents.event(start_time, duration, UnitGenerator(*args))

The very last line prints the samples generates by the score with the PrintSamples() iterator. The implementation is seriously lame at the moment, but is perfectly acceptable for a crude prototype.

Operators Made Easy

There was something that seriously annoyed me about yesterday’s instrument graph. The use of the Multiply() and the Sum() is bothersome; I’m used to expressing this functionality in a more concise manner using the * and + operators. Download today’s code here.

This doesn’t work for me:

return Multiply(Sum(a1, a2), RiseFall(dur, 0.5))

Thankfully, Python allows us to overload the operators, so we can express the same line as this:

return (a1 + a2) * RiseFall(dur, 0.5)

Less typing, easier to read. Let’s see it in the context of @Instr MyInstr:

@Instr
def MyInstr(dur=1.0, amp=1.0, freq=440, tune=12):
    a1 = Sine(amp * 0.5, freq)
    a2 = Sine(amp * 0.5, freq * 2 ** (tune / 12.0))
    return (a1 + a2) * RiseFall(dur, 0.5)

Here’s how I implemented it. I started by creating a generic iterator class called UnitGenerator:

class UnitGenerator:
    def __init__(self): pass
    def __iter__(self): pass                 
    def next(self): raise StopIteration    
    def __add__(self, i): return Add(self, i)
    def __mul__(self, i): return Mul(self, i)

The last two lines of the class redefine __add__() and __mul__(), which control the behaviors of + and *. These functions use the custom classes Add() and Mul(). These were originally called Sum() and Multiply(), though I renamed them to follow Python naming conventions. The last thing I had to do was alter some of the existing classes to derive from class UnitGenerator, so they automatically incorporate the overloaded operators.

class Instr(UnitGenerator):  ...
class IterReduce(UnitGenerator):  ...
class Mul(IterReduce):  ...
class Add(IterReduce):  ...
class RiseFall(UnitGenerator):  ...
class Sine(UnitGenerator):  ...

Classes Mul and Add are also of type UnitGenerator. They inherit for class IterReduce which inherents from UnitGenerator.

Instruments Simplified with Python Decorators

Python can be molded to look like an audio synthesis language with the help of decorators. In my last post, I showed an instrument graph built within a Python iterator class. However, Iterator classes are too clunky looking, and require more work than should be necessary for creating a simple graph. Read and download this blog’s example script here.

So how does it look in comparison?

@Instr
def MyInstr(dur=1.0, amp=1.0, freq=440, tune=12):
    a1 = Sine(amp * 0.5, freq)
    a2 = Sine(amp * 0.5, freq * 2 ** (tune / 12.0))
    return Multiply(Sum(a1, a2), RiseFall(dur, 0.5))

The @Instr decorator signals to both the human and the computer that the proceeding definition is an instrument class. Much easier to deal with than having to fiddle around with class methods __init__(), __iter__() and next(). There’s less code to write, and far less chance to break the internals of an iterator class. And it more closely resembles an audio synthesis language. Here’s a near equivalent version written in Csound:

instr MyInstr
    idur = p3
    iamp = p4
    ifreq = p5
    itune = p6
    
    a1 oscils iamp * 0.5, ifreq, 0
    a2 oscils iamp * 0.5, ifreq * 2 ^ (itune / 12), 0
    kenv linseg 0, idur * 0.5, 1, idur * 0.5, 0
    out kenv * (a1 + a2)
endin

What’s going on exactly? Well, to be honest, decorators have been eluding me for the past two weeks. It wasn’t until last night as I was halfway through watching Eddie Izzard’s Dress to Kill that it suddenly clicked. So any explanation I give will be insufficient. I’ll just say that @Instr refers to a generic graph iterator class designed by me that creates a new iterator class that assimilates def MyInstr. If you want to know more, you can read Bruce Eckel’s Introduction to Python Decorators.

Question: Cake or death?

Python Iterator Class as Instrument Graph

I built an iterator class from the graph in yesterday’s example. Read and download the full script here. Let’s take a look at the class, then discuss the implications:

class MyInstr:
    '''My Instrument'''

    def __init__(self, dur, amp=1.0, freq=440, tune=12):
        a1 = Sine(amp * 0.5, freq)
        a2 = Sine(amp * 0.5, freq * 2 ** (tune / 12.0))
        self.out = Multiply(Sum(a1, a2), RiseFall(dur, 0.5))

    def __iter__(self):
        self.index = 0
        self._iter = (i for i in self.out)
        return self
        
    def next(self): 
        if self.index >= ksmps: raise StopIteration 
        self.index += 1
        return self._iter.next()

What good is a graph defined as an iterator class? The class essentially works like a Csound instrument. Multiple instances with varied input can be created, like Csound score i-events. It is also, more or less, a new unit generator that can be patched into a new graph. Most importantly, they can be easily shared with others in the Slipmat community. Import. Reuse. Remix.

There is a downside. If you’re not familiar with Python code or Python iterator classes, then you probably can’t make heads or tails out of this. One of the fundamental principles of the Slipmat philosophy is that the code needs to be user-friendly. Iterator classes as graphs, well, that’s a big technical hurdle, especially for n00bs. I don’t like hurdles; they restrict the creative flow, and can be a show stopper.

Fortunately, there is a solution, which I’ll post later today. If you happen to follow my twitter feed, then you probably have figured out where I’m going.

Here’s the the new __main__:

if __name__ == "__main__":
    t = 0.002
    my_instr = MyInstr(t, 1, 440, 7)
    
    for frame in Run(t):
        print '%d:' % frame
        for i in my_instr:
            print 't%.8f' % i

This produces identical results to yesterday’s script. The only difference is that the code has been refactored to increase modularity and possibly clarity.

Control-Rate Envelope Generator

I wanted to generate a control-rate signal with my Python-Slipmat prototype. To my surprise, doing so was fairly straight forward; Python iterator classes have a control-rate mechanism built right in.

Read and download the full script at snipt.

I designed a simple envelope that generates a rise-fall shape. By default, the rise and fall times are identical, though users can specify a peak value relative to the duration. Here’s the code:

class RiseFall:
    '''A rise-fall envelope generator.'''
    
    def __init__(self, dur, peak=0.5):
        self.frames = int(dur * sr / float(ksmps))
        self.rise = int(peak * self.frames)
        self.fall = int(self.frames - self.rise)
        self.inc = 0
        self.v = 0
        
    def __iter__(self):
        self.index = 0
        
        if self.inc <= self.rise and self.rise != 0:
            self.v = self.inc / float(self.rise)
        else:
            self.v = (self.fall - (self.inc - self.rise)) / float(self.fall)
            
        self.inc += 1
        return self
    
    def next(self):
        if self.index >= ksmps:
            raise StopIteration

        self.index += 1          
        return self.v

To make sense of this, I’m going to compare this Python iterator class to Csound. More or less, Csound works at three rates: init (i), control (k) and audio (a). All three of these are represented in class RiseFall. The i-rate of RiseFall is __init__(), the k-rate is __iter__() and the a-rate is next().

What makes RiseFall a k-rate unit generator is that the code that calculates the current value of the envelope resides in __iter__(), which gets executed at the beginning of each new frame of audio. If you look at class Sine, you’ll see that the code responsible for generating the sine wave is in the class method next().

And here is RiseFall added to our graph:

if __name__ == "__main__":
    t = 0.002
    a1 = Sine(0.5, 440)
    a2 = Sine(0.5, 440 * 2 ** (7 / 12.0))
    amix = Sum(a1, a2)
    aenv = RiseFall(t, 0.5)
    aout = Multiply(amix, aenv)

    for frame in Run(t):
        print '%d:' % frame
        for i in aout:
            print 't%.8f' % i

I also refactored class Mixer; It is now known as Sum, which can now sum two or more signals. Additionally, I added the class Multiply, which takes after Sum.

Rendering 1 Second of Audio Data

Yesterday’s python script rendered 1 frame of a rudimentary additive synthesizer. Today’s script renders 1 second. You can download the complete example at textsnip or at snipt.

To render multiple frames of audio, I added two classes: Mixer and Run.

class Mixer:
    '''A simple mixer.'''
    
    def __init__(self, source1, source2):
        self.s1 = source1
        self.s2 = source2
    
    def __iter__(self):
        self.index = 0
        self.iter_1 = (i for i in self.s1)
        self.iter_2 = (i for i in self.s2)
        return self
    
    def next(self): 
        if self.index >= ksmps:
            raise StopIteration

        self.index += 1
        return self.iter_1.next() + self.iter_2.next()
    
class Run:
    '''Render frames over time.'''
    
    def __init__(self, dur=1.0):
        self.dur = dur  
    
    def __iter__(self):
        self.index = 0
        return self
    
    def next(self): 
        if self.index >= (self.dur * sr) / ksmps:
            raise StopIteration

        self.index += 1
        return self.index

Mixer is an iterator class that takes two iterator objects as its input. The values yielded by the inputs are summed together. Yesterday’s method was only good for one frame; A Mixer object does not have this restriction.

The Run iterator class is designed to loop through the iterator/audio graph over a user-defined duration of time, creating multiple frames of audio data.

The follow snippet of code resembles yesterday’s example as it creates a graph of two sine waves (a1 & a2) being fed into a mixer (amix.) The Run object is given a duration of 1 second, which produces 4410 frames (sr / ksmps) and 44100 samples (10 samples per frame.)

if __name__ == "__main__":
    a1 = Sine(0.5, 440)
    a2 = Sine(0.5, 440 * 2 ** (7 / 12.0))
    amix = Mixer(a1, a2)
    
    for frame in Run(1.0):
        print frame, ':'
        for sample in amix:
            print 't', sample

Still no output to an audio file. What it does output is a printed list of frames and samples. Here’s frame 4276:

4276 :
    0.128148365438
    0.138541849949
    0.14709747835
    0.153592896452
    0.157826911985
    0.15962183375
    0.158825589584
    0.155313602303
    0.148990404968
    0.139790979215

BTW, I’m trying a new service, textsnip.com, for storing and sharing my scripts online. If you have a better recommendation, let me know. Update: textsnip seems to add a couple of gremlins to the autodocs, so I’m trying out snipt.org as well.

Python Iterator as a Sine Oscillator

I wrote a sine oscillator as a Python iterator class. Not the fastest digital oscillator in the world. Though for now, doing prototype work in pure Python will do just fine, even if it means slow render times. In the long run, Slipmat will require a powerhouse of an engine for real-time audio synthesis and DSP, probably written in C. All in good time.

Here’s the script:

#!/usr/bin/env python
import math
import itertools

class Sine:
    '''A sine wave oscillator.'''
    
    sr = 44100
    ksmps = 10    
    
    def __init__(self, amp=1.0, freq=440, phase=0.0):
        self.amp = amp
        self.freq = float(freq)
        self.phase = phase
        
    def __iter__(self):
        self.index = 0
        return self
    
    def next(self):
        if self.index >= self.ksmps:
            raise StopIteration

        self.index += 1
        v = math.sin(self.phase * 2 * math.pi)
        self.phase += self.freq / self.sr
        return v * self.amp

if __name__ == "__main__":
    a1 = Sine(1, 4410, 0.25)
    a2 = Sine(0.5, 8820)
    amix = (i + j for i, j in itertools.izip(a1, a2))
    
    for i in amix:
        print i

Currently, it produces the same sound as a tree falling in the woods with no one around to hear it; It doesn’t write to a DAC or sound file. At least we can still view the results:

1.0
1.28454525252
0.602909620521
-0.602909620521
-1.28454525252
-1.0
-0.333488736227
-0.0151243682287
0.0151243682287
0.333488736227

Near the bottom of the code is a very simple graph, where two sine wave generators (a1 & a2) are patched into a mixer generator (amix), creating a very rudimentary additive synthesizer. The signals aren’t generated until the for-block at the very end. The script only prints one control-block’s worth of data, 10 samples, which coincides with the value of ksmps found in class Sine. The sample rate and control rate is built right into class Sine, and needs to be moved out.

Realm of the Practical

Brain storming is easy; I can make things up without being held accountable. However, I need to spend time in the realm of the practical. I won’t stop collecting ideas, but if Slipmat is going to be a reality, I need to know what the issues are. This means lots of research and lots of prototyping and lots of little scripts that test various facets of Python.

For example, creating a graph of Python 3 generators:

#!/usr/bin/env python3

import operator

ksmp = 8
foo = (i * 2 for i in range(ksmp))
foo = (i + 1 for i in foo)
bar = (11 for i in range(ksmp))
foo = map(operator.mul, foo, bar)

print(*foo)

This is interesting to me because the names foo and bar do not receive an array/collect/list of numbers. Instead, they point to generator objects. These generators are not evaluated until print(*foo) is called.

By the way, if my understand or terminology is ever off, please correct me. I’m also here to learn.

x Trigger Notation

In Roll You Own Syntax, I theorized how users could construct their own systems of notation using strings. I’ve constructed a working function, called trig(), to show how it’s done.

First, let’s see trig() in action. The following is complete conceptual prototype Slipmat program that generates a rock drum groove with 8th note hats:

#!/usr/bin/env slipmat

from JakeLib.Generators import trig
from EasyKit import hat, snare, kick
    
@trig('x.x. x.x. x.x. x.x.') hat()
@trig('.... x... .... x...') snare()
@trig('x... .... x... ....') kick()

This horizontal system for notating triggers, and others like it, can greatly improve the legibility of a piece, while catering to a composer’s preferred style of working. A composer quickly scans this and comprehends it without having to reconstruct it in their head from a list of individual events:

@0    hat()
@0    kick()
@0.5  hat()
@1    hat()
@1    snare()
@1.5  hat()
@2    hat()
@2    kick()
@2.5  hat()
@3    hat()
@3    snare()
@3.5  hat()

The form is lost in translation.

Building the function is pretty straight forward if you’re somewhat experienced with Python. I put together the following function definition, with docstrings, in roughly 15 minutes:

def trig(seq, res=0.25):
    '''
    Creates a numeric sequence from a string and returns a list.

    Description:
    A string trigger sequencer, where an 'x' creates a trigger, and a
    '.' creates a rest. All other glyphs are ignored. The resolution
    of triggers and rests are determined by the argument res.

    Input:
    seq -- A string containing a sequence of 'x' triggers and '.' rests
    res -- Resolution of note triggers and rests
        
    Output:
    return -- A numeric list
    '''    
    
    L = []  # The return list
    p = 0   # Position in sequence

    for c in seq:
        if c == 'x':
            L.append(p * res)
            p += 1            
        elif c == '.':
            p += 1

    return L

The trig() function accepts a string formatted in what I call ‘x trigger notation.’ The function parses the string and auto-generates a list of numbers representing trigger times. A trigger is denoted by an ‘x’, while a rest is a ‘.’. All other glyphs are ignored. I use a single space between beats for clarity. The default resolution is a 16th note, though trig() accepts an optional argument for changing the resolution, increasing its usefulness.

Import, Reuse, Remix

The best part about custom functions is that they can be reused multiple times in multiple programs by multiple people with the use of the import. No refactoring of code, no copy and paste, no reinventing of the wheel. Just import, reuse, remix.