Properly read and write harmony info

This commit is contained in:
Matthias Neeracher 2007-09-04 17:26:37 +00:00
parent 0cc5fb11b4
commit 7d8a579907
2 changed files with 267 additions and 120 deletions

View File

@ -108,39 +108,40 @@ class MusicXMLListener
@kind = nil # Ignore all tags not recognized here @kind = nil # Ignore all tags not recognized here
@text = "" @text = ""
case tag case tag
when 'score-timewise' then when 'score-timewise'
$stderr.puts "Can't read timewise MusicXML files yet" $stderr.puts "Can't read timewise MusicXML files yet"
exit 1 exit 1
when 'work-title' then when 'work-title'
@kind = 'textProp' @kind = 'textProp'
@key = 'title' @key = 'title'
when 'creator' then when 'creator'
case attrs['type'] case attrs['type']
when 'composer' then when 'composer'
@kind = 'textProp' @kind = 'textProp'
@key = 'composer' @key = 'composer'
when 'poet' then when 'poet'
@kind = 'textProp' @kind = 'textProp'
@key = 'lyricist' @key = 'lyricist'
end end
when 'miscellaneous-field' then when 'miscellaneous-field'
if attrs['name'] == 'VocalEasel-groove' if attrs['name'] == 'VocalEasel-groove'
@kind = 'textProp' @kind = 'textProp'
@key = 'groove' @key = 'groove'
end end
when 'encoding-date' then when 'encoding-date'
@kind = 'dateProp' @kind = 'dateProp'
@key = 'saved' @key = 'saved'
when 'software' then when 'software'
@kind = 'textProp' @kind = 'textProp'
@key = 'software' @key = 'software'
when 'part' then when 'part'
@part = attrs['id'] || "" @part = attrs['id'] || ""
@prop = {} @prop = {}
@props = [] @props = []
@measNo = -1 @measNo = -1
when 'measure' then when 'measure'
@notes= [] @notes= []
@note = nil
@harm = [] @harm = []
@chord= false @chord= false
@at = 0 @at = 0
@ -157,20 +158,20 @@ class MusicXMLListener
'melody' => [] 'melody' => []
} }
end end
when 'barline' then when 'barline'
@times = nil @times = nil
@type = nil @type = nil
@number = "" @number = ""
when 'repeat' then when 'repeat'
if attrs['direction'] == 'backward' && attrs['times'] if attrs['direction'] == 'backward' && attrs['times']
@times = attrs['times'].to_i @times = attrs['times'].to_i
elsif attrs['direction'] == 'forward' elsif attrs['direction'] == 'forward'
@times = 0 @times = 0
end end
when 'ending' then when 'ending'
@type = attrs['type'] @type = attrs['type']
@number = attrs['number'] @number = attrs['number']
when 'sound' then when 'sound'
if attrs['tocoda'] if attrs['tocoda']
@meas['tocoda'] = true @meas['tocoda'] = true
elsif attrs['coda'] elsif attrs['coda']
@ -179,47 +180,46 @@ class MusicXMLListener
if attrs['tempo'] if attrs['tempo']
OUTPUT['tempo'] = attrs['tempo'].to_i OUTPUT['tempo'] = attrs['tempo'].to_i
end end
when 'divisions' then when 'divisions'
@kind = 'prop' @kind = 'prop'
@key = 'divisions' @key = 'divisions'
when 'fifths' then when 'fifths'
@kind = 'prop' @kind = 'prop'
@key = 'key' @key = 'key'
when 'beats' then when 'beats'
@kind = 'prop' @kind = 'prop'
@key = 'timeNum' @key = 'timeNum'
when 'beat-type' then when 'beat-type'
@kind = 'prop' @kind = 'prop'
@key = 'timeDenom' @key = 'timeDenom'
when 'harmony' then when 'harmony'
@note = { 'pitch' => VL::NoPitch, 'steps' => 0, 'root' => VL::NoPitch, @note = { 'pitch' => VL::NoPitch, 'steps' => 0, 'root' => VL::NoPitch,
'at' => @at } 'at' => @at }
when 'degree' then when 'degree'
@degree_value = 0 @degree_value = 0
@degree_alter = 0 @degree_alter = 0
@degree_type = "alter" @degree_type = "alter"
when 'note' then when 'note'
@note = { 'pitch' => 0, 'durNum' => 0, 'durDenom' => 0 } @note = { 'pitch' => 0, 'durNum' => 0, 'durDenom' => 0 }
when 'rest' then when 'rest'
@note['pitch'] = VL::NoPitch @note['pitch'] = VL::NoPitch
when 'mode', 'step', 'alter', 'octave', 'duration', 'syllabic', 'text', when 'mode', 'step', 'alter', 'octave', 'duration', 'syllabic', 'text',
'root-step', 'root-alter', 'bass-step', 'bass-alter', 'kind', 'root-step', 'root-alter', 'bass-step', 'bass-alter', 'kind',
'degree-value', 'degree-alter', 'degree-type' 'degree-value', 'degree-alter', 'degree-type'
then
@kind = tag @kind = tag
when 'tie' then when 'tie'
@note['tied'] ||= 0 @note['tied'] ||= 0
case attrs['type'] case attrs['type']
when 'start' then when 'start'
@note['tied'] |= VL::TiedWithNext @note['tied'] |= VL::TiedWithNext
when 'stop' then when 'stop'
@note['tied'] |= VL::TiedWithPrev @note['tied'] |= VL::TiedWithPrev
end end
when 'lyric' then when 'lyric'
num = (attrs['number'] || "1").to_i-1 num = (attrs['number'] || "1").to_i-1
@note['lyrics'] ||= [] @note['lyrics'] ||= []
@note['lyrics'][num] = @lyric = {} @note['lyrics'][num] = @lyric = {}
when 'chord' then when 'chord'
@chord = true @chord = true
end end
end end
@ -283,44 +283,48 @@ class MusicXMLListener
# #
if @kind if @kind
case @kind case @kind
when 'textProp' then when 'textProp'
OUTPUT[@key] = @text OUTPUT[@key] = @text
when 'dateProp' then when 'dateProp'
begin begin
OUTPUT[@key] = Time.parse(@text) OUTPUT[@key] = Time.parse(@text)
rescue rescue
OUTPUT[@key] = Time.now OUTPUT[@key] = Time.now
end end
when 'prop' then when 'prop'
@prop[@key] = @text.to_i @prop[@key] = @text.to_i
when 'mode' then when 'mode'
@prop['mode'] = @text == 'minor' ? -1 : 1 @prop['mode'] = @text == 'minor' ? -1 : 1
when 'step' then when 'step'
@note['pitch'] += PITCH[@text] @note['pitch'] += PITCH[@text]
when 'alter', 'root-alter' then when 'alter', 'root-alter'
@note['pitch'] += @text.to_i @note['pitch'] += @text.to_i
when 'octave' then when 'octave'
@note['pitch'] += (@text.to_i+1)*12 @note['pitch'] += (@text.to_i+1)*12
when 'duration' then when 'duration'
@note['durNum'] = @text.to_i if @note
@note['durDenom'] = @prop['divisions']*4 @note['durNum'] = @text.to_i
when 'root-step' then @note['durDenom'] = @prop['divisions']*4
else
@duration = @text.to_i
end
when 'root-step'
@note['pitch'] = PITCH[@text] @note['pitch'] = PITCH[@text]
when 'bass-step' then when 'bass-step'
@note['root'] = PITCH[@text] @note['root'] = PITCH[@text]
when 'bass-alter' then when 'bass-alter'
@note['root'] += @text.to_i @note['root'] += @text.to_i
when 'kind' then when 'kind'
@note['steps'] = CHORD[@text] @note['steps'] = CHORD[@text]
when 'degree-value' then when 'degree-value'
@degree_value = @text.to_i-1 @degree_value = @text.to_i-1
when 'degree-alter' then when 'degree-alter'
@degree_alter = @text.to_i @degree_alter = @text.to_i
when 'degree-type' then when 'degree-type'
@degree_type = @text @degree_type = @text
when 'syllabic' then when 'syllabic'
@lyric['kind'] = SYLL[@text] @lyric['kind'] = SYLL[@text]
when 'text' then when 'text'
@lyric['text'] = @text @lyric['text'] = @text
end end
@kind = nil @kind = nil
@ -329,11 +333,11 @@ class MusicXMLListener
# Structures are distinguished by tag # Structures are distinguished by tag
# #
case tag case tag
when 'attributes' then when 'attributes'
if @prop != @props.last if @prop != @props.last
@props.push(@prop) @props.push(@prop)
end end
when 'note' then when 'note'
if @chord if @chord
note = @notes.last note = @notes.last
note['pitch'] = [*note['pitch']] << @note['pitch'] note['pitch'] = [*note['pitch']] << @note['pitch']
@ -342,13 +346,18 @@ class MusicXMLListener
@notes.push(@note) @notes.push(@note)
@at += @note['durNum'] @at += @note['durNum']
end end
when 'degree' then @note = nil
when 'forward'
@at += @duration
when 'backup'
@at -= @duration
when 'degree'
oldSteps = @note['steps'] oldSteps = @note['steps']
oldValue = @degree_value oldValue = @degree_value
case @degree_type case @degree_type
when 'subtract' then when 'subtract'
@note['steps'] &= ~DEGREE[@degree_value][0] @note['steps'] &= ~DEGREE[@degree_value][0]
when 'add' then when 'add'
@degree_value = DEGREE[@degree_value][1] @degree_value = DEGREE[@degree_value][1]
if @degree_alter < 0 if @degree_alter < 0
@degree_value >>= -@degree_alter @degree_value >>= -@degree_alter
@ -356,7 +365,7 @@ class MusicXMLListener
@degree_value <<= @degree_alter @degree_value <<= @degree_alter
end end
@note['steps'] |= @degree_value @note['steps'] |= @degree_value
when 'alter' then when 'alter'
@degree_value = @note['steps'] & DEGREE[@degree_value][0] @degree_value = @note['steps'] & DEGREE[@degree_value][0]
@note['steps'] ^= @degree_value @note['steps'] ^= @degree_value
if @degree_alter < 0 if @degree_alter < 0
@ -366,16 +375,16 @@ class MusicXMLListener
end end
@note['steps'] |= @degree_value @note['steps'] |= @degree_value
end end
# $stderr.puts "#{@degree_type} #{oldValue} #{@degree_alter} (#{oldSteps} -> #{@note['steps']})" when 'harmony'
when 'harmony' then
@harm.push(@note) @harm.push(@note)
when 'barline' then @note = nil
when 'barline'
case @type case @type
when 'start' then when 'start'
@meas['begin-ending'] = { @meas['begin-ending'] = {
'volta' => makeVolta(@number) 'volta' => makeVolta(@number)
} }
when 'stop', 'discontinue' then when 'stop', 'discontinue'
@meas['end-ending'] = { @meas['end-ending'] = {
'volta' => makeVolta(@number), 'volta' => makeVolta(@number),
'last' => @type == 'discontinue' 'last' => @type == 'discontinue'
@ -391,7 +400,7 @@ class MusicXMLListener
end end
end end
end end
when 'measure' then when 'measure'
@meas['properties'] = @props.length-1 unless @props.empty? @meas['properties'] = @props.length-1 unless @props.empty?
if @part == 'HARM' if @part == 'HARM'
@meas['chords'] = makeChords(@notes) @meas['chords'] = makeChords(@notes)
@ -416,7 +425,7 @@ class MusicXMLListener
end end
end end
end end
when 'part' then when 'part'
OUTPUT['properties'] = @props unless @part == 'HARM' OUTPUT['properties'] = @props unless @part == 'HARM'
end end
end end

View File

@ -50,13 +50,9 @@ end
def _part_list def _part_list
part_list = REXML::Element.new('part-list') part_list = REXML::Element.new('part-list')
chords = REXML::Element.new('score-part')
chords.add_attribute('id', 'HARM')
chords.add_element newTextElement('part-name', 'Chords')
part_list.add_element(chords)
melody = REXML::Element.new('score-part') melody = REXML::Element.new('score-part')
melody.add_attribute('id', 'MELO') melody.add_attribute('id', 'MELO')
melody.add_element newTextElement('part-name', 'Melody') melody.add_element newTextElement('part-name', 'Song')
part_list.add_element(melody) part_list.add_element(melody)
return part_list return part_list
@ -85,33 +81,43 @@ end
STEPS = 'C DbD EbE F GbG AbA BbB ' STEPS = 'C DbD EbE F GbG AbA BbB '
def _pitch(name, pitch, prefix="")
oct = pitch/12 - 1
stp = 2*(pitch%12)
step = STEPS[stp]
alt = STEPS[stp+1] == ?b
if alt
if $USE_FLATS
alt = -1
else
step = step == ?A ? ?G : step-1
alt = 1
end
end
if prefix.length > 0
prefix += "-"
end
pitch= REXML::Element.new(name)
pitch.add_element newTextElement(prefix+'step', step.chr)
if alt
pitch.add_element newTextElement(prefix+'alter', alt)
end
if prefix.length == 0
pitch.add_element newTextElement('octave', oct)
end
return pitch
end
def _note(pitch, dur, tied=0) def _note(pitch, dur, tied=0)
note = REXML::Element.new('note') note = REXML::Element.new('note')
if pitch == VL::NoPitch if pitch == VL::NoPitch
note.add_element(REXML::Element.new('rest')) note.add_element(REXML::Element.new('rest'))
else else
oct = pitch/12 - 1
stp = 2*(pitch%12)
step = STEPS[stp]
alt = STEPS[stp+1] == ?b
if alt
if $USE_FLATS
alt = -1
else
step = step == ?A ? ?G : step-1
alt = 1
end
end
if (tied & VL::InChord) != 0 if (tied & VL::InChord) != 0
note.add_element 'chord' note.add_element 'chord'
end end
pitch= REXML::Element.new('pitch') note.add_element(_pitch('pitch', pitch))
pitch.add_element newTextElement('step', step.chr)
if alt
pitch.add_element newTextElement('alter', alt)
end
pitch.add_element newTextElement('octave', oct)
note.add_element(pitch)
end end
note.add_element newTextElement('duration', dur) note.add_element newTextElement('duration', dur)
if (tied & VL::TiedWithPrev) != 0 if (tied & VL::TiedWithPrev) != 0
@ -125,44 +131,151 @@ def _note(pitch, dur, tied=0)
return note return note
end end
def _chords CHORD = {
chords = REXML::Element.new('part') #
chords.add_attribute('id', 'HARM') # Triads
#
'major' => VL::Chord::Maj,
'minor' => VL::Chord::Min,
'augmented' => VL::Chord::Aug,
'diminished' => VL::Chord::Dim,
#
# 7ths
#
'dominant' => VL::Chord::Dom7,
'major-seventh' => VL::Chord::Maj7,
'minor-seventh' => VL::Chord::Min7,
'diminished-seventh' => VL::Chord::Dim7,
'augmented-seventh' => VL::Chord::Aug7,
'half-diminished' => VL::Chord::M7b5,
'major-minor' => VL::Chord::MMin7,
#
# 6ths
#
'major-sixth' => VL::Chord::Maj6,
'minor-sixth' => VL::Chord::Min6,
#
# 9ths
#
'dominant-ninth' => VL::Chord::Dom9,
'major-ninth' => VL::Chord::Maj9,
'minor-ninth' => VL::Chord::Min9,
#
# 11ths
#
'dominant-11th' => VL::Chord::Dom11,
'major-11th' => VL::Chord::Maj11,
'minor-11th' => VL::Chord::Min11,
#
# 13ths
#
'dominant-13th' => VL::Chord::Dom13,
'major-13th' => VL::Chord::Maj13,
'minor-13th' => VL::Chord::Min13,
#
# Suspended
#
'suspended-second' => VL::Chord::Sus2,
'suspended-fourth' => VL::Chord::Sus4,
#
# Varia
#
'other' => VL::Unison,
'none' => 0
}
lastProp = -1 #
measNum = 0 # This list differs a bit from the list in .reader, as we prefer
INPUT['measures'].each do |meas| # certain degrees, i.e. #9 rather than b10
measNum += 1 #
m = REXML::Element.new('measure') DEGREE = [
m.add_attribute('number', measNum.to_s); [VL::Unison, VL::Unison],
if meas['properties'] != lastProp [VL::Min2nd+VL::Maj2nd, VL::Maj2nd],
lastProp = meas['properties'] [VL::Min3rd+VL::Maj3rd, VL::Maj3rd],
m.add_element(_attributes(INPUT['properties'][lastProp])) [VL::Fourth, VL::Fourth],
[VL::Dim5th+VL::Fifth+VL::Aug5th, VL::Fifth],
[VL::Dim7th, VL::Dim7th],
[VL::Min7th+VL::Maj7th, VL::Min7th],
[VL::Octave, VL::Octave],
[VL::Min9th+VL::Maj9th+VL::Aug9th, VL::Maj9th],
[0, VL::Dim11th],
[VL::Dim11th+VL::Eleventh+VL::Aug11th, VL::Eleventh],
[0, VL::Dim13th],
[VL::Dim13th+VL::Min13th+VL::Maj13th, VL::Maj13th]
];
def _chord(pitch, steps, root)
#
# Pick kind. sus takes precedence
#
if (steps & (VL::Min3rd|VL::Maj3rd)) == 0 and (steps & (VL::Maj2nd|VL::Fourth))!= 0
if (steps & VL::Fourth) != 0
kind = 'suspended-fourth'
else
kind = 'suspended-second'
end end
meas['chords'].each do |chord| else
dur = (chord['durNum'] * $DIVISIONS * 4) / chord['durDenom'] #
if chord['pitch'] == VL::NoPitch # It's hard to score alterations properly, so rank purely by correct steps
m.add_element _note(VL::NoPitch, dur) #
else best = 0
seenNote = 0 kind = 'none'
if chord['root'] != VL::NoPitch CHORD.each do |k,mask|
m.add_element _note(chord['root'], dur) mask &= steps
seenNote = VL::InChord score = 0
end while mask > 0
pitch = chord['pitch'] score += 1
steps = chord['steps'] mask &= mask-1
(0..25).each do |step| end
if (steps & (1<<step)) != 0 if score > best
m.add_element _note(pitch+step, dur, seenNote) kind = k
seenNote = VL::InChord best = score
end end
end end
end
harm = REXML::Element.new('harmony')
harm.add_element(_pitch('root', pitch, 'root'))
harm.add_element newTextElement('kind', kind)
if root != VL::NoPitch
harm.add_element(_pitch('bass', root, 'bass'))
end
needSteps = steps & ~CHORD[kind]
extraSteps= CHORD[kind] & ~steps
if (needSteps+extraSteps) > 0
DEGREE.each_index do |deg|
mask = DEGREE[deg][0]
type = nil
need = needSteps & mask
extra= extraSteps & mask
if need != 0
if extra != 0
type = 'alter'
alter= extra > need ? -1 : 1
else
type = 'add'
step = DEGREE[deg][1]
alter= step > need ? -1 : (step < need ? 1 : 0)
end
elsif extra != 0
type = 'subtract'
alter= 0
end
if type
degree = REXML::Element.new('degree')
degree.add_element newTextElement('degree-value', (deg+1).to_s)
degree.add_element newTextElement('degree-alter', alter.to_s)
degree.add_element newTextElement('degree-type', type)
harm.add_element(degree)
needSteps &= ~need
extraSteps &= ~extra
break if (needSteps+extraSteps) == 0
end end
end end
chords.add_element(m)
end end
return chords return harm
end end
SYLLABLE = %w[single begin end middle] SYLLABLE = %w[single begin end middle]
@ -217,8 +330,33 @@ def _melody
barline.add_element 'ending', {'type' => 'start', 'number' => num} barline.add_element 'ending', {'type' => 'start', 'number' => num}
m.add_element(barline) m.add_element(barline)
end end
noteAt = 0
chordAt= 0
chords = meas['chords']
chordIx= 0
meas['melody'].each do |note| meas['melody'].each do |note|
dur = (note['durNum'] * $DIVISIONS * 4) / note['durDenom'] dur = (note['durNum'] * $DIVISIONS * 4) / note['durDenom']
tempAt = noteAt
while chordIx < chords.length && chordAt < noteAt+dur
chord = chords[chordIx]
if chord['pitch'] != VL::NoPitch
if chordAt > tempAt
fw = REXML::Element.new('forward')
fw.add_element newTextElement('duration', (chordAt-tempAt).to_s)
m.add_element(fw)
tempAt = chordAt
end
m.add_element(_chord(chord['pitch'], chord['steps'], chord['root']))
end
chordAt += (chord['durNum'] * $DIVISIONS * 4) / chord['durDenom']
chordIx += 1
end
if tempAt > noteAt
bk = REXML::Element.new('backup')
bk.add_element newTextElement('duration', (tempAt-noteAt).to_s)
m.add_element(bk)
tempAt = noteAt
end
n = _note(note['pitch'], dur, note['tied'] || 0) n = _note(note['pitch'], dur, note['tied'] || 0)
stanza = 1 stanza = 1
note['lyrics'].each do |syll| note['lyrics'].each do |syll|
@ -231,6 +369,7 @@ def _melody
end end
stanza += 1 stanza += 1
end if note['lyrics'] end if note['lyrics']
noteAt += dur
m.add_element(n) m.add_element(n)
end end
if r = meas['end-repeat'] if r = meas['end-repeat']
@ -246,7 +385,6 @@ def _melody
barline.add_attribute('location', 'right') barline.add_attribute('location', 'right')
barline.add_element newTextElement('bar-style', barline.add_element newTextElement('bar-style',
(repeat[measNum+1] & 1) != 0 ? 'heavy-heavy' : 'light-heavy') (repeat[measNum+1] & 1) != 0 ? 'heavy-heavy' : 'light-heavy')
barline.add_element 'repeat', {'direction' => 'backward'}
volta = ending['volta'] volta = ending['volta']
num = nil num = nil
(0..7).each do |i| (0..7).each do |i|
@ -260,6 +398,7 @@ def _melody
end end
type = ending['last'] ? "discontinue" : "stop" type = ending['last'] ? "discontinue" : "stop"
barline.add_element 'ending', {'type', type, 'number', num} barline.add_element 'ending', {'type', type, 'number', num}
barline.add_element 'repeat', {'direction' => 'backward'}
m.add_element(barline) m.add_element(barline)
end end
if meas['tocoda'] if meas['tocoda']
@ -277,7 +416,6 @@ def _score
score.add_element(_work) score.add_element(_work)
score.add_element(_identification) score.add_element(_identification)
score.add_element(_part_list) score.add_element(_part_list)
score.add_element(_chords)
score.add_element(_melody) score.add_element(_melody)
return score return score