mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 03:04:00 +00:00
590 lines
14 KiB
Ruby
Executable File
590 lines
14 KiB
Ruby
Executable File
#!/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<<repeat) - 1) & ~voltas
|
|
elsif !curVoltas
|
|
curVoltas = 1
|
|
while (voltas&curVoltas) != 0
|
|
curVoltas <<= 1
|
|
end
|
|
end
|
|
NOTES.push({'end-ending' => 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:
|