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
@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
when 'duration'
if @note
@note['durNum'] = @text.to_i
@note['durDenom'] = @prop['divisions']*4
when 'root-step' then
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

View File

@ -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,11 +81,7 @@ end
STEPS = 'C DbD EbE F GbG AbA BbB '
def _note(pitch, dur, tied=0)
note = REXML::Element.new('note')
if pitch == VL::NoPitch
note.add_element(REXML::Element.new('rest'))
else
def _pitch(name, pitch, prefix="")
oct = pitch/12 - 1
stp = 2*(pitch%12)
step = STEPS[stp]
@ -102,16 +94,30 @@ def _note(pitch, dur, tied=0)
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
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
@ -125,44 +131,151 @@ def _note(pitch, dur, tied=0)
return note
end
def _chords
chords = REXML::Element.new('part')
chords.add_attribute('id', 'HARM')
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
}
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]))
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)
#
# 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
seenNote = 0
if chord['root'] != VL::NoPitch
m.add_element _note(chord['root'], dur)
seenNote = VL::InChord
kind = 'suspended-second'
end
pitch = chord['pitch']
steps = chord['steps']
(0..25).each do |step|
if (steps & (1<<step)) != 0
m.add_element _note(pitch+step, dur, seenNote)
seenNote = VL::InChord
else
#
# It's hard to score alterations properly, so rank purely by correct steps
#
best = 0
kind = 'none'
CHORD.each do |k,mask|
mask &= steps
score = 0
while mask > 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