#!/usr/bin/ruby # # VLLilypondType.reader - Import lilypond files # $KCODE = 'u' require File.dirname($0)+'/plistWriter' require File.dirname($0)+'/vl' OUTPUT = {'measures' => []} CHORDS = [] NOTES = [] STANZAS= [] MEAS = [] $RELPITCH = 0 $timeNum = 4 $timeDenom= 4 $key = 0 $mode = '\major' PITCH = { ?c => 0, ?d => 2, ?e => 4, ?f => 5, ?g => 7, ?a => 9, ?b => 11 } def lyPitch(pitch, base=-1) if !pitch || pitch =~ /^[rs]/ return VL::NoPitch end p = PITCH[pitch[0]] || 0 if base > -1 p += base elsif $RELPITCH > 0 while $RELPITCH-p > 5 if $RELPITCH-p == 6 # # f -> b choose upward interval, b -> f choose downward # break if p%12 == PITCH[?f] end p += 12 end else p += 48 end pitch.scan(/'/) {|c| p += 12} pitch.scan(/,/) {|c| p -= 12} puts "#{pitch}<>#{$RELPITCH} -> #{p}" if $DEBUG if base == -1 && $RELPITCH > 0 $RELPITCH = p end if pitch =~ /^[ea]s/ p -= 1 pitch[0..1] = "" end pitch.scan('is') { |x| p += 1 } pitch.scan('es') { |x| p -= 1 } return p end $timesNum = 1 $timesDen = 1 def lyDur(dur) dur =~ /^(\d+)(\.*)(?:\*(\d+).(\d+))?/ num = 1 den = $1.to_i if $2 (0...$2.length).each do |x| den = 2*den num = 2*num+1 end end if $3 num *= $3.to_i den *= $4.to_i end return [num*$timesNum,den*$timesDen] end STEPS = { '' => VL::Chord::Maj, 'm' => VL::Chord::Min, 'maj' => VL::Chord::Maj7, 'dim7' => VL::Chord::Dim7, 'dim' => VL::Chord::Dim, 'aug' => VL::Chord::Aug, 'sus4' => VL::Chord::Sus4, 'sus2' => VL::Chord::Sus2, 'sus' => VL::Chord::Sus4 } DEGREE = [ [VL::Unison, VL::Unison], [VL::Min2nd+VL::Maj2nd, VL::Maj2nd], [VL::Min3rd+VL::Maj3rd, VL::Maj3rd], [VL::Fourth, VL::Fourth], [VL::Fifth, VL::Fifth], [VL::Aug5th+VL::Dim7th, VL::Dim7th], [VL::Min7th+VL::Maj7th, VL::Min7th], [VL::Octave, VL::Octave], [VL::Min9th+VL::Maj9th, VL::Maj9th], [VL::Aug9th+VL::Dim11th, VL::Dim11th], [VL::Eleventh, VL::Eleventh], [VL::Aug11th+VL::Dim13th, VL::Dim13th], [VL::Min13th+VL::Maj13th, VL::Maj13th] ]; MAJORKEY = [ 0, # C -5, # Db 2, # D -3, # Eb 4, # E -1, # F -6, # Gb 1, # G -4, # Ab 3, # A -2, # Bb 5, # B ]; MINORKEY = [ -3, # Cm -> Eb 4, # Dbm -> E -1, # Dm -> F -6, # Ebm -> Gb 1, # Em -> G -4, # Fm -> Ab 3, # F#m -> A -2, # Gm -> Bb 5, # G#m -> B 0, # Am -> C -5, # Bbm -> Db 2, # Bm -> D ]; def lySteps(steps) steps =~ /^(maj|dim7?|aug|sus[42]?|m|)/ s = STEPS[$1] steps = $' if !($1 =~ /\d$/) && steps =~ /^(7|9|11|13)/ if (s & VL::Maj7th) == 0 s |= VL::Min7th end case $1 when '9' s |= VL::Maj9th when '11' s |= VL::Maj9th+VL::Eleventh when '13' s |= VL::Maj9th+VL::Eleventh+VL::Maj13th end steps = $' end steps.scan(/(\^)?(\d+)([-+])?/) do |ext| degree = DEGREE[$2.to_i-1] if $1 == '^' s &= ~degree[0] else step = degree[1] if $3 == '+' step <<= 1 elsif $3 == '-' step >>= 1 end s = (s & ~degree[0]) | step end end return s end def parseLilypond # # Lex # tokens = [] INFILE.each do |line| line.chomp!.sub!(/%.*/, "") line.gsub!(/\\breve/, "1*8/4") line.scan(%r$\G\s*(\{|\}|\(|\)|\||=|~|<<|>>|--|#'|#\(|##t|##f|\\\w+|\".*?\"|(\w|'|`)[-+^\w\d.'`,:*/?!]+|.)$) do |token| tokens.push(token[0]) end end # # Parse # nestLevel = 0 block = nil level = -1 stack = [] repeats = [] lyrics = [] lastDur = 1 tied = false repeat = 0 lyricFlags= 0 slur = false while tokens.length > 0 token = tokens.shift # # Title, composer, etc. # if tokens[0] == '=' case token when 'title','composer','poet' key = token=='poet' ? 'lyricist' : token value = tokens[1] value.sub!(/"(.*)"/, '\1') OUTPUT[key] = value tokens[0..1]= nil redo end end case block when '\header', '\paper' # Ignore when '\chords', '\chordmode' # # Possibly chords # if token.downcase =~ %r{^ ([rs] | # Rest [a-g](?:[ei]?s)? # g, ges, fis, es, as ) (\d+ # 1, 2, 4, 8, 16 ... \.*(?:\*\d+/\d+)? # ., *3/4 )? (?:\:([-+^:.a-z\d]*))? # :maj9.7-^2 (?:/\+?( # /+ [a-g](?:[ei]?s)? # Root: a, bes, fis, as ))? $}x pitch = lyPitch($1, 60) dur = $2 || lastDur ext = $3 ? lySteps($3) : 0 root = lyPitch($4, 48) lastDur = dur d = lyDur(dur) chord = {'pitch' => pitch, 'root' => root, 'steps' => ext, 'durNum'=> d[0], 'durDenom' => d[1]} p token, chord if $DEBUG CHORDS.push(chord) redo end when 'voice' # # Possibly notes # if token.downcase =~ %r{^ ([rs] | # Rest [a-g](?:[ei]?s)? # g, ges, fis, es, as [',]* # g''' ) (\d+\.* # 1, 2, 4, 8, 16 ... (?:\*\d+/\d+)? # *3/4 )? $}x pitch = lyPitch($1) dur = $2 || lastDur lastDur = dur d = lyDur(dur) if slur # # We don't support slurs, so we turn them into tied notes at the # final pitch # ix = NOTES.size tie= true while tie do break if ix == 0 note = NOTES[ix -= 1] note['pitch'] = pitch; note['tied'] ||= 0 note['tied'] |= VL::TiedWithNext tie = (note['tied'] & VL::TiedWithPrev) != 0 end tied = true end note = {'pitch' => pitch, 'durNum'=> d[0], 'durDenom' => d[1]} note['tied'] = VL::TiedWithPrev if tied p token, note if $DEBUG tied = false NOTES.push(note) redo elsif token == '~' if note = NOTES.last note['tied'] ||= 0 note['tied'] |= VL::TiedWithNext end tied = true elsif token == '(' slur = true elsif token == ')' slur = false elsif token == '\repeat' && (tokens[0] == 'volta' || tokens[0] == fold) && tokens[1] =~ /^\d+$/ stack.push([block, level, "repeat"]) level = nestLevel repeats.push(repeat) repeat = tokens[1].to_i NOTES.push({'begin-repeat' => true, 'times' => repeat}) tokens[0..1] = nil redo elsif token == '\alternative' inEndings = true stack.push([block, level, "endings"]) level = nestLevel+1 voltas = 0 curVoltas = nil NOTES.push({'begin-ending' => true}) elsif token == '\times' && tokens[0] =~ %r|^(\d+)/(\d+)| $timesNum = $1.to_i $timesDen = $2.to_i stack.push([block, level, "times"]) level = nestLevel end when '\lyricmode' if token == '--' lyrics.last[1] |= VL::TiedWithNext if lyrics.size > 0 lyricFlags = VL::TiedWithPrev elsif token == '\skip' p ["", 0] if $DEBUG lyrics.push ["", 0] lyricFlags = 0 if tokens[0] =~ /\d+/ tokens[0..0] = nil end elsif token =~ /\\skip\d+/ p ["", 0] if $DEBUG lyrics.push ["", 0] lyricFlags = 0 elsif token =~ /"(.*)"/ p [$1, lyricFlags] if $DEBUG lyrics.push [$1, lyricFlags] lyricFlags = 0 elsif token =~ /^(\w|'|`).*/ # # Handle smart quotes # token.gsub!(/``/, "\xE2\x80\x9C"); token.gsub!(/''/, "\xE2\x80\x9D"); token.gsub!(/'/, "\xE2\x80\x99"); p [token, lyricFlags] if $DEBUG lyrics.push [token, lyricFlags] lyricFlags = 0 end end # # Nesting levels # case token when '{', '<<' nestLevel += 1 when '}', '>>' nestLevel -= 1 if nestLevel <= level if lv = stack.pop block = lv[0] level = lv[1] type = lv[2] else block = nil level = -1 end if type == "repeat" if tokens[0] != '\alternative' NOTES.push({'end-repeat' => true}) repeat = repeats.pop end elsif type == "endings" last = tokens[0] == '}' if last curVoltas = ((1< true, 'volta' => curVoltas, 'last'=>last}) voltas |= curVoltas curVoltas = 0 if last repeat = repeats.pop else NOTES.push({'begin-ending' => true}) stack.push([block, level, "endings"]) level = nestLevel end elsif type == "times" $timesNum = 1 $timesDen = 1 end end when '\chords', '\header', '\paper', '\lyricmode' stack.push([block, level, ""]) block = token level = nestLevel STANZAS.push(lyrics= []) if block == '\lyricmode' when '\chordmode' stack.push([block, level, ""]) block = '\chords' level = nestLevel when '\lyricsto' tokens[0] = nil when '\new' if tokens[0] == "Lyrics" if tokens[1] =~ /^\\/ tokens[0..1] = nil else stack.push([block, level, ""]) block = '\lyricmode' level = nestLevel STANZAS.push(lyrics= []) tokens[0..0] = nil end end when '\relative' stack.push([block, level, ""]) if tokens[0] =~ /[a-g](?:[ei]?s)?[',]*/ $RELPITCH = lyPitch(tokens[0], 48) tokens[0..0] = nil else $RELPITCH = 60 end block = 'voice' level = nestLevel when '\time' if tokens[0] =~ %r{(\d+)/(\d+)} $timeNum = $1.to_i $timeDenom = $2.to_i tokens[0..0] = nil end if block != 'voice' stack.push([block, level, ""]) block = 'voice' level = nestLevel-1 end when '\key' p = lyPitch(tokens[0], 0) $mode = tokens[1] $key = $mode == '\minor' ? MINORKEY[p] : MAJORKEY[p] tokens[0..1] = nil if block != 'voice' stack.push([block, level, ""]) block = 'voice' level = nestLevel-1 end when '\repeat' tokens[0..1] = nil when '\alternative' end end end def peek(where, what) return where.first && where.first[what] end def makeMeasures measureLen = VL::Fract.new($timeNum, $timeDenom) # # Make measures # measCount= -1 while NOTES.size > 0 || CHORDS.size > 0 measCount += 1 meas = {} meas['measure'] = measCount meas['properties'] = 0 if peek(NOTES, 'begin-repeat') rep = NOTES.shift meas['begin-repeat'] = {'times' => rep['times']} end if peek(NOTES, 'begin-ending') NOTES.shift meas['begin-ending'] = {} end if CHORDS.size > 0 mchords = [] len = VL::Fract.new(0, 1) while len < measureLen && CHORDS.size > 0 chord = CHORDS.shift chordLen = VL::Fract.new(chord['durNum'], chord['durDenom']) if len+chordLen > measureLen remLen = len+chordLen-measureLen chordLen -= remLen remChord = { 'pitch' => VL::NoPitch, 'root' => VL::NoPitch, 'durNum' => remLen.num, 'durDenom' => remLen.denom} CHORDS.unshift(remChord) end mchords.push(chord) len += chordLen end meas['chords'] = mchords end if NOTES.size > 0 mnotes = [] len = VL::Fract.new(0, 1) while len < measureLen && NOTES.size > 0 note = NOTES.shift noteLen = VL::Fract.new(note['durNum'], note['durDenom']) if len+noteLen > measureLen remLen = len+noteLen-measureLen noteLen -= remLen remNote = note.dup remNote['durNum'] = remLen.num remNote['durDenom'] = remLen.denom remNote['tied'] = (remNote['tied'] || 0) | VL::TiedWithPrev note['tied'] = (note['tied'] || 0) | VL::TiedWithNext NOTES.unshift(remNote) end if note['pitch'] != VL::NoPitch && (!note['tied'] || (note['tied'] & VL::TiedWithPrev) == 0) ly = [] stanza = 0 STANZAS.each_index do |i| lyrics = STANZAS[i] if lyrics.size > 0 stanza = i+1 syll = lyrics.shift ly.push({'text' => syll[0].gsub('_', ' '), 'kind' => syll[1]}) else ly.push({'text' => '', 'kind' => 0}) end end if stanza < ly.size ly[stanza..-1] = nil end note['lyrics'] = ly if stanza > 0 end mnotes.push(note) len += noteLen end meas['melody'] = mnotes end if peek(NOTES, 'end-ending') ending = NOTES.shift meas['end-ending'] = {'last' => ending['last'], 'volta' => ending['volta']} end if peek(NOTES, 'end-repeat') NOTES.shift meas['end-repeat'] = {} end MEAS.push(meas) end end begin parseLilypond makeMeasures OUTPUT['measures'] = MEAS OUTPUT['properties'] = [{ 'key' => $key, 'mode' => $mode == '\minor' ? -1 : 1, 'timeNum' => $timeNum, 'timeDenom' => $timeDenom }] writePlist($stdout, OUTPUT) rescue => except $stderr.print except.message, "\n", except.backtrace.join("\n"), "\n" end # Local Variables: # mode:ruby # End: