diff --git a/TECHNICAL_GUIDE.md b/TECHNICAL_GUIDE.md index f6db971..ba04df0 100644 --- a/TECHNICAL_GUIDE.md +++ b/TECHNICAL_GUIDE.md @@ -71,6 +71,8 @@ Multiple staves/parts, D.C./D.S./coda/fine navigation, slurs, dynamics, articula Layout/exporter information such as page defaults, credits, print layout, font/color/position attributes, and source pagination does not determine the small-screen layout. Engraving is regenerated. Beam groups and written note values are retained; unrelated engraving metadata does not authorize ignoring musical descendants. `defusedxml==0.7.1` forbids XML entities and external access. Ordinary MusicXML DOCTYPE declarations are allowed without fetching their DTDs. +Chord display text is not a substitute for structured harmony. Some Soundslice exports encode minor chord labels as `other` without any degrees; these fail import. Use `minor-seventh` for `-7`, `half-diminished` for `-7b5`, `major-minor` for `-maj7`, and `minor-sixth` for `-6`, keeping the `text` attribute if desired. MusicXML [`other`](https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/kind-value/) requires explicit added `` elements defining the chord. Correct these exports before placing them in `data/`; the importer never guesses pitches from an incomplete chord label. + ### Vamp Metadata In MuseScore's score properties, set `vamp:style` to a supported player style. It exports as standard MusicXML miscellaneous metadata: diff --git a/data/practice/chord/001_three_note_shell_voicings/006-cycles.xml b/data/practice/chord/001_three_note_shell_voicings/006-cycles.xml index 45798ca..613539a 100644 --- a/data/practice/chord/001_three_note_shell_voicings/006-cycles.xml +++ b/data/practice/chord/001_three_note_shell_voicings/006-cycles.xml @@ -140,7 +140,7 @@ G - other + minor-seventh @@ -165,7 +165,7 @@ C - other + minor-seventh @@ -191,7 +191,7 @@ F - other + minor-seventh @@ -218,7 +218,7 @@ B -1 - other + minor-seventh diff --git a/data/practice/chord/001_three_note_shell_voicings/007-cycles.xml b/data/practice/chord/001_three_note_shell_voicings/007-cycles.xml index 894de16..d1206a6 100644 --- a/data/practice/chord/001_three_note_shell_voicings/007-cycles.xml +++ b/data/practice/chord/001_three_note_shell_voicings/007-cycles.xml @@ -279,7 +279,7 @@ G - other + minor-seventh @@ -340,7 +340,7 @@ C - other + minor-seventh @@ -405,7 +405,7 @@ F - other + minor-seventh @@ -470,7 +470,7 @@ B -1 - other + minor-seventh diff --git a/data/practice/chord/001_three_note_shell_voicings/008-ii-v-i-progressions.xml b/data/practice/chord/001_three_note_shell_voicings/008-ii-v-i-progressions.xml index b61c11b..0ded341 100644 --- a/data/practice/chord/001_three_note_shell_voicings/008-ii-v-i-progressions.xml +++ b/data/practice/chord/001_three_note_shell_voicings/008-ii-v-i-progressions.xml @@ -35,7 +35,7 @@ G - other + minor-seventh diff --git a/data/practice/chord/001_three_note_shell_voicings/009-ii-v-i-progressions.xml b/data/practice/chord/001_three_note_shell_voicings/009-ii-v-i-progressions.xml index c543a0f..e345dea 100644 --- a/data/practice/chord/001_three_note_shell_voicings/009-ii-v-i-progressions.xml +++ b/data/practice/chord/001_three_note_shell_voicings/009-ii-v-i-progressions.xml @@ -35,7 +35,7 @@ G - other + minor-seventh diff --git a/data/practice/chord/001_three_note_shell_voicings/010-ii-v-i-progressions.xml b/data/practice/chord/001_three_note_shell_voicings/010-ii-v-i-progressions.xml index 5e61108..5f6d1b5 100644 --- a/data/practice/chord/001_three_note_shell_voicings/010-ii-v-i-progressions.xml +++ b/data/practice/chord/001_three_note_shell_voicings/010-ii-v-i-progressions.xml @@ -35,7 +35,7 @@ C - other + minor-seventh diff --git a/data/practice/chord/001_three_note_shell_voicings/011-ii-v-i-progressions.xml b/data/practice/chord/001_three_note_shell_voicings/011-ii-v-i-progressions.xml index eb97a7a..17791d9 100644 --- a/data/practice/chord/001_three_note_shell_voicings/011-ii-v-i-progressions.xml +++ b/data/practice/chord/001_three_note_shell_voicings/011-ii-v-i-progressions.xml @@ -35,7 +35,7 @@ G - other + half-diminished @@ -124,7 +124,7 @@ F - other + major-minor @@ -158,7 +158,7 @@ F - other + minor-sixth diff --git a/data/practice/chord/001_three_note_shell_voicings/012-ii-v-i-progressions.xml b/data/practice/chord/001_three_note_shell_voicings/012-ii-v-i-progressions.xml index f1d6abf..5097868 100644 --- a/data/practice/chord/001_three_note_shell_voicings/012-ii-v-i-progressions.xml +++ b/data/practice/chord/001_three_note_shell_voicings/012-ii-v-i-progressions.xml @@ -35,7 +35,7 @@ C - other + half-diminished @@ -126,7 +126,7 @@ B -1 - other + major-minor @@ -162,7 +162,7 @@ B -1 - other + minor-sixth diff --git a/generator/harmony.py b/generator/harmony.py index 7b6f847..8a88090 100644 --- a/generator/harmony.py +++ b/generator/harmony.py @@ -49,6 +49,8 @@ def degrees(harmony: dict) -> dict[int, int]: else: raise ValueError(f'unsupported degree operation: {operation}') if kind != 'none' and not result: + if kind == 'other': + raise ValueError("harmony kind 'other' requires explicit added degrees; use a standard or add elements; display text alone does not define pitches") raise ValueError('harmony contains no pitch information') return result diff --git a/generator/musicxml.py b/generator/musicxml.py index fe2f711..4368f1e 100644 --- a/generator/musicxml.py +++ b/generator/musicxml.py @@ -591,9 +591,9 @@ class Importer: self.children(item, {'duration', 'voice', 'staff'}) self.identity(item) duration = self.number(item.findtext('duration'), positive=True) / self.divisions - self.sequence += 1 from generator.model import make_event if item.find('voice') is not None: + self.sequence += 1 voices.setdefault(self.voice_id, []).append(make_event(f'spacer-{self.sequence}', cursor, duration, 'spacer') | {'voice': self.voice_id}) cursor += duration extent = max(extent, cursor) diff --git a/tests/test_musicxml.py b/tests/test_musicxml.py index c34260d..afd14044 100644 --- a/tests/test_musicxml.py +++ b/tests/test_musicxml.py @@ -60,6 +60,14 @@ class MusicXMLTests(unittest.TestCase): a = parse(xml_score(xml_note(duration=4))) b = parse(xml_score(xml_note(duration=1920), divisions=480)) self.assertEqual(a, b) + def test_cursor_only_forward_does_not_change_event_ids(self): + movement = '11' + self.assertEqual(parse(xml_score(movement + xml_note())), parse(xml_score(xml_note()))) + def test_voiced_forward_has_its_own_event_id(self): + content = '11' + xml_note(duration=3, kind='half', extra='') + events = notes(parse(xml_score(content))) + self.assertEqual([(n['id'], n['kind']) for n in events], [('spacer-1', 'spacer'), ('note-2', 'note')]) + self.assertEqual([fraction(n['duration']) for n in events], [1, 3]) def test_doctype_allowed_but_entities_rejected(self): source = xml_score(xml_note()) source = source.replace('C{kind}' + score = parse(xml_score(content + xml_note())) + chord = score['staves'][0]['measures'][0]['harmonies'][0] + self.assertEqual(chord['symbol'], 'C' + text) + self.assertEqual(pitch_classes(chord), expected) + def test_other_harmony_requires_explicit_degrees(self): + for kind in ('other', 'other', 'other', 'other'): + with self.subTest(kind=kind), self.assertRaisesRegex(MusicXMLError, r"fixture.xml: part P1, measure 1: harmony kind 'other' requires explicit added degrees"): + parse(xml_score('C' + kind + '' + xml_note())) + def test_other_harmony_with_explicit_degrees_remains_supported(self): + from generator.harmony import pitch_classes + degrees = ''.join(f'{value}{alter}add' for value, alter in ((1, 0), (3, -1), (5, 0), (7, 0))) + content = 'Cother' + degrees + '' + score = parse(xml_score(content + xml_note())) + chord = score['staves'][0]['measures'][0]['harmonies'][0] + self.assertEqual(chord['kind'], 'other') + self.assertEqual(chord['symbol'], 'C-7') + self.assertEqual(pitch_classes(chord), (0, 3, 7, 10)) + with self.assertRaisesRegex(MusicXMLError, 'contradictory harmony text'): + parse(xml_score(content.replace('text="-7"', 'text="maj7"') + xml_note())) def test_grace_cue_and_measured_tremolo(self): grace = 'D416th' regular = xml_note(extra='3', prefix='')