Flylib.com

Books Software

 
 
 

Recipe 12.14. Representing Data as MIDI Music


Recipe 12.14. 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 Menard's midilib library makes it easy to generate MIDI music files from Ruby. It's available as the midilib gem.

Here's 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 'rubygems'
	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 you'd 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. Here's 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 track's channel to use the given instrument, and
	  # also set the track's 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

Here's 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('random.mid', 'w') { f song.write(f) }

See Also

  • midilib has a comprehensive set of RDoc, available online at http://midilib. rubyforge .org/

  • The library's examples/ directory has several good programs that demonstrate how to create and "play" MIDI files

  • The TimedTrack class presented takes several ideas from Emanuel Borsboom's Midi Scripter application; the Midi Scripter generates MIDI files from Ruby code that incorporates musical notationit's not really designed for use as a library, but it would make a good one (http://www.epiphyte.ca/downloads/midi_scripter/README.html)

  • The names of the standard MIDI instrument and drum sounds are kept in the arrays MIDI::GM_PATCH_NAMES and MIDI::GM_DRUM_NOTE_NAMES ; this isn't as useful as it could be, because you'll usually end up referring to instruments by their numeric IDs; the Wikipedia has a good mapping of numbers to names (http://en.wikipedia.org/wiki/General_MIDI#Program_change_events)