--Apple-Mail-1--124867675 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=US-ASCII; delsp=yes; format=flowed Begin forwarded message: > From: "Douglas A. Seifert" <doug / dseifert.net> > Date: May 16, 2006 12:36:12 AM CDT > To: submission / rubyquiz.com > Subject: Please Forward: Ruby Quiz Submission > > I attempted to write a parser that would preserve as much of the > tablature in an arbitrary input file as possible. It's a mess as a > result because I found so many special cases. My regex for > matching tablature lines could use help ... > > Modified guitar to "handle" tempo and more than 9 frets... > > My first quiz attempt, I'm sure my life as a Java programmer is > evident in this code ... not very "Ruby Way" ... > > For usage: > > # ./play.rb -h > > > --Apple-Mail-1--124867675 Content-Transfer-Encoding: 7bit Content-Type: application/x-ruby; x-unix-mode=0666; name="play.rb" Content-Disposition: attachment; filename=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) --Apple-Mail-1--124867675 Content-Transfer-Encoding: 7bit Content-Type: application/x-ruby; x-unix-mode=0666; name="tab.rb" Content-Disposition: attachment; filename=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 --Apple-Mail-1--124867675 Content-Transfer-Encoding: 7bit Content-Type: application/x-ruby; x-unix-mode=0666; name="guitar.rb" Content-Disposition: attachment; filename=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 --Apple-Mail-1--124867675 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=US-ASCII; format=flowed --Apple-Mail-1--124867675--