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.

Functions as Reusable Score Clips

I’m not a fan of the copy and paste method for composing computer music scores. I’d rather write something once, and reuse it as needed. Slipmat uses function definitions as one method for creating reusable score clips.

The following function rock_drums() stores a simple rock drum pattern with 8th note hats:

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

Here, the rock_drum() clip is played 4 times:

@0  rock_drums()
@4  rock_drums()
@8  rock_drums()
@12 rock_drums()

Here are 16 bars using the range() shorthand method:

@range(0, 64, 4) rock_drums()

Using functions as clips can save time and keystrokes while greatly improving legibility. The easier your code is to read, the more likely others will remix your work. If they can’t make heads or tails out of it, they’re more likely to move on.

Roll Your Own Score Syntax

A music language that comes with a fully-loaded set of string capabilities, including regular expressions, would allow users to develop their own methods for notating music. For example, here is a horizontal representation for rhythm guitar.

@0  strum('C',  '/... /... /./. /...')
@4  strum('Dm', '/... /... //// /./.')
@8  strum('G7', '/... /... ///. ../.')
@12 strum('C',  '/... /... /... ///.')

The slash triggers an event and advances time by a 16th note. The dot just advances time by a 16th note. A space does nothing.

These 4 lines of code represent 24 events. Not only does this shorten the score and save keystrokes, but is far more readable than if each individual event was explicitly written. In fact, I bet there are many guitar players out there that could play this as it’s written, providing he or she was given a brief explanation about the notation.

What about a system for triggering drums? Here’s the all-to-familiar 4-beat rock pattern with 8th note hats.

trigger(hat(),   'x.x. x.x. x.x. x.x.')
trigger(snare(), '.... x... .... x...')
trigger(kick(),  'x... .... x... ....')

Once again, the music is notated horizontally. Instead of 12 lines of code, there are only 3. Let’s take a look at the vertical equivalent:

@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()

In theory, people could write slipmat modules that mimic other text-based notation systems, such as the Csound score language.

import CsoundScore as CS
from MyLibrary import bassFM
from MyLibrary import pad

# For mapping slipmat instruments to a Csound-styled score
instrs = {"i1": bassFM(), "i2": pad()}

# A Csound-styled score
score = '''
i 1 0 2 0.5 7.00
i 1 + . .   6.05

i 2 0 8 0.333 8.09 0.77 1000
'''

CS.playScore(instrs, score)  # Play Csound-styled score

That would translate to:

@0 bassFM(2, 0.5, 7.00)
@2 bassFM(2, 0.5, 6.05)
@0 pad(8, 0.333, 8.09, 0.77, 1000)

Much of this can be done with some of the existing music languages out in the wild. The guitar strum and drum trigger examples can be accomplished in pure Csound code (see dseq — A Drum Machine Micro-Language). Even more so, Csound combined with the Python API can do some truly amazing things along these lines. Though it probably wouldn’t be nearly as fluid as a language that had this in mind when being designed from the ground up.

If you haven’t done so yet, take a moment to ponder the possibilities. And be sure to think about the things others will dream up that can be simply imported into your own compositions/synthesizers/sequencers/etc…