# Yet Another Byte Organ
# by Leonard Richardson (leonardr@crummy.com)
# Based on "Byte Organ" by Jack Aboutboul, Andy Doro and Adam Parrish
# (http://itp.nyu.edu/~ap1607/nbo/)
#
# Usage: organ.rb [MIDI instrument] [bpm] < data > song.mid

require 'rubygems'
require 'midilib'

# Generic MIDI library, taken from the Ruby Cookbook
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

  private

  def event(event)    
    @events << event
    event.time_from_start = @time
  end
end

# The real stuff
class ByteOrgan < MIDI::Sequence

  @@root = -1 # The Bb below middle C
  @@scale = [0, 2, 3, 7, 9, 12] # Pentatonic scale
  @@min_velocity = 30

  ## @@scale = [0, 3, 5, 6, 7, 11, 12, 15] # Blues scale

  def initialize(instrument, bpm)
    super()
    self.tracks << (@melody = TimedTrack.new(0, self))
    @melody.instrument = instrument

    @melody.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(bpm))
    @melody.events << MIDI::MetaEvent.new(MIDI::META_SEQ_NAME, 
                                          'The Byte Organ')
  end

  def feed(bytes)
    bytes.each_byte do |byte|
      # Make the six least significant bits into a chord.
      chord = []
      i = 0
      until i == 6 do
        chord << (@@root + @@scale[i]) if byte & (1 << i) != 0
        i += 1
      end

      # Use the two most significant bits to determine note velocity.
      velocity_bits = byte & (128+64)
      velocity = @@min_velocity 
      if velocity_bits != 0
        velocity += ((127-@@min_velocity)/velocity_bits)
      end
      @melody.add_notes(chord, velocity, 'sixteenth')
    end    
  end
end

# Command-line interface
if ARGV.size > 0
  instrument, bpm = ARGV
  instrument = MIDI::GM_PATCH_NAMES.index(instrument)
  unless instrument
    puts "No such instrument. Choose from the following:"
      puts " " + MIDI::GM_PATCH_NAMES.join("\n ")
      exit
  end
end
instrument ||= 1 # Piano
bpm ||= 120

organ = ByteOrgan.new(instrument, bpm)
organ.feed(STDIN)
organ.write(STDOUT)

