Tab Generation
ci / checks (push) Successful in 1h19m14s

This commit is contained in:
khodowany
2026-09-20 16:20:43 -05:00
parent 68ded4d8c8
commit cf7c0c010c
423 changed files with 1422 additions and 1332 deletions
+11 -63
View File
@@ -10,13 +10,18 @@ from generator.engraving import Fonts, Style, engrave_tree, layout_score
from generator.engraving.attributes import staff_attributes
from generator.engraving.notation import ScoreIndex
from generator.model import key_from_fifths, parse_pitch_name, rational, validate_document
from generator.practice.generate import generate_practice_scores, key_file_name, practice_document
from generator.score import load_json, write_json
from generator.transposition import TARGET_KEYS, concert_document, shift_key, transpose_document
from generator.pitch import concert_document, shift_key
from tests.test_musicxml import parse, xml_note, xml_score
FONT_ROOT = Path('package/assets/fonts/petaluma')
def instrument_score(document, steps, semitones):
source = copy.deepcopy(document)
for measure in source['staves'][0]['measures']:
measure['transpose'] = {'diatonic': steps, 'chromatic': semitones, 'octave_change': 0}
return concert_document(source)
def key_change(old, new, local=False, attributes=''):
initial = f'<key><fifths>{old}</fifths></key>' + attributes
change = f'<attributes><key><cancel>{old}</cancel><fifths>{new}</fifths></key></attributes>'
@@ -38,7 +43,7 @@ class KeyCancellationTests(unittest.TestCase):
with self.subTest(old=old, new=new, target=target, local=local):
source = key_change(old, new, local)
before = copy.deepcopy(source)
result = transpose_document(source, target)
result = instrument_score(source, *({'A': (1, 2), 'Ab': (-1, -2), 'G': (1, 2), 'F': (-1, -2)}[target]))
validate_document(result)
measures = result['staves'][0]['measures']
self.assertEqual(measures[0]['key'], key_from_fifths(expected_old))
@@ -66,7 +71,7 @@ class KeyCancellationTests(unittest.TestCase):
self.assertEqual(source, before)
def test_metadata_and_initial_signature_transpose_cancellation(self):
source = parse(xml_score(xml_note(), '<key><cancel>1</cancel><fifths>0</fifths></key>'))
result = transpose_document(source, 'D', octave_shift=1)
result = instrument_score(source, 1, 2)
expected = key_from_fifths(2) | {'cancel': 3}
self.assertEqual(result['metadata']['key'], expected)
self.assertEqual(result['staves'][0]['measures'][0]['key'], expected)
@@ -81,7 +86,7 @@ class KeyCancellationTests(unittest.TestCase):
def test_unchanged_accidentals_do_not_receive_naturals(self):
for old, new, target in [(1, 0, 'F'), (-1, 0, 'G'), (1, 1, 'A')]:
with self.subTest(old=old, new=new, target=target):
result = transpose_document(key_change(old, new), target)
result = instrument_score(key_change(old, new), *({'F': (-1, -2), 'G': (1, 2), 'A': (1, 2)}[target]))
index = ScoreIndex(result)
glyphs = staff_attributes(index, index.state(Fraction(4)), Fraction(4), self.fonts, 12, 'change', ('key',))
self.assertNotIn('accidentalNatural', [p.value for p in glyphs])
@@ -99,7 +104,7 @@ class KeyCancellationTests(unittest.TestCase):
change['key']['cancel'] = 14
before = copy.deepcopy(source)
with self.subTest(local=local), self.assertRaisesRegex(ValueError, 'key cancellation.*represented'):
transpose_document(source, 'D')
instrument_score(source, 1, 2)
self.assertEqual(source, before)
def test_concert_transposition_uses_the_same_interval_for_cancellation(self):
attributes = '<transpose><diatonic>1</diatonic><chromatic>2</chromatic></transpose>'
@@ -111,60 +116,3 @@ class KeyCancellationTests(unittest.TestCase):
measures = result['staves'][0]['measures']
change = measures[0]['attributes'][0] if local else measures[1]
self.assertEqual(change['key'], key_from_fifths(2) | {'cancel': 3})
class AuthoredKeyMetadataTests(unittest.TestCase):
def test_eb_authored_key_survives_every_generated_practice_variant(self):
with tempfile.TemporaryDirectory() as temp:
source, output = Path(temp) / 'source', Path(temp) / 'output'
text = xml_score(xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>')
source.mkdir()
path = source / '001.xml'
path.write_text(text, encoding='utf-8')
self.assertEqual(generate_practice_scores(source, output), 12)
for key in TARGET_KEYS:
with self.subTest(key=key):
result = load_json(output / '001' / key_file_name(key))
self.assertEqual(result['schema_version'], 3)
self.assertEqual(result['metadata']['authored_key_signature'], 'Eb')
self.assertEqual(result['metadata']['key_signature'], key)
self.assertEqual(result['metadata']['variant_tonic'], key)
self.assertEqual(path.read_text(encoding='utf-8'), text)
def test_modes_and_keyless_authored_labels_survive_variants(self):
for mode, fifths, label in [('minor', -3, 'C minor'), ('dorian', -2, 'C dorian'), ('mixolydian', -1, 'C mixolydian'), ('none', 0, 'No key')]:
source = parse(xml_score(xml_note(), f'<key><fifths>{fifths}</fifths><mode>{mode}</mode></key>'))
if mode == 'none':
source['metadata']['transpose_origin'] = parse_pitch_name('C')
before = copy.deepcopy(source)
base = practice_document(source)
self.assertEqual(base['metadata']['authored_key_signature'], label)
for key in TARGET_KEYS:
with self.subTest(mode=mode, key=key):
result = transpose_document(base, key)
validate_document(result)
self.assertEqual(result['metadata']['authored_key_signature'], label)
self.assertEqual(result['metadata']['key_signature'], 'No key' if mode == 'none' else f'{key} {mode}')
self.assertEqual(result['metadata']['variant_tonic'], key)
self.assertEqual(source, before)
def test_existing_authored_key_is_retained_when_practice_is_rebuilt(self):
source = parse(xml_score(xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
before = copy.deepcopy(source)
variant = transpose_document(practice_document(source), 'D')
result = transpose_document(practice_document(variant), 'F')
self.assertEqual(result['metadata']['authored_key_signature'], 'Eb')
self.assertEqual(result['metadata']['key_signature'], 'F')
self.assertEqual(source, before)
def test_schema3_bundles_accept_optional_authored_key(self):
source = parse(xml_score(xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
self.assertNotIn('authored_key_signature', source['metadata'])
for authored in (False, True):
with self.subTest(authored=authored), tempfile.TemporaryDirectory() as temp:
result = transpose_document(practice_document(source) if authored else source, 'D')
root = Path(temp)
path = root / 'variant.json'
write_json(path, result)
engrave_tree(root, FONT_ROOT)
bundle = json.loads(path.read_text(encoding='utf-8'))
self.assertEqual(bundle['schema_version'], 3)
self.assertEqual(bundle['music']['metadata']['key_signature'], 'D')
self.assertEqual(bundle['music']['metadata'].get('authored_key_signature'), 'Eb' if authored else None)
self.assertEqual(load_json(path), result)