From ec63fe8a7d35c02c8506dcef0d256cee7daff96b Mon Sep 17 00:00:00 2001 From: Matthias Neeracher Date: Wed, 31 Aug 2011 05:54:02 +0200 Subject: [PATCH] Improve MusicXML export, especially tuplets and ties --- Filters/VLMusicXMLType.reader | 8 +- Filters/VLMusicXMLType.writer | 203 +++++++++++++++++++++++++--------- Sources/VLModel.h | 3 +- Sources/VLPListDocument.mm | 74 ++++++++++++- 4 files changed, 229 insertions(+), 59 deletions(-) diff --git a/Filters/VLMusicXMLType.reader b/Filters/VLMusicXMLType.reader index 410b1f4..199bc8c 100755 --- a/Filters/VLMusicXMLType.reader +++ b/Filters/VLMusicXMLType.reader @@ -221,7 +221,9 @@ class MusicXMLListener 'root-step', 'root-alter', 'bass-step', 'bass-alter', 'kind', 'degree-value', 'degree-alter', 'degree-type', 'accidental' @kind = tag - when 'tie' + when 'notation' + @note['tied'] = 0 + when 'tie', 'tied' @note['tied'] ||= 0 case attrs['type'] when 'start' @@ -348,6 +350,10 @@ class MusicXMLListener else @duration = @text.to_i end + when 'actual-notes' + @note['actualNotes'] = @text.to_i + when 'normal-notes' + @note['normalNotes'] = @text.to_i when 'accidental' case @text when 'sharp' diff --git a/Filters/VLMusicXMLType.writer b/Filters/VLMusicXMLType.writer index 03c1011..da015c9 100755 --- a/Filters/VLMusicXMLType.writer +++ b/Filters/VLMusicXMLType.writer @@ -51,49 +51,68 @@ def _part_list part_list = REXML::Element.new('part-list') melody = REXML::Element.new('score-part') melody.add_attribute('id', 'MELO') - melody.add_element newTextElement('part-name', 'Song') + partname = newTextElement('part-name', 'Song') + partname.add_attribute('print-object', 'no') + melody.add_element(partname) part_list.add_element(melody) return part_list end +$LAST_DIVISIONS = nil +$LAST_KEY = nil +$LAST_MODE = nil +$LAST_TIME_NUM = nil +$LAST_TIME_DENOM= nil +$SEEN_CLEF = false + def _attributes(prop) $DIVISIONS = prop['divisions'] attr = REXML::Element.new('attributes') - attr.add_element newTextElement('divisions', prop['divisions']) + if $LAST_DIVISIONS != prop['divisions'] + attr.add_element newTextElement('divisions', $LAST_DIVISIONS = prop['divisions']) + end + if prop['key'] != $LAST_KEY || prop['mode'] != $LAST_MODE key = REXML::Element.new('key') - key.add_element newTextElement('fifths', prop['key']) - key.add_element newTextElement('mode', prop['mode'] > 0 ? "major" : "minor") - attr.add_element(key) + key.add_element newTextElement('fifths', $LAST_KEY = prop['key']) + key.add_element newTextElement('mode', ($LAST_MODE = prop['mode']) > 0 ? "major" : "minor") + attr.add_element(key) + end + if prop['timeNum'] != $LAST_TIME_NUM || prop['timeDenom'] != $LAST_TIME_DENOM time = REXML::Element.new('time') - time.add_element newTextElement('beats', prop['timeNum']) - time.add_element newTextElement('beat-type', prop['timeDenom']) - attr.add_element(time) + time.add_element newTextElement('beats', $LAST_TIME_NUM = prop['timeNum']) + time.add_element newTextElement('beat-type', $LAST_TIME_DENOM = prop['timeDenom']) + attr.add_element(time) + end + if !$SEEN_CLEF + $SEEN_CLEF = true clef = REXML::Element.new('clef') clef.add_element newTextElement('sign', 'G') clef.add_element newTextElement('line', 2) - attr.add_element(clef) - return attr -end - -def _groove(groove) - dir = REXML::Element.new('direction') - dty = REXML::Element.new('direction-type') - dty.add_element newTextElement('words', groove) - dir.add_element(dty) - - return dir + attr.add_element(clef) + end + return attr.has_elements? ? attr : nil end def _tempo(tempo) dir = REXML::Element.new('direction') dty = REXML::Element.new('direction-type') - metro = REXML::Element.new('metronome') - metro.add_element newTextElement('beat-unit', 'quarter') - metro.add_element newTextElement('per-minute', tempo) + metro = REXML::Element.new('metronome') + metro.add_element newTextElement('beat-unit', 'quarter') + metro.add_element newTextElement('per-minute', tempo) dty.add_element(metro) dir.add_element(dty) dir.add_element 'sound', {'tempo' => tempo} + + return dir +end + +def _groove(groove) + dir = REXML::Element.new('direction') + dir.add_attribute('placement', 'above') + dty = REXML::Element.new('direction-type') + dty.add_element newTextElement('words', groove) + dir.add_element(dty) return dir end @@ -106,14 +125,20 @@ def _pitch(name, pitch, accidental, prefix="") stp = (pitch%12)+2 step = STEPS[stp] if step == 0x20 - abort "Pitch #{pitch} not representable" + if $LAST_KEY > 0 + step = STEPS[stp-1] + accidental = 1 + else + step = STEPS[stp+1] + accidental = -1 + end end if prefix.length > 0 prefix += "-" end pitch= REXML::Element.new(name) pitch.add_element newTextElement(prefix+'step', step.chr) - if accidental != 0 + if accidental && accidental != 0 pitch.add_element newTextElement(prefix+'alter', accidental) end if prefix.length == 0 @@ -143,7 +168,7 @@ def _accidental(visual) accidental end -def _note(pitch, dur, visual, tied) +def _note(pitch, dur, visual, tuplet, tied) accidental = _accidental(visual) note = REXML::Element.new('note') if pitch == VL::NoPitch @@ -152,21 +177,49 @@ def _note(pitch, dur, visual, tied) note.add_element(_pitch('pitch', pitch, accidental)) end note.add_element newTextElement('duration', dur) + notations = nil if (tied & VL::TiedWithPrev) != 0 note.add_element 'tie', {'type' => 'stop' } + unless notations + notations = REXML::Element.new('notations') + end + notations.add_element 'tied', {'type' => 'stop'} end if (tied & VL::TiedWithNext) != 0 note.add_element 'tie', {'type' => 'start' } + unless notations + notations = REXML::Element.new('notations') + end + notations.add_element 'tied', {'type' => 'start'} end note.add_element newTextElement('voice', 1) note.add_element newTextElement('type', TYPE[visual & 7]) if accidental note.add_element newTextElement('accidental', ACC[accidental+2]) end + if tuplet + unless notations + notations = REXML::Element.new('notations') + end + notations.add_element 'tuplet', {'type' => (tuplet>0 ? 'start' : 'stop')} + end + if notations + note.add_element notations + end return note end +def _timeMod(note) + timeMod = REXML::Element.new('time-modification') + timeMod.add_element newTextElement('actual-notes', note['actualNotes']) + timeMod.add_element newTextElement('normal-notes', note['normalNotes']) + if note['normalType'] + timeMod.add_element newTextElement('normal-type', TYPE[note['normalType']]) + end + return timeMod +end + CHORD = { # # Triads @@ -350,27 +403,18 @@ def _melody lastGroove = groove m.add_element(_groove(lastGroove)) end - m.add_element(_attributes(INPUT['properties'][lastProp])) + if attr = _attributes(INPUT['properties'][lastProp]) + m.add_element(attr) + end end if meas['new-page'] m.add_element 'print', {'new-page' => 'yes'} elsif meas['new-system'] m.add_element 'print', {'new-system' => 'yes'} end - if meas['coda'] - m.add_element 'sound', {'coda' => 'A'} - end - if meas['begin-repeat'] - barline = REXML::Element.new('barline') - barline.add_attribute('location', 'left') - barline.add_element newTextElement('bar-style', - (repeat[measNum-1] & 2) != 0 ? 'heavy-heavy' : 'heavy-light') - barline.add_element 'repeat', {'direction' => 'forward'} - m.add_element(barline) - end + coda = meas['coda'] + eEnding = nil if ending = meas['begin-ending'] - barline = REXML::Element.new('barline') - barline.add_attribute('location', 'left') volta = ending['volta'] num = nil (0..7).each do |i| @@ -382,9 +426,37 @@ def _melody end end end - barline.add_element 'ending', {'type' => 'start', 'number' => num} + eEnding = REXML::Element.new('ending') + eEnding.add_attributes({'type' => 'start', 'number' => num}) + end + eBarstyle = nil + eRepeat = nil + if meas['begin-repeat'] + eBarstyle = newTextElement('bar-style', + (repeat[measNum-1] & 2) != 0 ? 'heavy-heavy' : 'heavy-light') + eRepeat = REXML::Element.new('repeat') + eRepeat.add_attribute('direction', 'forward') + end + if eEnding || eBarstyle || eRepeat || coda + barline = REXML::Element.new('barline') + barline.add_attribute('location', 'left') + if eBarstyle + barline.add_element(eBarstyle) + end + if coda + barline.add_element 'coda' + end + if eEnding + barline.add_element(eEnding) + end + if eRepeat + barline.add_element(eRepeat) + end m.add_element(barline) end + if coda + m.add_element 'sound', {'coda' => 'A'} + end noteAt = 0 chordAt= 0 chords = meas['chords'] @@ -412,7 +484,10 @@ def _melody m.add_element(bk) tempAt = noteAt end - n = _note(note['pitch'], dur, note['visual'], note['tied'] || 0) + n = _note(note['pitch'], dur, note['visual'], note['tuplet'], note['tied'] || 0) + if note['actualNotes'] + n.add_element _timeMod(note) + end stanza = 1 note['lyrics'].each do |syll| if syll['text'] @@ -427,19 +502,18 @@ def _melody noteAt += dur m.add_element(n) end + eRepeat = nil + eBarstyle = nil if r = meas['end-repeat'] - barline = REXML::Element.new('barline') - barline.add_attribute('location', 'right') - barline.add_element newTextElement('bar-style', + eBarstyle = newTextElement('bar-style', (repeat[measNum+1] & 1) != 0 ? 'heavy-heavy' : 'light-heavy') - barline.add_element 'repeat', {'direction' => 'backward', 'times' => r['times'].to_s} - m.add_element(barline) + eRepeat = REXML::Element.new('repeat') + eRepeat.add_attributes({'direction' => 'backward', 'times' => r['times'].to_s}) end + eEnding = nil if ending = meas['end-ending'] - barline = REXML::Element.new('barline') - barline.add_attribute('location', 'right') - barline.add_element newTextElement('bar-style', - (repeat[measNum+1] & 1) != 0 ? 'heavy-heavy' : 'light-heavy') + eBarstyle ||= newTextElement('bar-style', + (repeat[measNum+1] & 1) != 0 ? 'heavy-heavy' : 'light-heavy') volta = ending['volta'] num = nil (0..7).each do |i| @@ -452,13 +526,34 @@ def _melody end end type = ending['last'] ? "discontinue" : "stop" - barline.add_element 'ending', {'type', type, 'number', num} - barline.add_element 'repeat', {'direction' => 'backward'} - m.add_element(barline) + eEnding = REXML::Element.new('ending') + eEnding.add_attributes({'type', type, 'number', num}) + if !ending['last'] && !eRepeat + eRepeat = REXML::Element.new('repeat') + eRepeat.add_attribute('direction', 'backward') + end end - if meas['tocoda'] + if coda = meas['tocoda'] m.add_element 'sound', {'tocoda' => 'A'} end + if coda || eEnding || eRepeat || eBarstyle + barline = REXML::Element.new('barline') + barline.add_attribute('location', 'right') + if eBarstyle + barline.add_element(eBarstyle) + end + if coda + barline.add_element 'coda' + end + if eEnding + barline.add_element(eEnding) + end + if eRepeat + barline.add_element(eRepeat) + end + m.add_element(barline) + end + melody.add_element(m) end diff --git a/Sources/VLModel.h b/Sources/VLModel.h index d3ccae3..09a222c 100644 --- a/Sources/VLModel.h +++ b/Sources/VLModel.h @@ -147,7 +147,8 @@ struct VLNote { kTupletMask = 0xFF00 }; static int TupletNum(uint16_t visual) { return visual >> 12; } - static int TupletDenom(uint16_t visual) { return (visual >> 8) & 0x0F; } + static int TupletDenom(uint16_t visual) { return (visual >> 8) & 0x0F; } + static uint16_t Tuplet(int num, int denom) { return (num << 12) | (denom << 8); } VLNote(VLFraction dur=0, int pitch=kNoPitch, uint16_t visual=0); VLNote(std::string name); std::string Name(uint16_t accidentals=0) const; diff --git a/Sources/VLPListDocument.mm b/Sources/VLPListDocument.mm index 3e6da11..1eb8f09 100644 --- a/Sources/VLPListDocument.mm +++ b/Sources/VLPListDocument.mm @@ -39,9 +39,14 @@ protected: NSMutableArray * fMeasures; NSMutableArray * fNotes; NSMutableArray * fChords; + NSMutableArray * fNotesInTuplet; bool fPerfOrder; const VLSong * fSong; VLVisualFilter fVisFilter; + int fInTuplet; + uint16_t fTuplet; + uint16_t fTupletNote; + VLFraction fTupletDur; }; NSArray * VLPlistVisitor::EncodeProperties(const std::vector & properties) @@ -83,8 +88,11 @@ void VLPlistVisitor::VisitMeasure(size_t m, VLProperties & p, VLMeasure & meas) fChords= [NSMutableArray arrayWithCapacity:1]; fVisFilter.ResetWithKey(p.fKey); + fInTuplet = 0; VisitNotes(meas, p, true); VisitChords(meas); + if (fInTuplet) + [[fNotesInTuplet lastObject] setObject:[NSNumber numberWithInt:-1] forKey:@"tuplet"]; NSMutableDictionary * md = [NSMutableDictionary dictionaryWithObjectsAndKeys: @@ -142,8 +150,8 @@ void VLPlistVisitor::VisitNote(VLLyricsNote & n) : [NSDictionary dictionary]]; int grid = n.fPitch==VLNote::kNoPitch ? 0 : VLPitchToGrid(n.fPitch, n.fVisual, 0); - NSDictionary * nd = - [NSDictionary dictionaryWithObjectsAndKeys: + NSMutableDictionary * nd = + [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:n.fDuration.fNum], @"durNum", [NSNumber numberWithInt:n.fDuration.fDenom], @"durDenom", [NSNumber numberWithInt:n.fPitch], @"pitch", @@ -151,6 +159,62 @@ void VLPlistVisitor::VisitNote(VLLyricsNote & n) [NSNumber numberWithInt:fVisFilter(grid, n.fVisual)], @"visual", ly, @"lyrics", nil]; + if (uint16_t newTuplet = n.fVisual & VLNote::kTupletMask) { + if (!fInTuplet || newTuplet != fTuplet) { + if (fInTuplet) { + [[fNotesInTuplet lastObject] setObject:[NSNumber numberWithInt:-1] forKey:@"tuplet"]; + fInTuplet = 0; + } + fNotesInTuplet = [NSMutableArray array]; + fTuplet = newTuplet; + fTupletNote = n.fVisual & VLNote::kNoteHeadMask; + fTupletDur = 0; + [nd setObject:[NSNumber numberWithInt:1] forKey:@"tuplet"]; + } + int tupletNum = VLNote::TupletNum(fTuplet); + int tupletDenom = VLNote::TupletDenom(fTuplet); + [nd setObject:[NSNumber numberWithInt:tupletNum] forKey:@"actualNotes"]; + [nd setObject:[NSNumber numberWithInt:tupletDenom] forKey:@"normalNotes"]; + ++fInTuplet; + fTupletDur += n.fDuration; + if (fTuplet == VLNote::kTriplet) { + uint16_t newNote = n.fVisual & VLNote::kNoteHeadMask; + if (newNote == fTupletNote) { + if (fInTuplet == 3) + fInTuplet = 0; + } else if (fInTuplet == 2 && newNote == fTupletNote-1) { + // + // 8th, 4th triplet + // + [nd setObject:[NSNumber numberWithInt:fTupletNote] forKey:@"normalType"]; + fInTuplet = 0; + } else if (fInTuplet == 3 && newNote == fTupletNote-1) { + // + // 8th 8th 4th + // + for (NSMutableDictionary * prevNote in fNotesInTuplet) { + [prevNote setObject:[NSNumber numberWithInt:newNote] forKey:@"normalType"]; + } + fInTuplet = 2; + fTupletNote = newNote; + } else { + // + // 4th 4th 8th + // + [nd setObject:[NSNumber numberWithInt:fTupletNote] forKey:@"normalType"]; + if (fTupletDur.fNum == 1 && !(fTupletDur.fDenom&(fTupletDur.fDenom-1))) + fInTuplet = 0; + } + } else if (fInTuplet == tupletNum) + fInTuplet = 0; + if (!fInTuplet) + [nd setObject:[NSNumber numberWithInt:-1] forKey:@"tuplet"]; + else + [fNotesInTuplet addObject:nd]; + } else if (fInTuplet) { + [[fNotesInTuplet lastObject] setObject:[NSNumber numberWithInt:-1] forKey:@"tuplet"]; + fInTuplet = 0; + } [fNotes addObject:nd]; } @@ -255,9 +319,13 @@ enum { [[ndict objectForKey:@"durDenom"] intValue], true); note.fPitch = [[ndict objectForKey:@"pitch"] intValue]; - note.fVisual |= [[ndict objectForKey:@"visual"] intValue] + note.fVisual = [[ndict objectForKey:@"visual"] intValue] & VLNote::kAccidentalsMask; note.fTied = 0; + + if ([ndict objectForKey:@"actualNotes"]) + note.fVisual |= VLNote::Tuplet([[ndict objectForKey:@"actualNotes"] intValue], + [[ndict objectForKey:@"normalNotes"] intValue]); if ([[ndict objectForKey:@"tied"] intValue] & VLNote::kTiedWithPrev) { if (at != 0) {