|
MIDI Generator Update: I've improved this to the point where the new version is something I can actually use to generate ambient soundscapes for background layers in real tracks. I haven't written it up yet, although I did deliver a presentation about it to a Ruby users' group. This page documents the first version. I'm taking a music class and our teacher made a pretty awesome move. We got a lecture on John Cage (and a performance of 4'33"). I've taken this class before; this wasn't in it last time. We went from 4'33" (a famous and controversial piece which consists entirely of silence) to Cage's experiments with rule-based composition and the I Ching. Then we spent a class session creating music from a rule-based composition, and our homework was to come up with original rule-based compositions of our own. A rule-based composition is essentially an algorithm. Cage's experiments predate MIDI by several decades, but today, it's possible to implement rule-based composition as computer programs, and in fact there are lots of programs out there which do just that. I decided that would be a good way to do my homework. However, the initial rule-based composition we did in the class session used a shuffled deck of cards to drive the rules; that is to say, at each step, the decisions were made either by chance (the deck of cards) or by the rules set out in the composition. A shuffled deck of cards is essentially a random number generator. But the problem with that is that random number generators ultimately translate into white noise. And there wasn't anything in the homework assignment specifically limiting us to a purely random noise source. So, I did three iterations of a program which generated a MIDI file. The first iteration used Ruby's rand() function to produce random noise. The second alternated randomly between random noise and arbitrary pre-determined constants. The third used continuations to feed the code which generated MIDI. It alternated randomly between a generator which fed the code the Fibonacci sequence, and a generator which fed it the output from a Feigenbaum function. Feigenbaum functions model nonlinear fluid dynamics. Continuations are code fragments which essentially return IOUs on future computation, rather than performing computation which may or may not be necessary. Both of these topics are somewhat complex so we'll skip any further detail for now. Here's the code. serial.rb #! /usr/bin/env ruby
# the generator code is a direct copy from code by Jim Weirich in Hal Fulton's book
# "The Ruby Way." the Feigenbaum generator runs a Feigenbaum sequence instead of the
# Fibonacci sequence but otherwise is just a copy of Weirich's code also.
# here's Weirich's Fibonacci sequence generator.
class Generator
def initialize
do_generation
end
def next
callcc { | here |
@main_context = here
@generator_context.call
}
end
private
def do_generation
callcc { | context |
@generator_context = context
return
}
generating_loop
end
def generate(value)
callcc { | context |
@generator_context = context
@main_context.call(value)
}
end
end
# Weirich's Fibonacci subclass
class Fibonacci < Generator
def generating_loop
generate(1)
a, b = 1, 1
loop do
generate(b)
a, b = b, a+b
end
end
end
# extremely similar Feigenbaum generator. Feigenbaum functions look
# like this: X[n+1] = R*X[n] * (1 - X[n]). this is an iterating equation,
# check a good book on chaos theory for more detail. edit the value
# for r to see the function's output vary. it's chaos math, so the
# output can actually be anything from a single value to a consistent
# pattern to apparently random noise, depending on the value of the R
# constant (I think R stands for "robust").
class Feigenbaum < Generator
def generating_loop
r = 3.2
generate(0.1)
a = 0.1
b = (r * a * (1 - a))
loop do
generate(b)
a, b = b, (r * b * (1 - b))
end
end
end
# ok, that's the support stuff, here's the main code
def feigenbaum
($fig.next * 127).round
end
def fibonacci
the_number = $fib.next
if (the_number > 127)
$fib = Fibonacci.new
the_number = $fib.next
end
the_number
end
def get_a_number
# this is the cool bit. serial music operates by driving a sequence of
# instructions from an initial choice. the initial choice in the classroom
# example, and in John Cage's original composition experiments, was random;
# however, there are other ways to do it. here's how get_a_number does it.
# I did three iterations, basically rendering output each time I got some piece
# of code working. the first iteration just returned random numbers; the second
# returned random numbers one out of every three times and arbitrary constants
# (5 and 23) an equal number of other times. this third time, the number gets
# popped from one of two continuations, representing either the Fibonacci
# sequence, or a sequence generated by a Feigenbaum function. output from each
# iteration gets saved as MIDI and imported into Reason (by hand).
case rand(2)
when 0
fibonacci
else
feigenbaum
end
end
def new_note(note_length, track)
channel = 0
note = get_a_number
velocity = get_a_number
start = 0
track.events << NoteOnEvent.new(channel, note, velocity, start)
track.events << NoteOffEvent.new(channel, note, velocity, note_length)
end
def new_controller(track)
controller, data = get_a_number, get_a_number
if (controller == CC_VOLUME)
controller = controller + 1
end
track.events << Controller.new(0, controller, data)
end
# init section -- I basically just copied this from the example file
# for midilib, by Jim Menard.
$LOAD_PATH[0, 0] = File.join(File.dirname(__FILE__), '..', 'lib')
require 'midilib/sequence'
require 'midilib/consts'
include MIDI
seq = Sequence.new()
track = Track.new(seq)
seq.tracks << track
track.events << Tempo.new(Tempo.bpm_to_mpq(238))
track.events << MetaEvent.new(META_SEQ_NAME, 'Serial')
track = Track.new(seq)
seq.tracks << track
track.name = 'serial'
track.instrument = GM_PATCH_NAMES[0]
track.events << Controller.new(0, CC_VOLUME, 127)
track.events << ProgramChange.new(0, 1, 0)
note_length = seq.note_to_delta('32nd')
# new stuff
$fib = Fibonacci.new
$fig = Feigenbaum.new
for var in 23..532
new_note(note_length, track)
if (var % 23 == 0)
new_controller(track)
end
end
# from the original midilib example
File.open('serial.mid', 'wb') { | file |
seq.write(file)
}
# the shoutouts! Hal Fulton's book credits Hugh Sasse as involved
# with the development of Jim Weirich's generator code, Avi Bryant
# and Paul Graham get credit for making me interested in continuations
# in the first place, and Mark-Jason Dominus gets credit for explaining
# them better than anybody I'm aware of in "Higher-Order Perl,"
# even though he used different terminology. and of course Jim Menard
# wrote the MIDI library that made this possible in the first place. :-)
# last but not least Jason Goodyear gets credit for giving interesting
# homework.
And here's the mp3 (19 secs., 284kb). Obviously music driven by a random number generator, the Fibonacci sequence, and chaos theory isn't necessarily going to be the catchiest tune you'll ever hear. But here's the interesting part. The third MIDI file, driven by the Fibonacci sequence and the Feigenbaum function, is basically responsible for the majority of musical content in the piece, such as it is. Here's an mp3 without the third MIDI file, and here's an mp3 of just the third MIDI file. See what I mean? That sort of guitar sound at the end of the piece, I didn't put it in there, and I don't even know how it happened (although obviously I can look at the Reason file and make an educated guess). The use of mathematical patterns occurring in nature resulted in noise (in the programming sense) which was more musical than the noise (in the musical sense) generated by pure randomness. If you think about it, it makes perfect sense. |