Files
product-vamp/tests/test_training_io.py
khodowany e15dc31568
ci / checks (push) Successful in 49m36s
Remove XML Solo Gen
2026-10-06 04:27:29 -05:00

81 lines
5.7 KiB
Python

"""Literal training-score import, timing, pitch convention and TAB checks."""
import copy
import fractions
import pathlib
import tempfile
import unittest
import generator.musicxml_writer
import generator.score
import generator.training_io
import generator.solo.notation
import generator.solo.placement
import tests.solo_fixtures
def xml(notes, *, transpose=False, leadin=''):
return '<score-partwise version="4.0"><part-list><score-part id="P"><part-name>Vocal</part-name></score-part></part-list><part id="P">' + leadin + '<measure number="1"><attributes><divisions>6</divisions><key><fifths>0</fifths></key><time><beats>4</beats><beat-type>4</beat-type></time><clef><sign>G</sign><line>2</line></clef>' + ('<transpose><chromatic>0</chromatic><octave-change>-1</octave-change></transpose>' if transpose else '') + '</attributes><harmony><root><root-step>C</root-step></root><kind>major-seventh</kind></harmony>' + notes + '</measure></part></score-partwise>'
def pitched(duration, tie='', octave=5):
return f'<note><pitch><step>C</step><octave>{octave}</octave></pitch><duration>{duration}</duration>{tie}<type>quarter</type></note>'
def events(review):
return [(n['start'], n['end'], n['sounding_midi']) for n in review['sounding_events']]
class TrainingXmlTests(unittest.TestCase):
def read(self, directory, name, content, **kwargs):
path = pathlib.Path(directory) / name
path.write_text(content)
return generator.training_io.read(path, 'fixture/acoustic', **kwargs)
def test_note_container_does_not_turn_color_label_into_fingering(self):
with tempfile.TemporaryDirectory() as root:
note_xml = pitched(24).replace('</note>', '<notations><technical><fingering>6</fingering></technical></notations></note>')
r = self.read(root, 'annotation.xml', xml(note_xml), form_start=1)
self.assertEqual(r['annotations'][0]['text'], '6')
self.assertEqual(r['annotations'][0]['container'], 'notations/technical/fingering')
self.assertIsNone(r['notes'][0]['tablature'])
def test_ties_not_reattacks_and_one_guitar_transposition(self):
with tempfile.TemporaryDirectory() as root:
a = self.read(root, 'actual.xml', xml(pitched(24), transpose=True), form_start=1)
b = self.read(root, 'authored.xml', xml(pitched(12, '<tie type="start"/>') + pitched(12, '<tie type="stop"/>')), form_start=1, pitch_convention='guitar-written')
self.assertEqual(events(a), [('0', '4', '60')])
self.assertEqual(events(a), events(b))
repeated = self.read(root, 'repeat.xml', xml(pitched(12) + pitched(12)), form_start=1, pitch_convention='guitar-written')
self.assertEqual(events(repeated), [('0', '2', '60'), ('2', '4', '60')])
def test_leadin_is_reported_without_harmony_or_alignment_padding(self):
with tempfile.TemporaryDirectory() as root:
prelude = '<measure number="0"><attributes><divisions>6</divisions></attributes>' + pitched(6) + '<note><rest/><duration>18</duration></note></measure>'
r = self.read(root, 'leadin.xml', xml(pitched(24), leadin=prelude), form_start=2, pitch_convention='guitar-written')
self.assertEqual(events(r), [('0', '1', '60'), ('4', '8', '60')])
self.assertEqual(r['alignment']['encoded_offset'], '4')
self.assertIn('uncertainty', r['notes'][0]['harmonic_spans'][0])
def test_unplayable_pitch_is_reported_not_folded(self):
with tempfile.TemporaryDirectory() as root:
r = self.read(root, 'high.xml', xml(pitched(24, octave=7)), form_start=1, pitch_convention='guitar-written')
self.assertEqual(r['sounding_events'][0]['sounding_midi'], '84')
self.assertFalse(r['notes'][0]['legal_positions_at_sounding_pitch'])
self.assertEqual(r['notes'][0]['pitch']['octave'], 7)
def test_triplet_time_and_annotations_are_literal(self):
with tempfile.TemporaryDirectory() as root:
triplet = pitched(2).replace('<type>quarter</type>', '<type>eighth</type><time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>')
r = self.read(root, 'thirds.xml', xml('<direction><direction-type><words>Lead in</words></direction-type></direction>' + triplet * 3 + '<note><rest/><duration>18</duration></note>'), form_start=1)
self.assertEqual([n['start'] for n in r['sounding_events']], ['0', '1/3', '2/3'])
self.assertEqual(r['annotations'][0]['text'], 'Lead in')
def test_roundtrip_preserves_actual_sounding_events_and_tab(self):
document = tests.solo_fixtures.song(count=2, chords=('CM7',))
raw = (tests.solo_fixtures.note(0, fractions.Fraction(1, 3), 'c'), tests.solo_fixtures.note(fractions.Fraction(1, 3), fractions.Fraction(2, 3), 'd'), tests.solo_fixtures.note(fractions.Fraction(2, 3), 1, 'e'), tests.solo_fixtures.note(fractions.Fraction(7, 2), 5, 'g'), tests.solo_fixtures.note(5, 6, 'g'))
placed = generator.solo.placement.place(raw, fractions.Fraction(120))[1]
generator.solo.notation.replace_region(document, 0, generator.solo.notation.measures(document['staves'][0]['measures'], placed, 'roundtrip'))
before = copy.deepcopy(document)
with tempfile.TemporaryDirectory() as root:
path = pathlib.Path(root) / 'fixture.xml'
generator.musicxml_writer.write_musicxml(path, document, tablature=True)
loaded = generator.training_io.read(path, 'fixture/acoustic', 1)
sounded = [(fractions.Fraction(n['start']), fractions.Fraction(n['end']), fractions.Fraction(n['sounding_midi'])) for n in loaded['sounding_events']]
self.assertEqual([(n.start, n.end, n.pitch.number()) for n in raw], sounded)
tabs = [p['tablature'] for m in document['staves'][0]['measures'] for n in generator.score.measure_notes(m) for p in n['pitches']]
self.assertEqual(tabs, [n['tablature'] for n in loaded['notes'] if n['pitch']])
self.assertEqual(before, document)
if __name__ == '__main__':
unittest.main()