Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

For the best experience please use the latest Chrome, Safari or Firefox browser.

Making Music with Python and BeatLounge

April 12, 2012 @ PyATL
Drew Smathers
IRC/twitter/github/launchpad: djfroofy
Computer Music
What this talk is mostly about ...

...with Python of course

Computer Music Languages and Live Coding Software
Making a Sound with Python Recv/Send Music Data with Python
MIDI Overview

Focus: Communicating music events including pitch, velocity and control signals

Around since the 80s ... (note range: [0,127])

0=C, 1=C#, 2=D, ..., 11=B

Use NOTE % 12 to compute octave

NOTEON [0,127] [0,127]
NOTEOFF [0,127]
A Note on Notes

MIDI can be used arbitrarily but more often than not application is for 12TET music. Begin with C=0. A half step is 1.

C, Cs, D, Eb, E, F, Fs, G, Gs, A, As, B = range(12)
chromatic_scale = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
major_scale = [C, D, E, F, G, A, B]
minor_scale = aeolian =  [C, D, Eb, F, G, Gs, As]
# raise an octave
notes = [note + 12 for note in major_scale]
A Note on Notes (continued)

A chord is typically a triads (3 notes). These are fun to play with. You can sound them at the same time or play in sequence (arpeggio).

from itertools import combinations
C, Cs, D, Eb, E, F, Fs, G, Gs, A, As, B = range(12)
major_scale = [C, D, E, F, G, A, B]
all_triads = combinations(major_scale, 3)

Provides a MIDI interfaces for playing sound fonts

import time
from fluidsynth import Synth
fs = Synth()
sfid = fs.sfload("example.sf2")
track = 0
fs.program_select(track, sfid, 0, 0)
# Play some notes
fs.noteon(track, 60, 127)
fs.noteoff(track, 60)
fs.noteon(track, 64, 127)
BeatLounge (bl.instrument.fsynth)

Wraps pyfluidsynth. Some C-ish APIs hidden: sfload(), program_select(), track etc

from bl.instrument.fsynth import Instrument
instr = Instrument('example.sf2', connection='mono')
instr.playnote(60, 127)
instr.playnote(64, 127)
BeatLounge (bl.comps.midiexample)
from bl.midi import MonitorHandler, MidiDispatcher
from bl.midi import getInput, getOutput, init
midi_in = getInput('LPK25')
midi_out = getOutput('LPK25')
handler = MonitorHandler({1:instr})
disp = MidiDispatcher(midi_in, [handler])
sender = ClockSender(midi_out)
"Strongly Timed"

Terminology originated with ChucK. Strongly timed computer music systems provide a "consistent, sample-synchronous view of time" ... "embeds timing control directly in the code"


"Strongly Timed" Pop Quiz

Question: Is the following system strongly-timed?

import time
tick = 60. / 130. / 24.
def my_music_player(instruments, schedules):
    while True:
        for (instr, schedule) in zip(instruments, schedules):
            (t, command, args) = schedule[0]
            if time.time() == t:
                if command == 'noteon':
                elif command == 'noteoff':
"Strongly Timed" Pop Quiz (continued)

Answer: NO ... So many problems

# tick + <op time> + <preemption time>
while True:
    # <preemption time>
    for (instr, schedule) in zip(instruments, schedules):
        # <op time> + <preemption time>
        (t, command, args) = schedule[0]
        # Equality may never pass - we might end up "ahead"
        # of t forever. So ... this is not sample-synchronous.
        if time.time() == t:
            # ...
Is BeatLounge Strongly Timed?

Answer: ALMOST ... So many problems

Systems like ChucK are strongly timed and support for very high sample rates. BeatLounge (or CPython, really) is "good enough" for MIDI sequencing but not fast enough for DSP. <Insert Actual Metrics>

BeatLounge provides sample synchronous view of time: clock.ticks vs time.time()

bl.scheduler.BeatClock - Isochronous scheduler
# a
from itertools import cycle
import random
notes, octaves = [60, 64, 67], [-12, 0, 12]
velocity = cycle([127,80,120,90])
def play_instr():
    note = random.choice(notes) + random.choice(octaves)
    clock.callLater(18, instr.stopnote, note)
event = clock.schedule(play_instr)
# Start on the next measure, repeat call every 1/8 note
event.startAfter((1,1), (1,8))
BeatLounge "Live Coding": bl.console
$ python bl/
>>> from tutor import song1
>>> song1.event.stopAfter((4,1))

Hint: Loading soundfonts is very expensive. So ... load all resources and initialize before starting (not during) performance.

bl.console (continued)
$ python bl/ --help
Usage: [options]
-c, --channels=  Number of channels or a label: stereo,
                    mono, quad [default: stereo]
-l, --logfile=   Path to logfile [default: child.log]
-b, --bpm=       The tempo in beats per minute [default: 130]
-t, --tpb=       Ticks per beat [default: 24]
-m, --meter=     The meter (default 4/4) [default: 4/4]
--version    Display Twisted version and exit.
--help       Display this help and exit.
bl.console (continued) Audio device can be specified as well:
$ python bl/ -m 3/4 -b 150 jack

Popular feature on early synthesizers. Take notes held down to form arpeggio: sequencing of notes.

Today: software arpegiators. Play a sequence of notes: up, down, up + down, random, in order, ...

Different algorithms can be layered: play X (up, down, ...) over N octaves

Arpeggiators in BeatLounge (bl.player) API is actually horrible so I won't go into detail. In flux.
Arpeggiators in BeatLounge (bl.player)
from bl.player import Player
notes = cycle([60, 64, 67, 64]).next
velocity = cycle([127,90,120,80]).next
dtt = clock.meter.divisionToTicks
player = Player(instr, notes, velocity,
                stop=12, interval=dtt(1,8))
Arpeggiators in BeatLounge (bl.arp)
Arpeggiators in BeatLounge (bl.arp)
from bl.arp import Adder, OrderedArp,
notes = Adder(OctaveArp(RandomArp([0, 4, 7, 9]))
notes.amount = 36
velocity = OrderedArp([127,90,110,80,120,127,90,100])
dtt = clock.meter.divisionToTicks
player = Player(instr, notes, velocity,
                    stop=12, interval=dtt(1,8))


"... one of the basic patterns used in rudimental drumming. These patterns of drum strokes can be combined in many ways to create music."


Examples: Five Stroke Roll, Six Stroke Roll, Paradiddle, etc.

Components: Strokes ([R,L,R,L,R,R]) + Timing

Rudiments in BeatLounge
from bl.rudiments import FiveStrokeRoll
fsr = FiveStrokeRoll()
drum_player = RudimentSchedulePlayer(drum, fsr, 35, 25)

# RudimentSchedulePlayer first gets the stroke from the rudiment:
list(fsr.strokes(35, 25)) = [35, 35, 25, 25, 35,
                                              25, 25, 35, 35, 25]
# ... then a cycle over the time values
list(fsr.time()) == [0, 6, 12, 18, 24,
                                48, 54, 60, 66, 72]
def change_phrase(arp, phrases):
    for i in range(128):
        phrase = random.choice(phrases[i:])
        for j in range(8):
            phrase2 = []
            for (i1, i2) in zip([phrase[j]] * 4  + [phrase[-j]] * 4, phrase):
            arp.reset(phrase2[:8] + phrase + phrase2[8:] + phrase)
actor = clock.schedule(change_phrase(arp, phrases).next)
actor.startAfter((1,1), (4,1))
Demo Time







Use a spacebar or arrow keys to navigate