From: Ross Bamford Date: 2006-05-18T23:51:26+09:00 Subject: Re: [SUMMARY] Tab Player (#79) 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 < 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 :)).