CyberSpy

Rantings from a guy with way too much free time

Maestro Cue the Music!

2017-10-29 musings

Now it's time to get conducting.

We've learned all about L-systems, midi magic, tools like MISEP that generates musical patterns. Now we can take it all together and generate some algorithmically inspired music!

To do this, I'mg going to reach for the isobar kit I mentioned in the first post. Rather than spend time writing the tedious algorithms to perform the mappings, this fine python library offers a rich collection of algorithmic generators and mappings - for chromatic, velocity, and rhythmic elements.

Let's take a look at some simple constructions and listen to the sounds produced. Before we do that, let's get our environment established to use the isobar library.

Setup

As usual, let's establish a virtual environment sandbox where we can install and play without interfering with other applications environments on our box. Keep in mind, isobar is a Python 2.7-based library, so we'll use the traditional virualenv tool to establish our playground.

virtualenv sandbox
cd sandbox; source bin/activate

And to install isobar:

git clone https://github.com/ideoforms/isobar.git
cd isobar
pip install python-midi
pip install MIDIUtil
python setup.py install

isobar

Now that our environment is installed, we're ready to compose. First, let's take a quick look at the isobar library. Fundamentally, the library allows us to compose MIDI and direct that output to several places, most relevant, a file or a MIDI device. For ease of use, I'm goig to show how one generates output to a MIDI file.

The mechanisms are straight-forward (and an example code is given in the library's examples directory within the repo). Here's a slightly modified version of the midi file writing example that produces a chromatic scale:


from isobar import *
from isobar.io.midifile import MidiFileOut

filename = "output.mid"
output = MidiFileOut(filename)

timeline = Timeline(120, output)
timeline.sched({ 'note' : [ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 60 ], 'dur' : 0.25 })
timeline.run()

output.write()

Now that we can produce an audio file, let's dive deeper into the algorithms.

Isobar offers a robust collection of pattern classes which implement Python's iterator protocol affording use of python's built-ins like list(),sorted(), and itertools on any isobar pattern objects.

Here's a list of all of the Pattern classes:

Pattern classes:

CORE (core.py)

Pattern          - Abstract superclass of all pattern generators.
PConst           - Pattern returning a fixed value
PRef             - Pattern containing a reference to another pattern
PDict            - Dict of patterns
PIndex           - Request a specified index from an array.
PKey             - Request a specified key from a dictionary.
PConcat          - Concatenate the output of multiple sequences.
PAdd             - Add elements of two patterns (shorthand: patternA + patternB)
PSub             - Subtract elements of two patterns (shorthand: patternA - patternB)
PMul             - Multiply elements of two patterns (shorthand: patternA * patternB)
PDiv             - Divide elements of two patterns (shorthand: patternA / patternB)
PMod             - Modulo elements of two patterns (shorthand: patternA % patternB)
PPow             - One pattern to the power of another (shorthand: patternA ** patternB)
PLShift          - Binary left-shift (shorthand: patternA << patternB)
PRShift          - Binary right-shift (shorthand: patternA << patternB)

SEQUENCE (sequence.py)

PSeq             - Sequence of values based on an array
PSeries          - Arithmetic series, beginning at <start>, increment by <step>
PRange           - Similar to PSeries, but specify a max/step value.
PGeom            - Geometric series, beginning at <start>, multiplied by <step>
PLoop            - Repeats a finite <pattern> for <n> repeats.
PConcat          - Concatenate the output of multiple finite sequences
PPingPong        - Ping-pong input pattern back and forth N times.
PCreep           - Loop <length>-note segment, progressing <creep> notes after <count> repeats.
PStutter         - Play each note of <pattern> <count> times.
PWrap            - Wrap input note values within <min>, <max>.
PPermut          - Generate every permutation of <count> input items.
PDegree          - Map scale index <degree> to MIDI notes in <scale>.
PSubsequence     - Returns a finite subsequence of an input pattern.
PImpulse         - Outputs a 1 every <period> events, otherwise 0.
PReset           - Resets <pattern> each time it receives a zero-crossing from
PCounter         - Increments a counter by 1 for each zero-crossing in <trigger>.
PArp             - Arpeggiator.
PEuclidean       - Generate Euclidean rhythms.
PDecisionPoint   - Each time its pattern is exhausted, requests a new pattern by calling <fn>.

CHANCE (chance.py)

PWhite           - White noise between <min> and <max>.
PBrown           - Brownian noise, beginning at <value>, step +/-<step>.
PWalk            - Random walk around list.
PChoice          - Random selection from <values>
PWChoice         - Random selection from <values>, weighted by <weights>.
PShuffle         - Shuffled list.
PShuffleEvery    - Every <n> steps, take <n> values from <pattern> and reorder.
PSkip            - Skip events with some probability, 1 - <play>.
PFlipFlop        - flip a binary bit with some probability.
PSwitchOne       - Capture <length> input values; repeat, switching two adjacent values <n> times.

OPERATOR (operator.py)

PChanged         - Outputs a 1 if the value of a pattern has changed.
PDiff            - Outputs the difference between the current and previous values of an input pattern
PAbs             - Absolute value of <input>
PNorm            - Normalise <input> to [0..1].
PCollapse        - Skip over any rests in <input>
PNoRepeats       - Skip over repeated values in <input>
PMap             - Apply an arbitrary function to an input pattern.
PMapEnumerated   - Apply arbitrary function to input, passing a counter.
PLinLin          - Map <input> from linear range [a,b] to linear range [c,d].
PLinExp          - Map <input> from linear range [a,b] to exponential range [c,d].
PRound           - Round <input> to N decimal places.
PIndexOf         - Find index of items from <pattern> in <list>
PPad             - Pad <pattern> with rests until it reaches length <length>.
PPadToMultiple   - Pad <pattern> with rests until its length is divisible by <multiple>.

STATIC (static.py)

PStaticTimeline  - Returns the position (in beats) of the current timeline.
PStaticGlobal    - Static global value identified by a string, with OSC listener

FADE (fade.py)

PFadeNotewise    - Fade a pattern in/out by introducing notes at a gradual rate.
PFadeNotewise    - Fade a pattern in/out by gradually introducing random notes.

MARKOV (markov.py)

PMarkov          - First-order Markov chain.

LSYSTEM (lsystem.py)

PLSys            - integer sequence derived from Lindenmayer systems

WARP (warp.py)

PWInterpolate    - Requests a new target warp value from <pattern> every <length> beats
PWSine           - Sinosoidal warp, period <length> beats, amplitude +/-<amp>.
PWRallantando    - Exponential deceleration to <amp> times the current tempo over <length> beats.

As you can see, the toolkit is extensive. Let's focus in on the L-System construction and use the PLSys pattern class to generate a midi file. This is a derived example from the examples directory, but we've done two modifications:

  • We've modified the program to write to a MIDI file
  • We need to correct a bug in the MIDIutils library that causes the writing of the file to quit with an error on index bounds

Bug Fix! yikes!

In sandbox/lib/python2.7/site-packages/midiutil/MidiFile.py we need to modify the function deInterleaveNotes. It looks like the developers of this library pop a list that's potentially empty. Take a look at the modified code:

def deInterleaveNotes(self):
        '''
        Correct Interleaved notes.

        Because we are writing multiple notes in no particular order, we
        can have notes which are interleaved with respect to their start
        and stop times. This method will correct that. It expects that the
        MIDIEventList has been time-ordered.
        '''

        tempEventList = []
        stack = {}

        for event in self.MIDIEventList:

            if event.type == 'NoteOn':
                if str(event.pitch)+str(event.channel) in stack:
                    stack[str(event.pitch)+str(event.channel)].append(event.time)
                else:
                    stack[str(event.pitch)+str(event.channel)] = [event.time]
                tempEventList.append(event)
            elif event.type == 'NoteOff':
                if len(stack[str(event.pitch)+str(event.channel)]) > 1:
                    event.time = stack[str(event.pitch)+str(event.channel)].pop()
                    tempEventList.append(event)
                else:
                    if len(stack[str(event.pitch)+str(event.channel)]) > 0:  ### MODIFIED TO CONDITIONALLY POP ###
                        stack[str(event.pitch)+str(event.channel)].pop()
                    tempEventList.append(event)
            else:
                tempEventList.append(event)

        self.MIDIEventList = tempEventList

        # Note that ``processEventList`` makes the ordinality of a note off event
        # a bit lower than the note on event, so this sort will make concomitant
        # note off events processed first.

        self.MIDIEventList.sort(key=sort_events)

Without this modification, the code will break if you attempt to pass the dur tag a time list in the timeline.sched.

If you don't feel comfortable making the change to the MIDIUtil library, you can just use a static constant for dur (say a quarter note 0.25).

PLSys example.

In the example below, we use PLSys to generate notes, rhythms, and velocities (volume). Notice the patterns and depth.

As L-Systems are iterative (recursively so), we specify the iterations via the depth value passed to the PLSys call.

Also, notice the pattern and symbols:

PLSys uses the following alphabet:

  • $N$ generates a note node
  • $+$ transposes up one semitone
  • $-$ transposes down on semitone
  • $[$ enter a recursive branch (push)
  • $]$ leave a recursive branch (pop)

Similarly, for time and velocity we use the production rules, but modify them with the PAbs function to create non-negative values.

from isobar import *
from isobar.io.midifile import MidiFileOut

filename = "lsys.mid"
output = MidiFileOut(filename)


import random
import time

notes = PLSys("N+[+N+N--N+N]+N[++N]", depth = 4)
notes = notes + 60

# use another l-system to generate time intervals.
# take absolute values so that intervals are always positive.
times = PLSys("[N+[NN]-N+N]+N-N+N", depth = 3)
times = PAbs(PDiff(times)) * 0.25
# delay = PDelay(notes, times)

# ...and another l-system for amplitudes.
velocity = (PLSys("N+N[++N+N--N]-N[--N+N]") + PWhite(-4, 4)) * 8
velocity = PAbs(velocity)

timeline = Timeline(120, output)
timeline.sched({ 'note' : notes, 'amp' : velocity, 'dur' : times })
timeline.run()

output.write()

Generating the midi file by simply running the python code above. If successful, a lsys.mid file is generated. We can play the midi file using the mplay alias we introduced in the previous blog in this series. Or, if you'd prefer a wav or mp3 file, we can convert the midi file as well (also demonstrated in the previous post).

comments powered by Disqus