BRAID

Braid is a single-import module for Python 3 that comprises a sequencer and musical notation system for monophonic MIDI synths. Its emphasis is on interpolation, polyrhythms, phasing, and entrainment.

Braid can be downloaded from its GitHub repository.

It is developed by Brian House.

Contents

  1. Goals
  2. Installation
  3. Tutorial
    1. Prerequisites
    2. Hello World
    3. Notes and Thread.chord
    4. Thread.pattern, part 1
    5. Thread.pattern, part 2
    6. Thread.pattern, part 3
    7. Thread.velocity and Thread.grace
    8. Thread.phase
    9. Thread.rate
    10. Tweening
    11. Signals
    12. Tweening rate and sync
    13. Triggers
    14. MIDI devices and properties
    15. Adding properties
    16. Customizing MIDI behavior
  4. Reference
    1. Glossary
    2. Global functions
    3. Symbols
    4. Scales
    5. Signals

Goals

A note on names

This framework is called Braid, and the fundamental objects are called threads—a thread corresponds to a hardware or software monosynth, and refers to the temporal operations of Braid through which threads can come in and out of sync. This is not a thread in the programming sense (in that respect Braid is single-threaded).

Installation

You'll need to have Python 3 installed—if you're using macOS, I recommend doing so with Homebrew. Tested with python 3.7.

To install (or update) Braid via the terminal:
pip3 install git+git://github.com/brianhouse/braid --upgrade

Tutorial

Prerequisites

Braid is, fundamentally, an extension to Python 3. This documentation won't cover python usage and syntax—for that, look here. Most of the power of Braid comes from the fact that it can be interleaved with other python code. Such possibilities are left to the practitioner, or at least out of this documentation.

Additionally, this documentation assumes a general knowledge of MIDI.

If you don't know what you're doing with python and the terminal but you've gotten this far, follow the instructions for saving a Braid "Hello World" script below. Then in the Finder, right-click that python file, and open it using the latest version of IDLE, which should appear as one of your choices. You can then use IDLE's built-in text editor to write, save, and run ("Run->Run Module") Braid scripts.

Hello World

Any MIDI software or hardware device you have running should more or less work with Braid to make sounds. If you are on macOS, to simplify things download and run this simple MIDI bridge app which will let you use General MIDI for the purposes of this documentation (make sure no other MIDI devices are running before launching the app, and launch it before starting Braid).

To begin working with Braid, create a new file in your favorite text editor (such as Sublime Text). Create a thread—the fundamental object of Braid—and start it:

from braid import *

t = Thread(1)               # create a thread with MIDI channel
t.pattern = C, C, C, C      # add a pattern
t.start()                   # start it

play()                      # don't forget this

Save it as hello_world.py, and run it with python3 hello_world.py 0 0. The (optional) arguments designate the MIDI out and in interfaces to use.

$ python3 hello_world.py
Python 3.7.4 (default, Jul  9 2019, 18:13:23)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
MIDI outputs available: ['to general_MIDI_bridge 1', 'to general_MIDI_bridge 2']
MIDI OUT: to general_MIDI_bridge 1
MIDI  IN: from general_MIDI_bridge 1
Loaded synths
Braid started
Playing

That's it! You should be hearing the steady pulse of progress.

Top-level controls

You can start and stop individual threads, with a_thread.start() and a_thread.stop(), which essentially behave like a mute button.

Braid also has some universal playback controls. When Braid launches, it is automatically in play mode. Use pause() to mute everything, and play() to get it going again. If you use stop(), it will stop all threads, so you'll need to start them up again individually. clear() just stops the threads, but Braid itself is still going and if you start a thread it will sound right away.

Advanced note: If you're doing a lot of livecoding, it's easy to create a new thread with the same name as an old one, and this can lead to orphan threads that you hear but can't reference. Use stop() or clear() to silence these.

Try it now:

clear()

Notes and chord

Start a thread

t = Thread(1)   # create a thread on channel 1
t.start()

MIDI pitch value can be specified by MIDI number or with note-name aliases between C0 and B8. C is an alias for C4, likewise for the rest of the middle octave

t.pattern = C, C, C, C

is the same as

t.pattern = 60, 60, 60, 60

0s simply sustain (no MIDI sent)

t.pattern = C, 0, C, C

Rests (explicit MIDI note-offs) are specified with a Z

t.pattern = C, Z, C, Z

By default, there is no specified chord. But if there is one, notes can be specified by scale degree

t.chord = C4, MAJ
t.pattern = 1, 3, 5, 7

Negative numbers designate the octave below

t.pattern = -5, -7, 1, 5

A chord consists of a root note and a scale. For example, C, MAJ is a major scale built off of C4. That means 1, 2, 3, 4, 5 is the equivalent of C4, D4, E4, F4, G4. But behind the scenes, it's specified like this: Scale([0, 2, 4, 5, 7, 9, 11]). Here's the list of built-in scales.

Custom scales can be generated with the following syntax, where numbers are chromatic steps from the root

whole_tone_scale = Scale([0, 2, 4, 6, 8, 10])

R specifies a random note in the scale

t.chord = C4, whole_tone_scale
t.pattern = 1, R, R, -6

Grace notes are specified by using floats

t.pattern = 1, 1., 1., 1.

Use the g function to create a grace note on note specified with a symbol

t.chord = None
t.pattern = C, g(C), g(C), g(C)

Thread.pattern, part 1

Start a thread with a pattern

t = Thread(1)
t.chord = C, DOR
t.pattern = 1, 1, 1, 1
t.start()

Once started, a thread repeats its pattern. Each repetition is called a cycle. Each cycle is subdivided evenly by the steps in the pattern.

t.pattern = 1, 0, 1, 0              # 4/4
t.pattern = 1, 0, 1                 # 3/4
t.pattern = 1, 1, 0, 1, 1, 0, 1     # 7/8

Each step of a pattern can be a note, but it can also be a subdivision

t.pattern = 1, [1, 1], 1, 1
t.pattern = 1, [1, 1], 1, [1, 1, 1]

...or a subdivision of subdivisions, ad finitum

t.pattern = 1, [2, [1., 1.]], [3, [2, 1], 1.], [5, [4., 3.]]

So brackets indicate subdivisions. Parens, however, indicate a choice.

t.pattern = 1, (2, 3, 4), 1, 1

Brackets and parens can be combined to create intricate markov chains

tempo(132)                  # set the universal tempo
d = Thread(10)              # channel 10 is MIDI for drums

d.pattern = [([K, H], [K, K]), (K, O)], (H, [H, K]), (S, [S, (O, K), 0, g(S)]), [[H, H], ([H, H], O, [g(S), g(S), g(S), g(S)])]         # K, S, H, O are built-in aliases for 36, 38, 42, 46
d.start()

Patterns are python lists, so they can be manipulated as such

d.pattern = [K, [O, H]] * 4
d.pattern[2] = S
d.pattern[6] = S
d.pattern[6] = [(S, [S, K])]

d.pattern.reverse()

Thread.pattern, part 2

There are additional functions for working with rhythms. For example, euclidean rhythms can be generated with the euc function

tempo(132)   
d = Thread(10)
d.start()

steps = 7
pulses = 3
note = K
d.pattern = euc(steps, pulses, note)    # [K, 0, K, 0, K, 0, 0]

Adding a pattern to an existing pattern fills any 0s with the new pattern

d.pattern.add(euc(7, 5, H))             # [K, H, K, H, K, 0, H]

XOR'ing a pattern to an existing pattern adds it, but turns any collisions into 0s

d.pattern.xor([1, 1, 0, 0, 0, 0, 0])    # [0, 0, K, H, K, 0, H]

These can be done even if the patterns are different lengths, to create crossrhythms

d.pattern = [K, K] * 2
d.pattern.add([H, H, H, H, H])

Patterns can also be blended

d.pattern = blend([K, K, K, K], [S, S, S, S])   # this is probabilistic and will be different every time!

same as

d.pattern = K, K, K, K
d.pattern.blend([S, S, S, S])

blend can take a balance argument, where 0 is fully pattern A, and 1 is fully pattern B.

d.pattern = blend([K, K, K, K], [S, S, S, S], 0.2)   # more kicks, less snare

Thread.pattern, part 3

Additionally, any given step in a pattern may also be a function. This function should return a note value.

t = Thread(1)
t.chord = D, PRG

def x():
    return choice([1, 3, 5, 7])

t.pattern = [x] * 8

t.start()

This is particularly useful for manipulating synth parameters at each step (see below). In this case, creating a wrapped function allows the actual note value to be passed as a parameter.

t = VolcaKick(1)

def k(n):
    def f(t):
        t.pulse_colour = 127
        t.pulse_level = 127
        t.tone = 40
        t.amp_attack = 0
        t.amp_decay = 80
        t.resonator_pitch = 0
        return n
    return f

def s(n):
    def f(t):    
        t.pulse_colour = 127
        t.pulse_level = 127
        t.tone = 60
        t.amp_attack = 0
        t.amp_decay = 20
        t.resonator_pitch = 34
        return n
    return f

t.pattern = k(1), k(3), s(20), k(1)              # custom properties for k and s notes
t.start()

Thread.velocity and Thread.grace

All threads come with some properties built-in. We've seen chord already.

t = Thread(10)
t.chord = C, MAJ
t.pattern = 1, 1., 1, 1.
t.start()

There is also, of course, velocity

t.velocity = 0.5

and grace is a percentage of velocity, to control the depth of the grace notes

t.velocity = 1.0
t.grace = .45

Thread.phase

Consider the following:

t1 = Thread(10)
t1.chord = 76, CHR  # root note is "Hi Wood Block"

t2 = Thread(10)
t2.chord = 77, CHR  # root note is "Lo Wood Block"

t1.pattern = [1, 1, 1, 0], [1, 1, 0, 1], [0, 1, 1, 0]   # thanks Steve
t2.pattern = t1.pattern

t1.start()
t2.start(t1)            # t1 as argument

Note that in this example, t2 takes t1 as an argument. This ensures that t2 will start in sync with t1. Otherwise, t1 and t2 will start at arbitrary times, which may not be desirable.

However, each thread also has a phase property that allows us to control the relative phase of threads deliberately. Phase goes from 0-1 and indicates how much of the cycle the pattern is offset.

t2.phase = 1/12         # adjust phase by one subdivision
t2.phase = 3/12
t2.phase = 7/12

Thread.rate

As we've already used it, the tempo() function sets the universal BPM (or at least the equivalent BPM if cycles were in 4/4 time). Braid silently keeps track of cycles at this tempo. By default, the cycles of each thread match this reference. We just saw how phase can offset the patterns of a thread—it does this in relation to the reference cycle.

Likewise, individual threads can also cycle at their own rate. The rate property of each thread is a multiplier of the reference cycles—0.5 is twice as slow, 2 is twice as fast.

t1 = Thread(1)
t1.pattern = C, C, C, C
t1.start()

t2 = Thread(2)
t2.pattern = G, G, G, G
t2.start(t1)                    # keep in phase

t2.rate = 1/2                   # half-speed!    

Notice that depending on when you hit return, changing the rate can make threads go out of sync (similar to how starting threads at different times puts them out of phase). The way to get around this is to make sure it changes on a cycle edge. For this, use a trigger:

t2.stop()
t2.start(t1)
def x(): t2.rate = 0.5          # one-line python function
...                                 # hit return twice
t2.trigger(x)                   # executes x at the beginning of the next cycle

If you're working with scripts, using triggers like this isn't necessary, as things will execute simultaneously.

Tweening

Now for the fun part. Any property on a thread can be tweened—that is, interpolated between two values over time (yes, the term is borrowed from Flash).

This is done simply by assigning a tween() function to the property instead of a value. tween() has two required arguments: the target value, and the number of cycles to get there. (A transition function can also be specified, more on that below.) Braid will then automatically tween from the current value to the target value, starting with the next cycle.

p1 = Thread(1)
p2 = Thread(2)

pp = [E4, Gb4, B4, Db5], [D5, Gb4, E4, Db5], [B4, Gb4, D5, Db5]
p1.pattern = p2.pattern = pp

p1.start(); p2.start(p1)            # two commands, one line

p2.phase = tween(1/12, 4.0)         # take four cycles to move one subdivision

All properties on a thread can be tweened. Device specific MIDI parameters move stepwise between ints within the range 0-127 (see below). rate, phase, velocity, grace change continuously over float values. chord will probabilistically waver between the current value and the target value. pattern will perform a blend between the current and target patterns on each cycle, with the balance shifting from one to the other.

t = Thread(10)
t.start()
t.pattern = K, K, S, [0, 0, 0, K]
t.pattern = tween([[K, K], [S, 0, K, 0], [0, K], [S, K, 0, K]], 8)

# or:

t.pattern = euc(8, 5, 43)
t.pattern = tween(euc(8, 6, 50), 8)
t.pattern = tween(euc(8, 5, 43), 8)

When a tween completes, it can trigger a function, using the on_end parameter. The following example tweens the phase of Thread t over 8 cycles, and then stops the thread (Note the lack of parentheses around t.stop—we want the thread to execute the function at the end of the tween, not during this declaration!).

t.phase = tween(0.5, 8, on_end=t.stop)

Signals

Tweens can take an additional property, called a signal. This is any function that takes a float value from 0 to 1 and return another value from 0 to 1—a nonlinear transition function when you don't want to go from A to B in a straight line. (Yes, Flash again).

Built-in signals: linear (default), ease_in, ease_out, ease_in_out, ease_out_in

t = Thread(1)
t.chord = D, DOR
t.pattern = [1, 3, 5, 7] * 4
t.start()

t.chord = tween((E, MAJ), 8, ease_in_out())
t.chord = tween((E, MAJ), 8, ease_out_in())

Since signals are just functions, you can write your own in Python. ease_out, for example, is just

def ease_out(pos):
    pos = clamp(pos)    
    return (pos - 1)**3 + 1

To view a graphic representation of the function, plot it.

plot(ease_out)

You can also convert any timeseries data into a signal function using timeseries(). You might use this to tween velocity over an entire composition, for example, or for data sonification.

data = 0, 0, 1, 0.8, 1, 0.2, 0, 0.4, 0.8, 1     # arbitrary timseries
f = timeseries(data)
plot(f)

t = Thread(1)
t.chord = D, SUSb9
t.pattern = [1, 2, 3, 4] * 4
t.start()

t.velocity = 0.0                                # sets the lower bound of the range to 0.0
t.velocity = tween(1.0, 24, f)                  # sets the uppper bound of the range to 1.0, and applies the signal shape over 24 cycles

Likewise, you can specify a function with breakpoints using breakpoints(). Each breakpoint is specified with an x,y coordinate system—it doesn't matter what the range and domain are, as it will be normalized. Additionally, a signal shape can be specified for each breakpoint transition, which allows complex curves and transitions.

f = breakpoints(
                [0, 0],
                [2, 0],
                [8, 1, ease_in_out()],
                [13, 0, ease_in_out()],
                [20, 3, ease_in_out()],
                [24, 0, ease_out()],
                [27, 1, ease_in_out()],
                [28.5, 0, ease_out()],
                [37.5, 4, ease_in_out()],
                [48, 0, ease_out()]
                )
f = breakpoints(data)
plot(f)

Sync, and tweening rate

Braid does something special when you assign a tween to Thread.rate. Ordinarily, if two threads started in sync and one thread tweened its rate, they would inevitably end up out of sync. However, Braid automatically adjusts its tweening function such that threads will remain aligned as best as possible.

t1 = Thread(1)
t1.chord = D, SUSb9
t1.pattern = 1, 1, 1, 1
t1.start()

t2 = Thread(2)
t2.chord = D, SUSb9
t2.pattern = 4, 4, 4, 4
t2.start(t1)

t2.rate = tween(0.5, 4)

As simple as that is, that's probably the most interesting feature of Braid to me, and what give it its name.

If you don't want this functionality, pass sync=False to the thread constructor, and the thread won't try to reconcile itself.

t = Thread(1, sync=False)

Triggers

You can sequence in Braid using triggers. A trigger consists of a function, the number of complete cycles to wait before executing it, and whether or not (and how many times) to repeat. Triggers can be added to individual threads (Thread.trigger()), which then reference the thread's cycle, or they can use the universal trigger() function, which reference the universal (silent) cycles (as we've seen with Thread.rate and Thread.phase, these can be different).

Triggers execute at the edge between cycles.

Thread Triggers

t = Thread(1)
t.chord = D, SUSb9
t.pattern = 1, 1, 1, 1
t.start()

def x(): t.pattern = 4, 4, 4, 4         # one-line python function
...
t.trigger(x)                            # triggers x at the end of the current cycle
t.trigger(x, 1)                         # triggers x at the end of the first complete cycle
t.trigger(x, 4)                         # triggers x at the end of the fourth complete cycle

You might want to reuse the same triggered function with different threads. This is facilitated by including an argument in the function definition which will be passed the thread that triggered it.

t1 = Thread(1)
t1.chord = D, SUSb9
t1.pattern = 1, 1, 1, 1
t1.start()

t2 = Thread(2)
t2.chord = D, SUSb9
t2.pattern = 4, 4, 4, 4
t2.start(t1)

def x(t): t.pattern = R, R, R, R    # generic 't' argument
...
t1.trigger(x, 2)
t2.trigger(x, 2)                    # same function

Using a third argument, triggers can be repeated infinitely or for a set number of times.

t.trigger(x, 4, 2)      # trigger x after 4 cycles and after 8 cycles
t.trigger(y, 6, True)   # trigger x every 6 cycles

Also:

t.trigger(y, 0, True)   # nope

t.trigger(y)            # you probably wanted to do this
t.trigger(y, 1, True)   

To cancel all triggers on a thread, pass False.

t.trigger(False)

To cancel any triggers on a thread that are repeating infinitely, pass repeat=False without other arguments.

t.trigger(repeat=False)

Universal Triggers

For universal triggers, no thread argument can be supplied to the trigger function. And universal triggers operate via the underlying cycle at the global tempo. Otherwise, they are the same as thread triggers, and are particularly useful for sets of changes, as defined in larger functions, or universal functions.

t1 = Thread(1)
t1.chord = D, SUSb9
t1.pattern = 1, 1, 1, 1
t1.start()

t2 = Thread(2)
t2.chord = D, SUSb9
t2.pattern = 4, 4, 4, 4
t2.start(t1)

def x():
...     tempo(tempo() * 1.3)                    # calling tempo without arguments returns the current value
...     t1.chord = D2, SUSb9
...     t1.phase = 1/8
...     t1.pattern = t2.pattern = R, R, R, R
trigger(x, 2)

MIDI devices and properties

Braid is designed to work with hardware monosynths. Thus far, the only actual MIDI output we've been sending are MIDI notes. But CC values from devices are mapped directly to thread properties.

Devices are represented in Braid as extensions to the thread object. To create a custom thread, use the make() function. make() is passed a dictionary with property names mapped to MIDI CC channels.

Voltron = make({'attack': 54, 'decay': 53, 'filter_cutoff': 52, 'pulse_width': 51})

Now, Voltron can be used like any thread, but it will also have the specified CC values that can be set, tweened, etc.

t = Voltron(1)
t.pattern = [1, 2, 3, 5] * 4
t.filter_cutoff = 0
t.filter_cutoff = tween(127, 8)
t.start()

Since you'll probably be using the same MIDI devices all the time, and it is tedious to specify this each time you run Braid (especially with large numbers of controls), Braid also automatically loads custom thread types from the synths.yaml file in the root directory.

Note: A second dictionary can be passed to make() as an additional parameter with property names mapped to default MIDI values.

Customizing MIDI behavior

See custom.py in the examples.

Adding properties

In some cases, you may want to use a reference property that does not directly affect a thread itself or send any MIDI data—a thread-specific variable that can be tweened as though it were a property, in order to guide other processes.

t = Thread(1)
t.add('ref')
t.ref = 0
t.ref = tween(1.0, 8)

Reference

Glossary

Global functions

Symbols

Scales

CHR Chromatic, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
MAJ Major, 0, 2, 4, 5, 7, 9, 11
DOM Dominant, 0, 2, 4, 5, 7, 9, 10
MIN Harmonic minor, 0, 2, 3, 5, 7, 8, 11
MMI Major-Minor, 0, 2, 4, 5, 6, 9, 11
PEN Pentatonic, 0, 2, 5, 7, 10
SUSb9 Suspended flat 9, 0, 1, 3, 5, 7, 8, 10
ION Ionian, 0, 2, 4, 5, 7, 9, 11
DOR Dorian, 0, 2, 3, 5, 7, 9, 10
PRG Phrygian, 0, 1, 3, 5, 7, 8, 10
MYX Myxolydian, 0, 2, 4, 5, 7, 9, 10
AOL Aolian, 0, 2, 3, 5, 7, 8, 10
LOC Locrian, 0, 1, 3, 5, 6, 8, 10
BLU Blues, 0, 3, 5, 6, 7, 10
SDR Gamelan Slendro, 0, 2, 5, 7, 9
PLG Gamelan Pelog, 0, 1, 3, 6, 7, 8, 10
JAM jamz, 0, 2, 3, 5, 6, 7, 10, 11
DRM stepwise drums, 0, 2, 7, 14, 6, 10, 3, 39, 31, 13

Signals

linear (default)
ease_in
ease_out
ease_in_out
ease_out_in