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, and a tutorial at guitar.about.com. You'll also find lots of (non-commercial use only) tabs at OLGA.
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.
Once that's done, you'll need to choose a guitar. This quiz includes a free guitar that supports the bare minimum you need to play simple tabs, with output to MIDI format (thanks to midilib). 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.
Quiz Summary
by Ross Bamford
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:
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:
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:
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:
notes.split(//).each_with_index do |fret, channel|
...
end
to (some whitespace and comments edited):
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:
#
# "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:
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 :)).