Tab Player Source Code

play.rb

#!/usr/bin/ruby

require 'guitar'
require 'tab'
require 'optparse'
require 'ostruct'

# Parse command line arguments
options = OpenStruct.new
options.debug = false
options.speed_factor = 1.0
options.tab_file = nil
options.midi_file = nil
options.guitar_type = Guitar::NYLON_ACOUSTIC

opts = OptionParser.new do |opts|
  opts.banner = "Usage: play.rb [options]"
  opts.separator ""

  opts.on("-t", "--tab-file TAB_FILE",
    "Set input tablature file.  If not specified, STDIN is read.") do |ifile|
    options.tab_file = ifile
  end

  opts.on("-m", "--midi-file MIDI_FILE",
    "Set output midi file.  If not specified, STDOUT is written.") do |mfile|
    options.midi_file = mfile
  end

  opts.on("-s", "--speed-factor SPEED_FACTOR",
    "Set speed factor.  < 1 = slower, > 1 = faster.") do |sf|
    options.speed_factor = sf.to_f
  end

  opts.on("-g", "--guitar-type GUITAR_TYPE",
    "Set type of guitar: n = nylon acoustic (default), s = steel acoustic",
    "  j = jazz electric, c = clean electric, m = muted electric",
    "  o = overdriven electric, d = distorted electric",
    "  h = harmonics") do |gt|
    options.guitar_type = Guitar.type_for_string(gt)
  end

  opts.on("-d", "--debug",
    "Turn on debug mode. Debug info is written to stdout,", "must use with -m switch.") do |dm|
    options.debug = dm
  end

  opts.on_tail("-h", "--help", "Show this message") do
    puts opts
    exit 1
  end
end

opts.parse!(ARGV)

if options.debug && options.midi_file.nil?
  puts "Midi file must be written to file using -m switch when -d debug mode is used."
  puts opts
  exit 1
end

istream = STDIN
if ! options.tab_file.nil?
  istream = File.new(options.tab_file)
end

# Create a (modified) guitar ... TODO: command line option for the scale
axe = Guitar.new(options.guitar_type, Guitar::EADGBE, (140*options.speed_factor).to_i, "eighth")

ostream = STDOUT
if ! options.midi_file.nil?
  ostream = File.new(options.midi_file, "w+")
end

tab = Tab.new(options.debug)
tab.parse(istream)
tab.play(axe, ostream)

tab.rb

class Tab
  @@notes = [ 's', 'e', 'q', 'h', 'w']

  def initialize(debug_mode)
    @debug = debug_mode
    @tabs = Array.new
  end

  def parse(instream)
    strings = nil
    string_count = 0
    while line = instream.gets
      line.chomp!
      # ignore lines that might not be tablature lines
      if !(line =~ /^[BbGgDdAaEe\d]?[\|\) \:]?[-x<> \[\]\(\)\{\}\*\=\^\/\\~\w\.:\|\d]+$/) || line.index("-").nil?
        if ! strings.nil?
          @tabs << strings
        end
        if @debug
          puts "re does not match: #{line}"
        end
        strings = nil
        string_count = 0
        next
      end

      # Get rid of the headers
      line.sub!(/^[BbGgDdAaEe\d]?[\|\) \:]?/, "")

      # Strip off trailing garbage (nb: this doesn't strip garbage unless it is separated off from
      #  the end of the tablature line by a space... couldn't figure out how to do strip garbage in that
      #  case)
      line.sub!(/([\|\-]) .+$/, '\1')

      # Eliminate measure markers ... the way measure markers are handled is inconsistent ... most
      #  files will experience extra delay where ever there is a measure marker, but there are some tabs
      #  that don't
      line.gsub!(/[\|\:]/, "")

      # Change capital letter oh 'O' to zero '0'  ... WOW!  There are tabs that use letter O instead
      #  of number 0
      line.gsub!(/O/, "0");

      # hack until I figure out all the special notation ... this script is stupid, it just plays notes.
      # Any special notation is replaced by silence :(
      line.gsub!(/[^\d]/, "x")

      # initialize the array of guitar strings
      if strings.nil?
        strings = Array.new
      end

      # Allows appending to the guitar string line, not sure if this is necessary any more ...
      # it probably is if sets of tab lines are right next to each other with no intervening 
      # lines
      if strings[string_count].nil?
        strings[string_count] = line
      else
        strings[string_count] += line
      end

      string_count += 1
    end

    # Collect the last array of strings ...
    @tabs << strings if not strings.nil?
  end

  def play(axe, ostream)

    # For each set of tablature lines
    @tabs.each { |ss|

      if @debug
        puts "strings:\n#{ss.join("\n")}"
      end

      # Skip unless we found tablature for 6 strings
      if ss.length() != 6
        next
      end

      if @debug
        puts "PLAYING these lines!"
      end

      # Figure out how many "notes" there are ... each guitar string line is considered
      # a list of eighths, just take the string with the smallest number of eighths
      num_eighths = 1000000
      ss.each { |str|
        if str.length < num_eighths
          num_eighths = str.length
        end
      }

      # Counter for consecutive silences
      delay_index = -1

      # What does a "silent chord" look like
      empty_chord = 'x|' * ss.size()
      i = 0
      while i < num_eighths - 1
        chord = ''
        max_number_length = 1
        # Figure out the chord ... it will be of the form 1|2|3|5|4|3|
        # two passes to handle alignment issues with two digit and one
        # digit notes... some tabs line them up on the first digit, yet
        # others line them up on the last digit.  This algorithm only
        # handles up to two consecutive digits for a note.
        ss.size().downto(1) { |s|
          this_max_number_len = 1

          # First case here is trying to deal with two digit numbers
          if ss[s-1][i].chr != "x" && ss[s-1][i+1].chr != "x"
            this_max_number_len += 1
          end

          # Save the size of the maximum string of numbers for later
          if this_max_number_len > max_number_length
            max_number_length = this_max_number_len
          end
        }

        # Second pass, we know the max consecutive digits, either 1 or 2
        ss.size().downto(1) { |s|
          # First case handles single digit lined up on the right
          if max_number_length > 1 && ss[s-1][i].chr == "x" && ss[s-1][i+1].chr != "x"
            chord << ss[s-1][i+1]
          # Second case handles two digit notes
          elsif ss[s-1][i].chr != "x" && ss[s-1][i+1].chr != "x"
            chord << ss[s-1][i]
            chord << ss[s-1][i+1]
          # single digit notes lined up on left
          else
            chord << ss[s-1][i]
          end
          chord << "|"
        }

        # Keep track of number of consecutive empty chords for poor man's timing
        if chord == empty_chord
          if delay_index + 1 < @@notes.length()
            delay_index += 1
          end
        else
          if delay_index == -1
            delay_index = 0
          end

          # get rid of the last pipe
          chord.chomp!("|")

          # Modified guitar wants the note in new format.  First char indicates the delay
          # that passed before current note.  After colon, we have pipe delimited note values
          # for each string
          axe.play("#{@@notes[delay_index]}:#{chord}")

          # reset the consecutive empty chords counter
          delay_index = -1
        end

        # skip past multiple digit notes
        i += max_number_length
      end

      # Not sure if this is valid, trying to put in a whole note of silence in between tabs 
      # found in the tab file.
      axe.play("w:x|x|x|x|x|x")

    }

    # Dump it to the stream
    ostream << axe.dump
  end
end

guitar.rb

require 'stringio'
begin
  require 'midilib'
rescue LoadError
  require 'rubygems' and retry
end

# *Very* simple software guitar for use with Ruby Quiz.
# See #play and the quiz for more on using this, and 
# why you might want to write your own instead.
class Guitar
  EADGBE = [40, 45, 50, 55, 59, 64]
  DADGBE = [38, 45, 50, 55, 59, 64]
  EbAbDbGbBbEb = [39, 44, 49, 54, 58, 63]
  DGCFAD = [38, 43, 48, 53, 57, 62]

  NYLON_ACOUSTIC = 25
  STEEL_ACOUSTIC = 26
  JAZZ_ELECTRIC = 27
  CLEAN_ELECTRIC = 28
  MUTED_ELECTRIC = 29
  OVERDRIVEN_ELECTRIC = 30
  DISTORTED_ELECTRIC = 31
  HARMONICS = 32

  def Guitar.type_for_string(t)
    case t
      when 'n'
        return NYLON_ACOUSTIC
      when 's'
        return STEEL_ACOUSTIC
      when 'j'
        return JAZZ_ELECTRIC
      when 'c'
        return CLEAN_ELECTRIC
      when 'm'
        return MUTED_ELECTRIC
      when 'o'
        return OVERDRIVEN_ELECTRIC
      when 'd'
        return DISTORTED_ELECTRIC
      when 'h'
        return HARMONICS
    end

    return NYLON_ACOUSTIC
  end

  # Create a new guitar with the specified tuning, sounding
  # like the specified hardware guitar. You can change 
  # tempo and timing here too if you like. EADGBE is the
  # standard tuning - see the appropriate consts for 
  # some alternate tunings if you need them. 
  def initialize(instr = NYLON_ACOUSTIC,
                 tuning = EADGBE,
                 bpm = 140,           # sounds okay with 
                 note = "eighth")     # most tabs...
    @tuning = tuning
    @seq = MIDI::Sequence.new
    @seq.tracks << (ctrack = MIDI::Track.new(@seq))
    @seq.tracks << (@track = MIDI::Track.new(@seq))
    @note = note

    @notes = { 's' => 'sixteenth', 'e' => 'eighth', 'q' => 'quarter', 'h' => 'half', 'w' => 'whole' }

    ctrack.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(bpm))
    ctrack.events << MIDI::ProgramChange.new(0,instr,0)
    ctrack.events << MIDI::ProgramChange.new(1,instr,0)
    ctrack.events << MIDI::ProgramChange.new(2,instr,0)
    ctrack.events << MIDI::ProgramChange.new(3,instr,0)
    ctrack.events << MIDI::ProgramChange.new(4,instr,0)
    ctrack.events << MIDI::ProgramChange.new(5,instr,0)

    @prev = [nil] * 6
    @prev_dist = [0] * 6
  end

  # Play some notes on the guitar. Pass notes in this notation:
  # 
  #   "n:6|5|4|3|2|1"
  #   first char is the note type (sixteenth, eighth, quarter, etc) followed
  #     by a colon and then the frets for each string (pipe '|' separates
  #     fret number for each string)
  #
  # Unplayed strings should be '-' or 'x'.
  #
  # So, an open Am chord could be played with:
  #
  #   axe.play("x|0|2|2|1|0")
  #
  # Which would look like this on a hardware guitar:
  #
  #   E  A  D  G  B  e
  #   ---O-----------O
  #   |  |  |  |  |  |
  #   |  |  |  |  X  |
  #   ----------------
  #   |  |  |  |  |  |
  #   |  |  X  X  |  |
  #   ----------------
  #   |  |  |  |  |  |
  #
  # for example. To play the guitar, keep calling this
  # method with your notes, and then call dump when you're
  # done to get the MIDI data.
  #   
  def play(notes)

    md = /(\w):(.+)/.match(notes)

    notetype = @notes[md[1]]
    d = @seq.note_to_delta(notetype)

    # n.b channel is inverse of string - chn 0 is str 6    
    md[2].split('|').each_with_index do |fret, channel|
      if fret.to_i.to_s == fret
        fret = fret.to_i
        oldfret = @prev[channel]
        @prev[channel] = fret

        if oldfret
          oldnote = @tuning[channel] + oldfret
          @track.events << MIDI::NoteOffEvent.new(channel,oldnote,0,d)
          d = 0
        end

        noteval = @tuning[channel] + fret
        @track.events << MIDI::NoteOnEvent.new(channel,noteval,80 + rand(38),d)
        d = 0
      end
    end
  end

  # dump out the notes played on this guitar to MIDI and return
  # as a string. This can be written out to a midi file, piped
  # to timidity, or whatever.
  #
  # The generated midi is simple and just uses a single track
  # with no effects or any fancy stuff.
  def dump
    out = StringIO.new
    @track.recalc_times
    @seq.write(out)
    out.string
  end
end