Representing Data as MIDI Music

Problem

You want to represent a series of data points as a musical piece, or just create music algorithmically.

Solution

Jim Menards midilib library makes it easy to generate MIDI music files from Ruby. Its available as the midilib gem.

Heres a simple method for visualizing a list of numbers as a piano piece. The largest number in the list is mapped to the highest note on the piano keyboard (MIDI note 108), and the smallest number to the lowest note (MIDI note 21).

require ubygems require midilib # => false class Array def to_midi(file, note_length=eighth) midi_max = 108.0 midi_min = 21.0 low, high = min, max song = MIDI::Sequence.new # Create a new track to hold the melody, running at 120 beats per minute. song.tracks << (melody = MIDI::Track.new(song)) melody.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(120)) # Tell channel zero to use the "piano" sound. melody.events << MIDI::ProgramChange.new(0, 0) # Create a series of note events that play on channel zero. each do |number| midi_note = (midi_min + ((number-midi_min) * (midi_max-low)/high)).to_i melody.events << MIDI::NoteOnEvent.new(0, midi_note, 127, 0) melody.events << MIDI::NoteOffEvent.new(0, midi_note, 127, song.note_to_delta(note_length)) end open(file, w) { |f| song.write(f) } end end

Now you can get an audible representation of any list of numbers:

((1..100).collect { |x| x ** 2 }).to_midi(squares.mid)

Discussion

The midilib library provides a set of classes for modeling a MIDI file: you can parse a MIDI file, modify it with Ruby code, and write it back to disk.

A MIDI file is modeled by a Sequence object, which contains track objects. A track is a mainly a series of Event objects: for instance, each note in the piece has a NoteOnEvent and a NoteOffEvent.

Array#to_midi works by transforming each number in the array into a corresponding MIDI note. A standard piano keyboard can produce notes ranging from MIDI note 21 to MIDI note 108, with middle C being at MIDI note 60. Array#to_midi scales the values of the array to fit into this range as closely as possible, using the same formula youd use to convert between two temperature scales.

Working directly with the MIDI classes is difficult, especially if you want to compose music instead of just transfering a data stream into MIDI note events. Heres a subclass of MIDI::Track that provides some simplifying assumptions and some higher-level musical functions, making it easy to compose simple multitrack tunes. Each TimedTrack uses its own MIDI channel and makes sounds from only one instrument. A TimedTrack can sound chords (this is very difficult with stock midilib), and instead of having to remember the MIDI note range, you can refer to notes in terms of half-steps away from middle C.

class TimedTrack < MIDI::Track MIDDLE_C = 60 @@channel_counter=0 def initialize(number, song) super(number) @sequence = song @time = 0 @channel = @@channel_counter @@channel_counter += 1 end # Tell this tracks channel to use the given instrument, and # also set the tracks instrument display name. def instrument=(instrument) @events << MIDI::ProgramChange.new(@channel, instrument) super(MIDI::GM_PATCH_NAMES[instrument]) end # Add one or more notes to sound simultaneously. Increments the per-track # timer so that subsequent notes will sound after this one finishes. def add_notes(offsets, velocity=127, duration=quarter) offsets = [offsets] unless offsets.respond_to? :each offsets.each do |offset| event(MIDI::NoteOnEvent.new(@channel, MIDDLE_C + offset, velocity)) end @time += @sequence.note_to_delta(duration) offsets.each do |offset| event(MIDI::NoteOffEvent.new(@channel, MIDDLE_C + offset, velocity)) end recalc_delta_from_times end # Uses add_notes to sound a chord (a major triad in root position), using the # given note as the low note. Like add_notes, increments the per-track timer. def add_major_triad(low_note, velocity=127, duration=quarter) add_notes([0, 4, 7].collect { |x| x + low_note }, velocity, duration) end private def event(event) @events << event event.time_from_start = @time end end

Heres a script to write a randomly generated composition with two tracks. The melody track (a trumpet)takes a random walk around the musical scale, and the harmony track (an organ) plays a matching chord at the beginning of each measure.

song = MIDI::Sequence.new song.tracks << (melody = TimedTrack.new(0, song)) song.tracks << (background = TimedTrack.new(1, song)) melody.instrument = 56 # Trumpet background.instrument = 19 # Church organ melody.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(120)) melody.events << MIDI::MetaEvent.new(MIDI::META_SEQ_NAME, A random Ruby composition) # Some musically pleasing intervals: thirds and fifths. intervals = [-5, -1, 0, 4, 7] # Start at middle C. note = 0 # Create 8 measures of music in 4/4 time (8*4).times do |i| note += intervals[rand(intervals.size)] #Reset to middle C if we go out of the MIDI range note = 0 if note < -39 or note > 48 # Add a quarter note on every beat. melody.add_notes(note, 127, quarter) # Add a chord of whole notes at the beginning of each measure. background.add_major_triad(note, 50, whole) if i % 4 == 0 end open( andom.mid, w) { |f| song.write(f) }

See Also

Категории