Forum: Ruby Tab Player (#79)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
4299e35bacef054df40583da2d51edea?d=identicon&s=25 James Gray (bbazzarrakk)
on 2006-05-12 15:26
(Received via mailing list)
The three rules of Ruby Quiz:

1.  Please do not post any solutions or spoiler discussion for this quiz
until
48 hours have passed from the time on this message.

2.  Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3.  Enjoy!

Suggestion:  A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby Talk follow the discussion.  Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Ross Bamford

One for the music lovers this week - can your computer play guitar?
Basically,
the aim of this week's quiz is to implement a Ruby program that will
'read'
guitar tablature, and generate a rendition of that tablature in some
sound
format.

In case you're unfamiliar with tablature, it's a form of written guitar
music
that's easier to learn and often more convenient than real sheet music.
It
doesn't carry nearly as much information about a piece, but it provides
a good
starting point and is an easy way to record key bits you don't want to
forget.
Also, it's quite amenable to ASCII formatting. It looks like this:

	e|-----------------------------0-1-3-|
	B|-----------------------0-1-3-------|
	G|-------------------0-2-------------|
	D|-------------0-2-3-----------------|
	A|-------0-2-3-----------------------|
	E|-0-1-3-----------------------------|

	e|1-0--------------------------------|
	B|----3-1-0--------------------------|
	G|----------2-0----------------------|
	D|--------------3-2-0----------------|
	A|--------------------3-2-0----------|
	E|--------------------------3-1-0----|

The six horizontal lines represent the six strings of the guitar (the
thickest
string, the sixth, being the bottom line) with time progressing left to
right,
top to bottom. The numbers on the strings show the fret at which that
string
should be held when the string is played.

You can find a bit more about tableture at Wikipedia [1], and a tutorial
at
guitar.about.com [2]. You'll also find lots of (non-commercial use only)
tabs at
OLGA [3].

	What you need to do
	===================

Firstly, you'll need to select some tabs to work with. You might try
OLGA, or
google around a bit. Unfortunately, the quality of freely available tabs
is very
variable, so you might prefer to bug your musician friends for something
you can
use. Also, a few simple scales and other bits are included with the quiz
[4].

Once that's done, you'll need to choose a guitar. This quiz includes a
free
guitar [4] that supports the bare minimum you need to play simple tabs,
with
output to MIDI format (thanks to midilib [5]). To use this guitar,
you'll need
midilib installed (it's available as a Gem). See guitar.rb for more
info.

The aim is that your program will play the guitar, and then dump the
midi. You
may accept any commandline parameters you wish, and should output the
midi data
to stdout.

If you don't have anything to listen with, check out TiMidity++ [6].

	Oh man, this guitar blows!
	==========================

If you're a guitarist, or you surfed a few tabs, you'll notice
immediately that
the provided guitar is actually pretty useless. Firstly, it only has
nine frets
(an odd number, I admit). And sure, you can hit the notes, but that's
all -
there's a world of stuff it can't support: bends, hammer-on/pull-off,
letting
notes to ring, harmonics, the list goes on.

A lot of this could be supported by MIDI, if only you had a better
guitar. Some
people build their own guitars, and for extra credit in this quiz,
that's
exactly what you should do: extend or rewrite the Guitar class to
support a
full-size neck, and as many tabbable effects as you can.

	Things to bear in mind
	======================

	* As mentioned, tab quality on the 'net is variable - don't expect your
	  favourite song to sound exactly right. Often, tabs provide more of a
	  starting point than an accurate transcript.

	* Timing is one of the most important qualities in a piece of music,
but
	  tablature sadly lacks any notion of it. For that reason you shouldn't
	  worry too much about it, though it's nice if you can provide a way
	  to specify tempo and timing for a given tab on the command line.

	* ASCII tabs are often hand-written, and vary slightly in their
formatting
	  and the different characters used. Try to be liberal in what you
accept.

	---------
	[1] http://en.wikipedia.org/wiki/Tablature
	[2] http://guitar.about.com/library/blhowtoreadtab.htm
	[3] http://www.olga.net/dynamic/browse.php?setPrintabl...
	[4] http://rubyquiz.com/tab_player.zip
	[5] http://midilib.rubyforge.org/
	[6] http://timidity.sourceforge.net/
2ef955fe05ff30bd952e90ba2751614d?d=identicon&s=25 Shane Emmons (semmons99)
on 2006-05-12 17:08
This is really cool. Now what would be even cooler is if someone could
modify their Markov Chain program to read tabs and generate new songs to
be played.
Ce8b03e5750097942c58e12b46724312?d=identicon&s=25 Giles Bowkett (Guest)
on 2006-05-12 18:54
(Received via mailing list)
On 5/12/06, Shane Emmons <shane.emmons@ffgrp.com> wrote:
> This is really cool. Now what would be even cooler is if someone could
> modify their Markov Chain program to read tabs and generate new songs to
> be played.

I've actually done that already, or at least, something reasonably
similar. I had a class on serial music and implemented it in Ruby
generating MIDI files for Reason. Then I expanded the original code
into an only slightly bigger script I think of as the texturizer,
which generates ambient soundscapes via midilib, a Fibonacci sequence,
a Feigenbaum function, and ReDrum (Reason's internal sample-based drum
machine).

Generating ambient soundscapes was very easy. I'm currently working on
a version which generates grooves. It's the same basic code, with
different samples in the ReDrum, and obviously a much shorter span of
time (two bars instead of seven minutes). The first test version
worked beautifully but since then I've broken it somehow and I'm not
sure what exactly I did.

Most of the code is actually very simple. I won't post any, because
it's obviously related to the subject of the quiz, but if you want you
can hunt around on gilesgoatboy.org and you should find it fairly
quickly. ^_^ (The serial music code, that is, I haven't properly
written up the texturizer, and the groove-makey-thing isn't even
finished yet.)
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-05-12 19:22
(Received via mailing list)
On Sat, 2006-05-13 at 00:09 +0900, Shane Emmons wrote:
> This is really cool. Now what would be even cooler is if someone could
> modify their Markov Chain program to read tabs and generate new songs to
> be played.
>

You read my mind :) Actually, the idea for this quiz grew out of my
playing around with exactly that. I never ended up taking it anywhere,
mainly because it got pretty difficult to make it differentiate between
different styles of play, tempo, key, etc. Just doing the Markov thing
it's pretty difficult to get anything but noise - I guess by very
carefully controlling the inputs to contain only a specific type of
music (e.g. just solos, just strummed progressions, etc.,) some
reasonably coherent sounds could be obtained, but then of course you'd
get the same problems as with the regular Markov chains and limited
bodies of input.

I still think it's possible to come up with halfway decent results using
that technique, but I think you'd have to step back from the notes and
chords, and instead look for similar phrases - Markov with a high order
value - and maybe give it the ability to transpose a phrase to fit, but
I still don't know if you'd find enough scope to do anything except
regenerate one of the inputs, or get stuck in a loop.

There are some fairly cool things you can do to this end, though:

	http://en.wikipedia.org/wiki/Computer-generated_music

It would certainly be very cool to see all this brought together in
Ruby :)
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-05-12 19:32
(Received via mailing list)
On Sat, 2006-05-13 at 01:52 +0900, Giles Bowkett wrote:
> a Feigenbaum function, and ReDrum (Reason's internal sample-based drum
> machine).

Very cool! I've just been having a browse with my ears, and I have to
say I'm pretty impressed with some of the sounds you've generated with
this thing. I'm definitely going to be studying your code - although
I've always played, I've only recently gotten interested in
computer-aided composition so this stuff is very new to me.
Ce8b03e5750097942c58e12b46724312?d=identicon&s=25 Giles Bowkett (Guest)
on 2006-05-12 22:00
(Received via mailing list)
On 5/12/06, Ross Bamford <rossrt@roscopeco.co.uk> wrote:
> > which generates ambient soundscapes via midilib, a Fibonacci sequence,
> > a Feigenbaum function, and ReDrum (Reason's internal sample-based drum
> > machine).
>
> Very cool! I've just been having a browse with my ears, and I have to
> say I'm pretty impressed with some of the sounds you've generated with
> this thing. I'm definitely going to be studying your code - although
> I've always played, I've only recently gotten interested in
> computer-aided composition so this stuff is very new to me.

muchas gracias! although I have to tell you, the sounds are only
partly programmatically generated -- they come from synths in
Propellerhead Reason. (a very large proportion of the parameters
defining the sounds which the synths played were automated in
real-time programmatically, however.) I generate the MIDI file
directly with Ruby and midilib and then pop into Reason. and I'm not
above adding a little distortion and reverb to sweeten the overall
sound.

also, the stuff that's online, I'm very glad you like it, but it
probably is fairly easy to recognize as computer-generated. the new
thing I'm working on, you can't actually tell the computer-generated
texture is computer-generated. it sounds pretty much like your
standard cinematic texture in dubby dance music. take out the drums
(non-programmatic) and it sounds like seven minutes of very pretty
ambient music.

(I think the computer-generated groove-maker will (when complete)
display similar versimillitude to "human" music, but don't quote me on
that.)

this new stuff works by triggering samples at times determined by a
function almost identical to the function in the code on my site.
(I'll put an mp3 and a code sample online soonish.) the Markov chain
stuff is very interesting to me because it would be a huge quantum
leap for my stuff if I could generate melodies programmatically.
Markov chains could be a good method. the other idea I had for doing
this, actually, was A*, which is the pathfinding algorithm favored in
video games -- the idea being that you define a melody as a path from
note A to note B, and create arbitrary "obstacles" in that path to
encourage more or less complicated melodies. haven't really got that
part started yet but it is a neat idea.
E025e426244a038645a4a08ba1a2d97f?d=identicon&s=25 Anthony Moralez (Guest)
on 2006-05-15 16:08
(Received via mailing list)
Here is my attempt at a guitarist.
class Guitarist
  def initialize(guitar=nil)
    @guitar = guitar unless guitar.nil?
    @guitar = Guitar.new(Guitar::CLEAN_ELECTRIC) if guitar.nil?
  end
  def play(tab)
    tab.chords.each { |notes| @guitar.play(notes)  }
    @guitar.dump
  end
end

He's not a good guitarist. He only plays in the first 9 frets. He can
play all of the included tabs on the included guitar. Of course the
hard work is done by the tab reader.

class Tab
  def initialize(tab_file)
    @chords = []
    @file = tab_file
    @tab = extract_tabs(tab_file)
  end

  #select only lines containing tab notation and remove extraneous chars
  def extract_tabs(file)
    File.readlines(file).select { |line|
      line =~ /[eBCDAE|-][|-]/
    }.collect { |line|
      line.gsub(/[eBGDAE|\s]/, '')
    }
  end

  def chords
    return [] if @tab.empty?
    return make_chords if @chords.empty?
    @chords
  end

  #break each string into individual notes
  # then zip and join the notes into the notation the guitar expects
  def make_chords
    @tab.collect! { |string| string.split(//) }
    (0...sections).each { |e|
      @chords << @tab[e+5].zip(@tab[e+4], @tab[e+3], @tab[e+2],
@tab[e+1], @tab[e]).collect { |chord| chord.join }
    }
    @chords.flatten!
  end

  def sections
    @tab.length / 6
  end
end


And that's all there is to my simple guitarist. Thanks Ross for
another neat quiz.

Anthony Moralez
http://anthonysseblog.blogspot.com
2ef955fe05ff30bd952e90ba2751614d?d=identicon&s=25 Shane Emmons (semmons99)
on 2006-05-16 18:00
Here is my simple solution. The main work horse of the whole thing is
the Tab class. Which I will display first.

--BEGIN CODE--

#!/usr/local/bin/ruby -w

class Tab

  attr_reader :file, :music

  def initialize( file )
    @file = file
  end

  def parse( file = @file )
    @music = Array.new
    tab = Hash.new { |hash, key| hash[key] = Array.new }

    File.open(file).read.each do |line|
      next unless line =~ /^[EADGBe]/
      bar = line.chomp.split(//)
      bar.each do |note|
        next if note =~ /[EADGBe|]/
        tab[bar[0]] << note
      end
    end

    ['E', 'A', 'D', 'G', 'B', 'e'].each do |string|
      tab[string].each_index do |i|
        @music[i] = '' unless @music[i]
        @music[i] += tab[string][i]
      end
    end

    @music
  end

end

if __FILE__ == $0
    tab = Tab.new('tabs/Em.tab')
    print "\"#{tab.parse.join('", "')}\"\n\n"
end

--END CODE--

Next we have the tabplayer program, which uses the Tab and Guitar
classes to create some music.

--BEGIN CODE--

#!/usr/local/bin/ruby -w

require 'lib/guitar'
require 'lib/tab'

Tab.new(ARGV[0]).parse

axe = Guitar.new
Tab.new(ARGV[0]).parse.each do |notes|
  axe.play(notes)
end
print axe.dump

--END CODE--

I have some test code that I used while developing the Tab class.

--BEGIN CODE--

#!/usr/local/bin/ruby -w

require 'test/unit'

require 'lib/tab'

class TC_Tab < Test::Unit::TestCase

  def setup
    test_construction
  end

  def test_construction
    @tab = Tab.new('tabs/Em.tab')

    assert_not_nil(@tab)
    assert_instance_of(Tab, @tab)
    assert_equal('tabs/Em.tab', @tab.file)
    assert_nil(@tab.music)
  end

  def test_read
    @tab.parse
    assert_equal( [ '------', '0-----', '------', '-2----', '------',
                    '--2---', '------', '---0--', '------', '----0-',
                    '------', '-----0'
                  ], @tab.music )

    @tab.parse('tabs/AmStrum.tab')
    assert_equal( [ '------', '-0----', '------', '--2---', '------',
                    '---2--', '------', '----1-', '------', '-----0',
                    '------', '------', '------', '-02210', '------',
                    '------', '------', '------', '------', '------',
                    '------', '------', '------', '------'
                  ], @tab.music )
  end

end

--END CODE--

Since I cannot attach a zip file from the Ruby Forum, I will just type
the directory structure. If you would like a zipfile of the code along
with the tabs, etc. just let me know. Thanks for an interesting quiz.

tab_player(folder)
|
|-tabplayer.rb
|
|-lib
| |-guitar.rb
| |-tab.rb
|
|-tabs
| |-*.tab files
|
|-test
  |-tc_tab.rb
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-05-17 17:25
(Received via mailing list)
This is the 'proof of concept' solution I wrote for this quiz. It just
uses a regexp approach to extract whatever tab it can find from the
input, and it has the limitations mentioned in the quiz (9 frets, etc).

#!/usr/local/bin/ruby
#
# This is a very simple solution to this quiz, using
# the simple software guitar supplied. Run e.g:
#
#   ./playtab.rb sometab.tab | timidity -Os -
#
# Using the correct -O (s is ALSA - see --help for more).
# Alternatively, file the output and load it in your
# midi player.
#
require 'guitar'

tabre = /(([eADGBE]\|?
         [\-0-9~xhpbrBend\(\)\[\]\{\}=*|#]+
         [.\r\n]*){6})/x
tabs = []

ARGF.read.scan(tabre) {
  tab = $1.split
  tabs << tab if tab.all? { |line| line.length == tab[0].length }
}

axe = Guitar.new(Guitar::CLEAN_ELECTRIC)
tabs.inject([[]]) do |bars,t|
  (t[0].length - 2).times do |i|
    notes = t.inject("") { |s,line| s << line[i+2] }.reverse
    if notes =~ /[|*]{6}/
      bars << []
    else
      bars.last << notes
    end
  end
  bars
end.reject { |a| a.empty? }.each do |bar|
  bar.each do |notes|
    axe.play(notes)
  end
end

$stdout.write(axe.dump)
Ddbfebb47432f6599da361df6a135c7c?d=identicon&s=25 Adam Shelly (Guest)
on 2006-05-17 18:53
(Received via mailing list)
Here's my short solution.  It just assumes any set of 6 equal-length
lines
starting with ('A'..'G') are a tab, and ignores the rest of the file.
It passes everything it finds on the tab lines to the guitar, since I
was
hoping to improve guitar.rb to handle hammers and slides and such..
Unfortunately, real life just takes too much time...

---begin player.rb  ------
require 'guitar.rb'

tuningMap = {
  'EADGBE' => Guitar::EADGBE,
  'DADGBE' => Guitar::DADGBE,
  'DGCFAD'=> Guitar::DGCFAD
}


puts "usage: Player.rb tabfile" or exit if ARGV.size < 1
lines,lastlength=[],nil
until ARGF.eof do
  lines << ARGF.gets.chomp.split('');
  #read until we find 6 lines of same length
  if lastlength and lastlength != lines[-1].length
    #throw away nonmatching lines
    lines.shift while lines.size > 1
  end
  lastlength = lines[-1].length
  if lines.size == 6
    sig = lines.inject([]){|a,l| a <<l.shift}
    #make sure it has a key signature
    if !sig.find{|e| !e or !(("A".."G").include?(e.upcase))}
      #create a guitar in the key of the first tab found.
      g ||= Guitar.new(Guitar::NYLON_ACOUSTIC,
                        tuningMap[sig.reverse.join.upcase])
      until (lines[0].empty?)
        note = lines.inject([]){|a,l| a << l.shift}
        if (note[0]!='|')
          p note.join if $DEBUG
          g.play(note.join)
        end
      end
    end
    lines.clear
  end
end
File.open("tab.mid","wb") {|f| f.write(g.dump)}


----
-Adam
Ce8b03e5750097942c58e12b46724312?d=identicon&s=25 Giles Bowkett (Guest)
on 2006-05-18 00:51
(Received via mailing list)
This is another proof of concept solution, I haven't been able to test
it. In fact it's really just a quick sketch. I really haven't done
much with Ruby, but one thing I've done which I'm reasonably happy
with has been code-generated music, so, for what it's worth, here we
go.




first you steal the Jim Weirich's generators code from Hal Fulton's
book "The Ruby Way."

then use Jim Menard's midilib.

then:

class Tab < Generator
  def generating_loop
    # stuff here which reads the tab file and returns a string and a
note
  end
end

class String(starting_note)
  :attr starting_note, track
  # each String starts on a particular MIDI note, each integer in tab
notation
  # represents fret numbers, both MIDI notes and fret numbers increase
in half
  # steps, therefore you simply add the tab note to the start note to
get the total
  # MIDI note
  def play(note)
    @track.events << NoteOnEvent.new(1, (starting_note + note), 127, 0)
    @track.events << NoteOffEvent.new(1, (starting_note + note), 0,
1000)
    # (the magic numbers are constants which would be altered in an
implementation that
    # cared about rhythm and dynamics. they govern volume, note length,
and MIDI
    # channel.)
  end
end

then init stuff standard to midilib:

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(120))

and then something like:

tab = Tab.new("filename.tab")

guitar [String.new(46, track),
          String.new(52, track),
          String.new(67, track)] # these aren't the right MIDI notes
                                          # for strings to start on,
but you get the idea.
                                          # also in real life the
guitar would have more
                                          # than three strings

while (tab.next) do |string, note|
  string.play(note)
end


in addition to the oddity of a three-stringed guitar, the fact that I
left out the file-reading code entirely, and any newbie syntax errors
-- all of which I apologize for -- there are two additional problems
with this solution: the first is that it's not properly set up to
accomodate chords, since it'll play simultaneous notes in series. the
other is that it outputs a MIDI file instead of making sound directly.
(MIDI is the protocol common to all digital music applications since
the late 70s, however, so turning MIDI files into sound is definitely
an easy thing to do.)
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-05-18 16:52
(Received via mailing list)
Putting aside the musical theme for a moment, this quiz boiled down to a
text processing problem, complicated slightly by the fact that tab
formats are actually quite variable, and often input will contain text
mixed with valid tab. This required the solutions to be liberal in what
they would accept, and conservative in their output - something that all
the solutions achieved in different ways. Let's look first at the Tab
class from Anthony Moralez's solution:

  class Tab
    def initialize(tab_file)
      @chords = []
      @file = tab_file
      @tab = extract_tabs(tab_file)
    end

    #select only lines containing tab notation and remove extraneous
chars
    def extract_tabs(file)
      File.readlines(file).select { |line|
        line =~ /[eBGDAE|-][|-]/
      }.collect { |line|
        line.gsub(/[eBGDAE|\s]/, '')
      }
    end

We can already see how Anthony is handling plain-text in the tab files -
lines are selected from the input using a regular expression, looking
for the string note at the start of a tab line, followed by tab
characters. Failing that, two consecutive -- are accepted. This is a
trade-off on Anthony's part: many tabs omit the string notes on some or
all of the tablature, and Anthony's solution will still pick that up.
However, it may also pick up some extraneous lines, such as message
headers and the like.

The selected lines are then massaged using gsub, to remove any
extraneous characters (those string notes, spaces, and bars).

Adam Shelly took a different approach. His code is based around a main
loop, which loops through the input file looking for runs of six
consecutive lines of equal length. Here's the first part of that loop:

  until ARGF.eof do
    lines << ARGF.gets.chomp.split('');
    #read until we find 6 lines of same length
    if lastlength and lastlength != lines[-1].length
      #throw away nonmatching lines
      lines.shift while lines.size > 1
    end
    lastlength = lines[-1].length

This is a clever way to approach the problem, and I think it will
reliably find most tabs in a given file. There are still potential false
positive matches (an ASCII table over six lines, for example) but those
are probably relatively few and far between. Here's the rest of the
loop, which converts the notes to the guitar's expected format, and does
the actual playing:

    if lines.size == 6
      sig = lines.inject([]){|a,l| a <<l.shift}
      #make sure it has a key signature
      if !sig.find{|e| !e or !(("A".."G").include?(e.upcase))}
        #create a guitar in the key of the first tab found.
        g ||= Guitar.new(Guitar::NYLON_ACOUSTIC,
                          tuningMap[sig.reverse.join.upcase])
        until (lines[0].empty?)
          note = lines.inject([]){|a,l| a << l.shift}
          if (note[0]!='|')
            p note.join if $DEBUG
            g.play(note.join)
          end
        end
      end
      lines.clear
    end
  end

This runs every time the buffer gets to six equal-length lines, and
handles both conversion and playing. Firstly, Adam shifts the first
character from each tab line and stores them in an array - this will be
the string note at the start of the line. These are then checked to make
sure they all fall within the valid range of notes (A to G), and if so
they are used to select a tuning for the guitar. Adam then steps through
the tab lines, using inject to shift the first character from each line,
and putting them together to make the notes passed into the guitar.

(Incidentally, Adam's solution seems to be designed for a left-handed
guitar, since it passes the notes to the guitar in reverse order).

These solutions, and most of the others, used the guitar provided with
the quiz, and as such are subject to the limitations mentioned in the
quiz. As it turned out, extending the guitar entailed more work than
first appeared, but Douglas Seifert had a go, removing that nine-fret
limitation and providing the ability to vary the note type that was
played. To achieve this, Douglas changed this:

  d = @seq.note_to_delta(@note)
  notes.split(//).each_with_index do |fret, channel|
    ...
  end

to (some whitespace and comments edited):

  md = /(\w):(.+)/.match(notes)
  notetype = @notes[md[1]]
  d = @seq.note_to_delta(notetype)
  md[2].split('|').each_with_index do |fret, channel|
    ...
  end

This changes the guitar's expected input format, but that's probably
unavoidable if we want to support more than single digit frets. Douglas
kindly explains the new format in his comments:

  # 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)

I don't want to get too technical here, but for those with no musical
background the different note types represent the duration of the note,
as a fraction of a beat. In real music different note values are often
mixed, so this addition potentially allows us to play a wider range of
music, given the extra information in the original tab.

Douglas' tab parsing code is very well commented and easy to follow, so
I recommend taking a look for the full lowdown, but let's focus here on
the section of code that handles those awkward two-digit fret numbers:

  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

The comments tell the story very well here, so I'll just wrap up by
drawing your attention to the way Douglas handles the varying tab style
used by different authors, with special cases to handle double-digits
aligned both left and right with the rest of the notes, and also the way
empty chords are tracked and used to select a duration for the following
note. This adds an interesting variability to the speed and style with
which tabs are played. Coupled with the support for double-digit fret
numbers, this gave a very nice sound, especially with the included tab
for Metallica's 'Nothing else matters'.

This summary could go on forever, with all the submitted solutions
providing some interesting points, but I'm aware that this is already
running rather long. I would definitely urge you take a look at the
solutions in full, and especially to run them and listen to their output
- every one of them has it's own sound, and one of the great things
about music is that it's never really 'right' or 'wrong'. Thanks
everyone who took the time to play with this!

Before I go, just a quick reminder that there will be no quiz tomorrow,
as James is still on Holiday (and hopefully having a fine time), so
maybe it'd be a good week to write up those Ruby Quiz suggestions you've
been meaning to get around to. Don't forget there's another copy of Best
Of Ruby Quiz up for grabs and trust me, it's definitely worth having
(mine arrived yesterday :)).
4299e35bacef054df40583da2d51edea?d=identicon&s=25 James Gray (bbazzarrakk)
on 2006-05-21 18:35
(Received via mailing list)
Thanks so much Ross.  Great summary!

James Edward Gray II
This topic is locked and can not be replied to.