+44
-41
@@ -1,14 +1,18 @@
|
||||
from pathlib import Path
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from generator import harmony
|
||||
from generator.model import rational
|
||||
from generator.musicxml import MusicXMLError, load_musicxml
|
||||
from generator.musicxml_writer import write_musicxml
|
||||
from generator.practice.generate import practice_document
|
||||
from generator.pitch import concert_document
|
||||
from tests.test_musicxml import parse, xml_note, xml_score
|
||||
import generator.engraving.fonts
|
||||
import generator.engraving.layout
|
||||
import generator.engraving.types
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.musicxml
|
||||
import generator.musicxml_writer
|
||||
import generator.pitch
|
||||
import generator.practice.generate
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
|
||||
def chord_xml(text, kind='other', root='C', extra=''):
|
||||
return f'<harmony><root><root-step>{root}</root-step></root><kind text="{text}">{kind}</kind>{extra}</harmony>'
|
||||
@@ -20,82 +24,81 @@ class HarmonyTests(unittest.TestCase):
|
||||
'C-maj7': 'CmM7', 'CmM': 'CmM7', 'C6': 'CM6', 'C-6': 'Cm6', 'Fmaj7#11/A': 'FM7#11/A', 'G7b9': 'G7b9'}
|
||||
for spelling, canonical in cases.items():
|
||||
with self.subTest(spelling=spelling):
|
||||
chord = harmony.parse_symbol(spelling)
|
||||
self.assertEqual(harmony.symbol(chord), canonical)
|
||||
self.assertEqual(harmony.degrees(harmony.parse_symbol(canonical)), harmony.degrees(chord))
|
||||
chord = generator.harmony.parse_symbol(spelling)
|
||||
self.assertEqual(generator.harmony.symbol(chord), canonical)
|
||||
self.assertEqual(generator.harmony.degrees(generator.harmony.parse_symbol(canonical)), generator.harmony.degrees(chord))
|
||||
def test_extended_chords_include_sevenths_but_added_ninths_do_not(self):
|
||||
for prefix, seventh in (('', -1), ('m', -1), ('M', 0), ('mM', 0)):
|
||||
for highest in (9, 11, 13):
|
||||
with self.subTest(prefix=prefix, highest=highest):
|
||||
chord = harmony.parse_symbol(f'C{prefix}{highest}')
|
||||
values = harmony.degrees(chord)
|
||||
chord = generator.harmony.parse_symbol(f'C{prefix}{highest}')
|
||||
values = generator.harmony.degrees(chord)
|
||||
self.assertEqual(values[7], seventh)
|
||||
self.assertTrue(all(values[degree] == 0 for degree in range(9, highest + 1, 2)))
|
||||
self.assertEqual(harmony.symbol(chord), f'C{prefix}{highest}')
|
||||
self.assertEqual(harmony.degrees(harmony.parse_symbol('Cmadd9')), {1: 0, 3: -1, 5: 0, 9: 0})
|
||||
self.assertEqual(harmony.degrees(harmony.parse_symbol('C7no5')), {1: 0, 3: 0, 7: -1})
|
||||
self.assertEqual(harmony.degrees(harmony.parse_symbol('C7sus')), {1: 0, 4: 0, 5: 0, 7: -1})
|
||||
self.assertEqual(generator.harmony.symbol(chord), f'C{prefix}{highest}')
|
||||
self.assertEqual(generator.harmony.degrees(generator.harmony.parse_symbol('Cmadd9')), {1: 0, 3: -1, 5: 0, 9: 0})
|
||||
self.assertEqual(generator.harmony.degrees(generator.harmony.parse_symbol('C7no5')), {1: 0, 3: 0, 7: -1})
|
||||
self.assertEqual(generator.harmony.degrees(generator.harmony.parse_symbol('C7sus')), {1: 0, 4: 0, 5: 0, 7: -1})
|
||||
def test_soundslice_text_only_chords_become_structured(self):
|
||||
for text, kind, values in (('mM7', 'major-minor', {1: 0, 3: -1, 5: 0, 7: 0}), ('m7b5', 'half-diminished', {1: 0, 3: -1, 5: -1, 7: -1}), ('7b9', 'dominant', {1: 0, 3: 0, 5: 0, 7: -1, 9: -1})):
|
||||
with self.subTest(text=text):
|
||||
source = parse(xml_score(chord_xml(text) + xml_note()))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord_xml(text) + tests.test_musicxml.xml_note()))
|
||||
chord = source['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertEqual(chord['kind'], kind)
|
||||
self.assertEqual(harmony.degrees(chord), values)
|
||||
self.assertEqual(generator.harmony.degrees(chord), values)
|
||||
self.assertEqual(chord['symbol'], 'C' + text)
|
||||
self.assertNotIn('kind_text', chord)
|
||||
source = parse(xml_score(chord_xml('m9', 'minor-ninth') + xml_note()))
|
||||
self.assertEqual(harmony.degrees(source['staves'][0]['measures'][0]['harmonies'][0])[7], -1)
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord_xml('m9', 'minor-ninth') + tests.test_musicxml.xml_note()))
|
||||
self.assertEqual(generator.harmony.degrees(source['staves'][0]['measures'][0]['harmonies'][0])[7], -1)
|
||||
def test_parenthesized_full_symbols_transpose_and_roundtrip(self):
|
||||
for label, transposed in (('(Fm7)', '(Gm7)'), ('(Bb7)', '(C7)'), ('(Fm7/Ab)', '(Gm7/Bb)')):
|
||||
with self.subTest(label=label):
|
||||
source = parse(xml_score(chord_xml(label) + xml_note()))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord_xml(label) + tests.test_musicxml.xml_note()))
|
||||
chord = source['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertEqual(chord['symbol'], label)
|
||||
self.assertTrue(chord['chord_optional'])
|
||||
self.assertTrue(chord['parenthesized'])
|
||||
practice = practice_document(source, 'vocal')['staves'][0]['measures'][0]['harmonies'][0]
|
||||
practice = generator.practice.generate.practice_document(source, 'vocal', rhythms=tests.rhythm_fixtures.library())['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertNotIn('chord_optional', practice)
|
||||
self.assertEqual(harmony.symbol(practice), label)
|
||||
self.assertEqual(generator.harmony.symbol(practice), label)
|
||||
source['staves'][0]['measures'][0]['transpose'] = {'diatonic': 1, 'chromatic': 2, 'octave_change': 0}
|
||||
shifted = concert_document(source)
|
||||
shifted = generator.pitch.concert_document(source)
|
||||
result = shifted['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertEqual(result['symbol'], transposed)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / 'score.xml'
|
||||
write_musicxml(path, shifted)
|
||||
restored = load_musicxml(path)['staves'][0]['measures'][0]['harmonies'][0]
|
||||
path = pathlib.Path(temporary) / 'score.xml'
|
||||
generator.musicxml_writer.write_musicxml(path, shifted)
|
||||
restored = generator.musicxml.load_musicxml(path)['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertEqual(restored, result)
|
||||
def test_requested_labels_are_engraved_without_overlaps(self):
|
||||
from generator.engraving import Fonts, Style, layout_score
|
||||
labels = ('CM', 'Cm7', 'CM13#11', 'CmM7', 'CaugM7', 'Cdim7', 'Csus', 'Csus2', 'C7sus4', '(Fm7/Ab)')
|
||||
for label in labels:
|
||||
with self.subTest(label=label):
|
||||
source = parse(xml_score(chord_xml(label) + xml_note()))
|
||||
layout = layout_score(source, Style(), Fonts(Path('package/assets/fonts/petaluma')))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord_xml(label) + tests.test_musicxml.xml_note()))
|
||||
layout = generator.engraving.layout.layout_score(source, generator.engraving.types.Style(), generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma')))
|
||||
rendered = [p.value for page in layout.pages for p in page.primitives if p.role == 'harmony']
|
||||
self.assertEqual(rendered, [harmony.symbol(harmony.parse_symbol(label))])
|
||||
self.assertEqual(rendered, [generator.harmony.symbol(generator.harmony.parse_symbol(label))])
|
||||
def test_unknown_or_contradictory_symbols_are_rejected(self):
|
||||
for label in ('Cunknown', 'Cm79', 'C7b9#9', 'C7add7', 'C7no3no3', 'C7no#5', '(Cm7', 'Cm7)', 'C7/G/Ab'):
|
||||
with self.subTest(label=label), self.assertRaises(ValueError):
|
||||
harmony.parse_symbol(label)
|
||||
generator.harmony.parse_symbol(label)
|
||||
for content in (chord_xml('m7', 'major-seventh'), chord_xml('(Fm7)', 'minor-seventh'), chord_xml('(Fm7)', root='D'), chord_xml('m7/G', extra='<bass><bass-step>A</bass-step></bass>')):
|
||||
with self.subTest(content=content), self.assertRaisesRegex(MusicXMLError, 'contradictory'):
|
||||
parse(xml_score(content + xml_note()))
|
||||
with self.subTest(content=content), self.assertRaisesRegex(generator.musicxml.MusicXMLError, 'contradictory'):
|
||||
tests.test_musicxml.parse(tests.test_musicxml.xml_score(content + tests.test_musicxml.xml_note()))
|
||||
def test_kind_text_cannot_override_canonical_output(self):
|
||||
chord = harmony.parse_symbol('Cm7') | {'kind_text': '-7'}
|
||||
self.assertEqual(harmony.symbol(chord), 'Cm7')
|
||||
source = parse(xml_score(chord_xml('7', 'dominant', extra='<degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree>') + xml_note()))
|
||||
chord = generator.harmony.parse_symbol('Cm7') | {'kind_text': '-7'}
|
||||
self.assertEqual(generator.harmony.symbol(chord), 'Cm7')
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord_xml('7', 'dominant', extra='<degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree>') + tests.test_musicxml.xml_note()))
|
||||
self.assertEqual(source['staves'][0]['measures'][0]['harmonies'][0]['symbol'], 'C7b9')
|
||||
def test_current_exercise_and_song_exports_import_without_edits(self):
|
||||
exercise = load_musicxml(Path('data/practice/0001.xml'))
|
||||
exercise = generator.musicxml.load_musicxml(pathlib.Path('data/practice/0001.xml'))
|
||||
measures = exercise['staves'][0]['measures']
|
||||
self.assertEqual(len(measures), 20)
|
||||
self.assertEqual(sum(len(n['pitches']) for m in measures for v in m['voices'] for n in v['notes']), 70)
|
||||
self.assertEqual(measures[1]['harmonies'][0]['symbol'], 'Bm9')
|
||||
self.assertEqual(measures[3]['harmonies'][0]['symbol'], 'DmM7')
|
||||
self.assertEqual(measures[0]['voices'][0]['notes'][0]['duration'], rational(1))
|
||||
self.assertEqual(measures[0]['voices'][0]['notes'][0]['duration'], generator.model.rational(1))
|
||||
for name, count, key in (('autumn-leaves', 33, 'F'), ('it-could-happen-to-you', 32, 'Eb')):
|
||||
source = load_musicxml(Path('data/songs') / f'{name}.xml')
|
||||
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs') / f'{name}.xml')
|
||||
self.assertEqual(len(source['staves'][0]['measures']), count)
|
||||
self.assertEqual(source['metadata']['key_signature'], key)
|
||||
|
||||
Reference in New Issue
Block a user