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.

The Slipcue

The Slipcue (@) is the object responsible for the fourth dimension of a Slipmat program: time.

The name is derived from the term Slip-cueing, which Wikipedia defines as, “a turntable-based DJ technique that consists of holding a record still while the platter rotates underneath the slipmat and releasing it at the right moment.”

The Slipcue is formally known as the @ scheduler. There are various reasons for the name change. I’ve only shown the Slipcue as a simple mechanism for scheduling tasks, though it controls so much more:

  • Syncing other Slipmat processes, ticks, threads and shreds
  • Starting, Stoping, Pausing, fast forwarding, rewinding
  • The Master Clock (or Mistress Clock)
  • Task Management
  • Units of Time
  • Tempo
  • etc

Plus, “the Slipcue” rolls off the tongue rather well.

List Comprehension Grain Generator

This is a granular synthesizer:

@[random() * 8 for i in range(1000)] foo()

This generates 1000 events over the span of 8 beats, using list comprehension.

The function foo() is intentionally left blank. It may produce random single-cycle waveforms, bits of a sample, or glowing TRON-like particles for your live performance visuals. Whatever.

Custom Sequence List Generators

Python’s built-in range() list generator is full of possibilities when used in conjunction with Slipmat’s @ scheduler. And that’s just one generator out of, dare I say, a million possibilities.

Imagine being able to conveniently import generators from a massive user library of modules. This will be a reality for Slipmat for two reasons:

I built a list generator called range_plus(), which takes Python’s range() function a few steps farther. range() is limited to integers; range_plus() removes this restriction and allows for floats. range() supports a single increment step value; range_plus() additionally allows users to pass a list of increment step values that are chosen at random.

Here’s the working Python defintion for range_plus():

def range_plus(start, stop, random_steps=1):
    '''
    Returns a list of successive incremented values
    chosen randomly from a list.

    Input:
    start -- Starting value
    stop --  End point
    random_steps -- A list of incremental values chosen at random or a
        single numeric value
        
    Output:
    return -- A numeric list
    '''
    
    if stop < start:  # Avoid infinite loop from bad input
        return []

    if not isinstance(random_steps, list):
        random_steps = [random_steps]

    L = []

    while start < stop:
        L.append(start)
        start = start + random.choice(random_steps)

    return L

I made sure to write the docstrings, to make life easier for anyone who may import my code later. Here's a Python interpreter session to test out some numbers:

>>> from MyListGenerators import range_plus
>>> range_plus(0, 4, 0.5)
[0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5]
>>> range_plus(0, 4, [0.25, 0.25, 0.5, 0.75])
[0, 0.5, 1.0, 1.5, 1.75, 2.5, 3.0, 3.25, 3.75]

As applied to music, range_plus() would be a perfect fit for randomly generating rhythmic patterns, for things such as hi-hats.

@0  @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()
@4  @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()
@12 @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()
@16 @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()

Here's is one scenario for what those patterns would look like in trigger notation:

Bar 1:  x.x. x.xx ..x. xx.x
Bar 2:  xx.. x..x ..x. .xx.
Bar 3:  x..x ..x. xx.x .x..
Bar 4:  x.x. .xxx .xxx x..x

The 'x' is a 16th note trigger, the '.' is a 16th note rest, and spaces are used to distinguish between each quarter note.

Lists as Micro-Sequencers

On Friday, I listed nine ways in which python methodologies could be used with the @ scheduler. How would they work in a real-world musical context? Today, I’m showcasing the List as a super convenient micro-sequencer.

When the @ scheduler is given a list of numbers, every value in the list is used to schedule an event; This saves keystrokes and increases legibility. Let’s see this applied to a simple four-beat rock groove with 8th note hats:

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

That plays hat() eight times, and snare() and kick() twice each. This beats having to type out 12 events.

Alternatively, an identifier can point to a predefined list, thus, a sequence can be reused multiple times. The following stores a complex hi-hat pattern in identifier p, and then plays it four times:

p = [0, 0.5, 1, 1.5, 2, 2.25, 2.5, 2.75, 3, 3.75]

@0  @p hat()
@4  @p hat()
@8  @p hat()
@12 @p hat()

Here’s the shorthand equivalent:

p = [0, 0.5, 1, 1.5, 2, 2.25, 2.5, 2.75, 3, 3.75]

@[0, 4, 8, 12] @p hat()

That’s 40 events in two lines of code, with improved legibility. If this was presented as 40 individual events, it would not be obvious that the same hat pattern is repeated four times.

Banks of Patterns

A list can be utilized as a bank of patterns, a list of lists. In the following example, an empty bank is created, filled with three patterns, and then used in a four measure sequence.

b = []
b.append([0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5])               # x.x. x.x. x.x. x.x.
b.append([0, 0.5, 1, 1.5, 2, 2.25, 2.5, 2.75, 3, 3.75])  # x.x. x.x. xxxx x..x
b.append([0, 0.5, 1, 1.5, 2, 2.5, 2.75, 3, 3.5, 3.75])   # x.x. x.x. x.xx x.xx

@0  @b[0] hat()
@4  @b[1] hat()
@8  @b[0] hat()
@12 @b[2] hat()

Bonus Round — Eight Ways to Notate 8th Note Hats

Some good, some bad, some ugly. All produce the same result.

1. @map(lambda x: x / 2.0, range(0, 8)) hat()
2. @[0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] hat()
3. @[i / 2.0 for i in range(0, 8)] hat()
4. @[i / 2.0 for i in range(8)] hat()
5. @[0, 2] @[0, 1] @[0, 0.5] hat()
6. @[0, 1, 2, 3] @[0, 0.5] hat()
7. @range(0, 4) @[0, 0.5] hat()
8. @range(4) @[0, 0.5] hat()

Examples 2, 6 and 8 are my personal favorites.

Nine Rules for Scheduling Events

How would Slipmat’s @ scheduler work when applying various Python methodologies?

To celebrate the release of my friend Sarah MacLean’s new book, Nine Rules to Break When Romancing a Rake, here are 9 rules for scheduling events. Each of the following examples produce the exact same result:

1. Individual Events

@0 foo()
@1 foo()
@2 foo()
@3 foo()

2. List

@[0, 1, 2, 3] foo()

3. Identifier

t = [0, 1, 2, 3]
@t foo()

4. List Comprehension

@[i for i in range(0, 4)] foo()

5. Loop

for i in range(0, 4):
    @i foo()    

6. Function

def bar():
    @0 foo()
    @1 foo()
    @2 foo()
    @3 foo()
    
bar()

7. Nested

foo()
1:
    foo()
    1:
        foo()
        1:
            foo()

8. Item Iteration

items = (0, 1, 2, 3)
@[i for i in items] foo()

9. Map

def bar(x): return x
@(map(bar, range(0, 4)) foo()

And a special message for Sarah, “Ninjas.”

Live Coding and Capturing a Perfomance

It’s the latest fad that’s sweeping computer music. And I would love for Slipmat to have this ability in its arsenal of tools. Without having to sacrifice non-realtime rendering for computationally expensive processes, of course.

The following conceptual live coding prototype shows what a simple session would look like if it was modeled on the Python interpreter:

$ slipmat --capture_performance my_session.txt
>>> from LiveCodeSeq import seq
>>> from MyBassLibrary import rad_rezzy
>>> from random import random
>>> p[0] = [int(random() * 12) for i in range(0, 16)]
>>> p[1] = [int(random() * 12) for i in range(0, 16)]
>>> p[0]
[5, 9, 11, 8, 7, 8, 5, 1, 10, 7, 4, 4, 6, 4, 4, 2]
>>> p[1]
[6, 6, 5, 3, 5, 7, 8, 4, 0, 0, 8, 7, 9, 7, 2, 4]
>>> r = rad_rezzy()
>>> s = seq(instr=r, pattern=p[0], base_pch=6.00, resolution=1/16, tempo=133)
>>> s.start()
>>> s.change_pattern(pattern=p[1], on_beat=0)
>>> @60 s.stop(onbeat=0)

I have a gut feeling that there are some changes that should be made. Though as a starting point, this isn’t a terrible one.

Being able to capture a live coding performance would be fantastic. Not sure how workable it would be, but perhaps such a feature would produce a file that could be played back later:

$ cat my_session.txt
@0             global.seed(7319991298)
@4.04977535403 from LiveCodeSeq import seq
@8.43528123231 from MyBassLibrary import rad_rezzy
@10.9562488312 from random import random
@15.6027957075 p[0] = [int(random() * 12) for i in range(0, 16)]
@20.7757632586 p[1] = [int(random() * 12) for i in range(0, 16)]
@26.2462371683 p[0]
@29.3961696828 p[1]
@34.0424988199 r = rad_rezzy()
@40.3211374075 s = seq(instr=r, pattern=p[0], base_pch=6.00, resolution=1/16, 
                   tempo=133)
@45.5491938514 s.start()
@47.8991166715 s.change_pattern(pattern=p[1], onbeat=0)
@52.6267958091 @60 s.stop(onbeat=0)

The @ schedules are the times in which return was originally pressed for each event. Looks like I’ll be spending some time with ChucK soon.

When Does That Happen?

Look at the following code and try to answer these two questions: When is foo() scheduled to play? When is bar() scheduled to play? Be sure to read about the @ scheduler if you haven’t already done so.

@0 x = 0
@1 x = 3

@(5 + x) foo()
@5:
    @x bar()

The answers: foo() is scheduled to play at beat 5, while bar() plays at beat 8.

Why is this?

For foo(), the expression for the scheduler @(5 + x) is evaluated at beat 0. At beat 0, x equals 0. This is a single schedule.

For bar(), there are two separate schedules that happen in tandem: @5 and then @x. The @x schedule isn’t evaluated until the @5 schedule is triggered. You might say that the @x schedule is a task of the @5 schedule. At the time @x is evaluated, x equals 3.

Of course, this is all theoretical at the moment.

Coding in Time with the @ Scheduler

One idea I have for my theoretical computer music language is having scheduling built right into the syntax, with the hopes that it will add the right balance of functionality and clarity.

I like the idea of having a score language separate from the orchestra language, though I’ve learned over the years that this approach acts as a bottle neck. The @ scheduler is a potential solution to bring both together, without losing the purpose of the score.

Instead of going into great detail on how the @ scheduler might work, I’ll just present the following four examples.

Example 1 — Nested Time:

do_something()     # Do something at beat 0, (@0 assumed)
@2 do_something()  # Do something at beat 2

@5:
    do_something()     # Do something at beat 5: 5 + 0, (@0 assumed)
    @3 do_something()  # Do something at beat 8: 5 + 3
    
    @4:                    # Block starts at beat 9: 5 + 4
        do_something()     # Do something at beat 9: 5 + 4 + 0, (@0 assumed)
        @1 do_something()  # Do something at beat 10: 5 + 4 + 1

Example 2 — Changing values mid-event:

def foo():
    freq = 440                                 # Initial frequency
    @1 freq *= 2                               # Frequncy doubles at time 1
    output Wavetable.osc(1, freq, sine(8192))  # Output signal

Example 3 — Scheduler error:

def foo():
    @1 freq = 440
    output Wavetable.osc(1, freq, sine(8192))  # Broken, freq doesn't exist

Example 4 — Organized score + generated events:

def hat_eights():
    for i in range(0, 8):
        @(i / 2.0) hat()

@0:
    hat_eights()
    @0 kick()
    @1 snare()
    @2 kick()
    @3 snare()
    
@4:
    hat_eights()
    @0 kick()
    @1 snare()
    @2 kick()
    @2.5 kick()
    @3 snare()

That last example reminds me of Max Mathews’ Radio Baton Conductor language.