mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 03:04:00 +00:00
Properly read and write harmony info
This commit is contained in:
parent
0cc5fb11b4
commit
7d8a579907
|
@ -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
|
||||
|
|
|
@ -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<<step)) != 0
|
||||
m.add_element _note(pitch+step, dur, seenNote)
|
||||
seenNote = VL::InChord
|
||||
end
|
||||
end
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue
Block a user