From 7d8a5799074ad1a2fced942a04909f80820bff9e Mon Sep 17 00:00:00 2001 From: Matthias Neeracher Date: Tue, 4 Sep 2007 17:26:37 +0000 Subject: [PATCH] Properly read and write harmony info --- Filters/VLMusicXMLType.reader | 129 +++++++++-------- Filters/VLMusicXMLType.writer | 258 ++++++++++++++++++++++++++-------- 2 files changed, 267 insertions(+), 120 deletions(-) diff --git a/Filters/VLMusicXMLType.reader b/Filters/VLMusicXMLType.reader index 0787c39..1d4ff4b 100755 --- a/Filters/VLMusicXMLType.reader +++ b/Filters/VLMusicXMLType.reader @@ -108,39 +108,40 @@ class MusicXMLListener @kind = nil # Ignore all tags not recognized here @text = "" case tag - when 'score-timewise' then + when 'score-timewise' $stderr.puts "Can't read timewise MusicXML files yet" exit 1 - when 'work-title' then + when 'work-title' @kind = 'textProp' @key = 'title' - when 'creator' then + when 'creator' case attrs['type'] - when 'composer' then + when 'composer' @kind = 'textProp' @key = 'composer' - when 'poet' then + when 'poet' @kind = 'textProp' @key = 'lyricist' end - when 'miscellaneous-field' then + when 'miscellaneous-field' if attrs['name'] == 'VocalEasel-groove' @kind = 'textProp' @key = 'groove' end - when 'encoding-date' then + when 'encoding-date' @kind = 'dateProp' @key = 'saved' - when 'software' then + when 'software' @kind = 'textProp' @key = 'software' - when 'part' then + when 'part' @part = attrs['id'] || "" @prop = {} @props = [] @measNo = -1 - when 'measure' then + when 'measure' @notes= [] + @note = nil @harm = [] @chord= false @at = 0 @@ -157,20 +158,20 @@ class MusicXMLListener 'melody' => [] } end - when 'barline' then + when 'barline' @times = nil @type = nil @number = "" - when 'repeat' then + when 'repeat' if attrs['direction'] == 'backward' && attrs['times'] @times = attrs['times'].to_i elsif attrs['direction'] == 'forward' @times = 0 end - when 'ending' then + when 'ending' @type = attrs['type'] @number = attrs['number'] - when 'sound' then + when 'sound' if attrs['tocoda'] @meas['tocoda'] = true elsif attrs['coda'] @@ -179,47 +180,46 @@ class MusicXMLListener if attrs['tempo'] OUTPUT['tempo'] = attrs['tempo'].to_i end - when 'divisions' then + when 'divisions' @kind = 'prop' @key = 'divisions' - when 'fifths' then + when 'fifths' @kind = 'prop' @key = 'key' - when 'beats' then + when 'beats' @kind = 'prop' @key = 'timeNum' - when 'beat-type' then + when 'beat-type' @kind = 'prop' @key = 'timeDenom' - when 'harmony' then + when 'harmony' @note = { 'pitch' => VL::NoPitch, 'steps' => 0, 'root' => VL::NoPitch, 'at' => @at } - when 'degree' then + when 'degree' @degree_value = 0 @degree_alter = 0 @degree_type = "alter" - when 'note' then + when 'note' @note = { 'pitch' => 0, 'durNum' => 0, 'durDenom' => 0 } - when 'rest' then + when 'rest' @note['pitch'] = VL::NoPitch when 'mode', 'step', 'alter', 'octave', 'duration', 'syllabic', 'text', 'root-step', 'root-alter', 'bass-step', 'bass-alter', 'kind', 'degree-value', 'degree-alter', 'degree-type' - then @kind = tag - when 'tie' then + when 'tie' @note['tied'] ||= 0 case attrs['type'] - when 'start' then + when 'start' @note['tied'] |= VL::TiedWithNext - when 'stop' then + when 'stop' @note['tied'] |= VL::TiedWithPrev end - when 'lyric' then + when 'lyric' num = (attrs['number'] || "1").to_i-1 @note['lyrics'] ||= [] @note['lyrics'][num] = @lyric = {} - when 'chord' then + when 'chord' @chord = true end end @@ -283,44 +283,48 @@ class MusicXMLListener # if @kind case @kind - when 'textProp' then + when 'textProp' OUTPUT[@key] = @text - when 'dateProp' then + when 'dateProp' begin OUTPUT[@key] = Time.parse(@text) rescue OUTPUT[@key] = Time.now end - when 'prop' then + when 'prop' @prop[@key] = @text.to_i - when 'mode' then + when 'mode' @prop['mode'] = @text == 'minor' ? -1 : 1 - when 'step' then + when 'step' @note['pitch'] += PITCH[@text] - when 'alter', 'root-alter' then + when 'alter', 'root-alter' @note['pitch'] += @text.to_i - when 'octave' then + when 'octave' @note['pitch'] += (@text.to_i+1)*12 - when 'duration' then - @note['durNum'] = @text.to_i - @note['durDenom'] = @prop['divisions']*4 - when 'root-step' then + when 'duration' + if @note + @note['durNum'] = @text.to_i + @note['durDenom'] = @prop['divisions']*4 + else + @duration = @text.to_i + end + when 'root-step' @note['pitch'] = PITCH[@text] - when 'bass-step' then + when 'bass-step' @note['root'] = PITCH[@text] - when 'bass-alter' then + when 'bass-alter' @note['root'] += @text.to_i - when 'kind' then + when 'kind' @note['steps'] = CHORD[@text] - when 'degree-value' then + when 'degree-value' @degree_value = @text.to_i-1 - when 'degree-alter' then + when 'degree-alter' @degree_alter = @text.to_i - when 'degree-type' then + when 'degree-type' @degree_type = @text - when 'syllabic' then + when 'syllabic' @lyric['kind'] = SYLL[@text] - when 'text' then + when 'text' @lyric['text'] = @text end @kind = nil @@ -329,11 +333,11 @@ class MusicXMLListener # Structures are distinguished by tag # case tag - when 'attributes' then + when 'attributes' if @prop != @props.last @props.push(@prop) end - when 'note' then + when 'note' if @chord note = @notes.last note['pitch'] = [*note['pitch']] << @note['pitch'] @@ -342,13 +346,18 @@ class MusicXMLListener @notes.push(@note) @at += @note['durNum'] end - when 'degree' then + @note = nil + when 'forward' + @at += @duration + when 'backup' + @at -= @duration + when 'degree' oldSteps = @note['steps'] oldValue = @degree_value case @degree_type - when 'subtract' then + when 'subtract' @note['steps'] &= ~DEGREE[@degree_value][0] - when 'add' then + when 'add' @degree_value = DEGREE[@degree_value][1] if @degree_alter < 0 @degree_value >>= -@degree_alter @@ -356,7 +365,7 @@ class MusicXMLListener @degree_value <<= @degree_alter end @note['steps'] |= @degree_value - when 'alter' then + when 'alter' @degree_value = @note['steps'] & DEGREE[@degree_value][0] @note['steps'] ^= @degree_value if @degree_alter < 0 @@ -366,16 +375,16 @@ class MusicXMLListener end @note['steps'] |= @degree_value end -# $stderr.puts "#{@degree_type} #{oldValue} #{@degree_alter} (#{oldSteps} -> #{@note['steps']})" - when 'harmony' then + when 'harmony' @harm.push(@note) - when 'barline' then + @note = nil + when 'barline' case @type - when 'start' then + when 'start' @meas['begin-ending'] = { 'volta' => makeVolta(@number) } - when 'stop', 'discontinue' then + when 'stop', 'discontinue' @meas['end-ending'] = { 'volta' => makeVolta(@number), 'last' => @type == 'discontinue' @@ -391,7 +400,7 @@ class MusicXMLListener end end end - when 'measure' then + when 'measure' @meas['properties'] = @props.length-1 unless @props.empty? if @part == 'HARM' @meas['chords'] = makeChords(@notes) @@ -416,7 +425,7 @@ class MusicXMLListener end end end - when 'part' then + when 'part' OUTPUT['properties'] = @props unless @part == 'HARM' end end diff --git a/Filters/VLMusicXMLType.writer b/Filters/VLMusicXMLType.writer index 7d0e9e4..ee9c2bd 100755 --- a/Filters/VLMusicXMLType.writer +++ b/Filters/VLMusicXMLType.writer @@ -50,13 +50,9 @@ end def _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.add_attribute('id', 'MELO') - melody.add_element newTextElement('part-name', 'Melody') + melody.add_element newTextElement('part-name', 'Song') part_list.add_element(melody) return part_list @@ -85,33 +81,43 @@ end 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) note = REXML::Element.new('note') if pitch == VL::NoPitch note.add_element(REXML::Element.new('rest')) 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 note.add_element 'chord' end - pitch= REXML::Element.new('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) + note.add_element(_pitch('pitch', pitch)) end note.add_element newTextElement('duration', dur) if (tied & VL::TiedWithPrev) != 0 @@ -124,45 +130,152 @@ def _note(pitch, dur, tied=0) return note end - -def _chords - chords = REXML::Element.new('part') - chords.add_attribute('id', 'HARM') - - lastProp = -1 - measNum = 0 - INPUT['measures'].each do |meas| - measNum += 1 - m = REXML::Element.new('measure') - m.add_attribute('number', measNum.to_s); - if meas['properties'] != lastProp - lastProp = meas['properties'] - m.add_element(_attributes(INPUT['properties'][lastProp])) + +CHORD = { + # + # 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 +} + +# +# This list differs a bit from the list in .reader, as we prefer +# certain degrees, i.e. #9 rather than b10 +# +DEGREE = [ + [VL::Unison, VL::Unison], + [VL::Min2nd+VL::Maj2nd, VL::Maj2nd], + [VL::Min3rd+VL::Maj3rd, VL::Maj3rd], + [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 - meas['chords'].each do |chord| - dur = (chord['durNum'] * $DIVISIONS * 4) / chord['durDenom'] - if chord['pitch'] == VL::NoPitch - m.add_element _note(VL::NoPitch, dur) - else - seenNote = 0 - if chord['root'] != VL::NoPitch - m.add_element _note(chord['root'], dur) - seenNote = VL::InChord - end - pitch = chord['pitch'] - steps = chord['steps'] - (0..25).each do |step| - if (steps & (1< 0 + score += 1 + mask &= mask-1 + end + if score > best + kind = k + best = score + 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 - chords.add_element(m) end - return chords + return harm end SYLLABLE = %w[single begin end middle] @@ -217,8 +330,33 @@ def _melody barline.add_element 'ending', {'type' => 'start', 'number' => num} m.add_element(barline) end + noteAt = 0 + chordAt= 0 + chords = meas['chords'] + chordIx= 0 meas['melody'].each do |note| 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) stanza = 1 note['lyrics'].each do |syll| @@ -231,6 +369,7 @@ def _melody end stanza += 1 end if note['lyrics'] + noteAt += dur m.add_element(n) end if r = meas['end-repeat'] @@ -246,7 +385,6 @@ def _melody barline.add_attribute('location', 'right') barline.add_element newTextElement('bar-style', (repeat[measNum+1] & 1) != 0 ? 'heavy-heavy' : 'light-heavy') - barline.add_element 'repeat', {'direction' => 'backward'} volta = ending['volta'] num = nil (0..7).each do |i| @@ -260,6 +398,7 @@ def _melody end type = ending['last'] ? "discontinue" : "stop" barline.add_element 'ending', {'type', type, 'number', num} + barline.add_element 'repeat', {'direction' => 'backward'} m.add_element(barline) end if meas['tocoda'] @@ -277,7 +416,6 @@ def _score score.add_element(_work) score.add_element(_identification) score.add_element(_part_list) - score.add_element(_chords) score.add_element(_melody) return score