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='')