Generation
This commit is contained in:
+76
-22
@@ -21,46 +21,100 @@ import tests.test_musicxml
|
||||
|
||||
class GuitarTests(unittest.TestCase):
|
||||
def test_fret_boundaries_and_span(self):
|
||||
self.assertIn(generator.guitar.TabPosition(6, 0, 40), generator.guitar.positions_for_pitch(fractions.Fraction(40)))
|
||||
self.assertFalse(generator.guitar.positions_for_pitch(fractions.Fraction(40)))
|
||||
self.assertIn(generator.guitar.TabPosition(6, 1, 41), generator.guitar.positions_for_pitch(fractions.Fraction(41)))
|
||||
self.assertIn(generator.guitar.TabPosition(1, 20, 84), generator.guitar.positions_for_pitch(fractions.Fraction(84)))
|
||||
self.assertFalse(generator.guitar.positions_for_pitch(fractions.Fraction(85)))
|
||||
self.assertFalse(generator.guitar.positions_for_pitch(fractions.Fraction(129, 2)))
|
||||
self.assertTrue(generator.guitar.within_fret_span((generator.guitar.TabPosition(4, 11, 61), generator.guitar.TabPosition(3, 12, 67), generator.guitar.TabPosition(2, 14, 73))))
|
||||
self.assertFalse(generator.guitar.within_fret_span((generator.guitar.TabPosition(6, 0, 40), generator.guitar.TabPosition(1, 20, 84))))
|
||||
self.assertFalse(generator.guitar.within_fret_span((generator.guitar.TabPosition(4, 10, 60), generator.guitar.TabPosition(1, 20, 84))))
|
||||
def test_open_strings_count_in_span_and_proximity(self):
|
||||
for frets, valid in (((0, 2, 3), True), ((0, 4), True), ((0, 5), False), ((12, 0, 11), False), ((0, 0), True)):
|
||||
def test_fretted_positions_keep_span_and_proximity_scoring(self):
|
||||
for frets, valid in (((1, 2, 3), True), ((1, 5), True), ((1, 6), False), ((12, 1, 11), False), ((3, 3), True)):
|
||||
with self.subTest(frets=frets):
|
||||
positions = tuple(generator.guitar.TabPosition(6 - i, fret, 0) for i, fret in enumerate(frets))
|
||||
self.assertEqual(generator.guitar.within_fret_span(positions), valid)
|
||||
self.assertEqual(generator.guitar.compactness(positions), max(frets) - min(frets))
|
||||
self.assertEqual(generator.guitar.position_center(positions)[0], min(frets) + max(frets))
|
||||
self.assertTrue(generator.guitar.within_fret_span(()))
|
||||
def test_held_open_string_cannot_mix_with_high_frets(self):
|
||||
held = tests.test_musicxml.xml_note('E', octave=2)
|
||||
high = tests.test_musicxml.xml_note('C', 2, 'half', octave=6, extra='<voice>2</voice>')
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(held + '<backup><duration>4</duration></backup><forward><duration>2</duration></forward>' + high))
|
||||
with self.assertRaisesRegex(ValueError, 'no guitar placement'):
|
||||
def test_low_open_e_fails_without_octave_fitting_or_mutation(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=2)))
|
||||
before = copy.deepcopy(document)
|
||||
with self.assertRaisesRegex(ValueError, 'measure-1, event .*pitch 40.*1-20'):
|
||||
generator.guitar.place_guitar(document)
|
||||
self.assertEqual(document, before)
|
||||
def test_open_string_pitch_uses_a_fretted_alternative(self):
|
||||
for _, midi in generator.guitar.STANDARD_TUNING[1:]:
|
||||
positions = generator.guitar.positions_for_pitch(fractions.Fraction(midi))
|
||||
self.assertTrue(positions)
|
||||
self.assertTrue(all(p.fret >= 1 and p.midi == midi for p in positions))
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=4)))
|
||||
generator.guitar.place_guitar(document)
|
||||
tone = tests.test_musicxml.notes(document)[0]['pitches'][0]
|
||||
self.assertGreater(tone['tablature']['fret'], 0)
|
||||
self.assertEqual(generator.model.pitch_number(tone), 64)
|
||||
def test_explicit_open_tab_is_rejected_without_substitution(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=4)))
|
||||
tests.test_musicxml.notes(document)[0]['pitches'][0]['tablature'] = {'string': 1, 'fret': 0}
|
||||
before = copy.deepcopy(document)
|
||||
with self.assertRaisesRegex(ValueError, 'event .*1-20'):
|
||||
generator.guitar.place_guitar(document)
|
||||
self.assertEqual(document, before)
|
||||
document['staves'][0]['tablature'] = True
|
||||
with self.assertRaisesRegex(ValueError, 'invalid generated tablature'):
|
||||
generator.model.validate_document(document)
|
||||
def test_theory_controls_omissions_and_bass(self):
|
||||
for symbol in ('CM', 'G7b9', 'FM7#11/A', 'CmM7', 'Cdim7', 'C13', 'Csus', 'Cmadd9'):
|
||||
with self.subTest(symbol=symbol):
|
||||
required, allowed, bass = generator.guitar.chord_requirements(generator.harmony.parse_symbol(symbol))
|
||||
candidates = generator.guitar.chord_positions(required, allowed, bass)
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol(symbol))
|
||||
candidates = generator.guitar.chord_positions(requirements)
|
||||
self.assertTrue(candidates)
|
||||
for candidate in candidates:
|
||||
covered = {p.midi % 12 for p in candidate}
|
||||
self.assertTrue(set(required) <= covered <= set(allowed))
|
||||
self.assertTrue(requirements.covers(covered))
|
||||
self.assertTrue(covered <= set(requirements.allowed))
|
||||
self.assertEqual(len(candidate), len({p.string_number for p in candidate}))
|
||||
self.assertTrue(generator.guitar.within_fret_span(candidate))
|
||||
if bass is not None:
|
||||
self.assertEqual(min(p.midi for p in candidate) % 12, bass)
|
||||
self.assertIn(2, generator.guitar.chord_requirements(generator.harmony.parse_symbol('Cmadd9'))[0])
|
||||
self.assertNotIn(7, generator.guitar.chord_requirements(generator.harmony.parse_symbol('C7'))[0])
|
||||
self.assertIn(7, generator.guitar.chord_requirements(generator.harmony.parse_symbol('CM'))[0])
|
||||
self.assertEqual(generator.guitar.chord_requirements({**generator.harmony.parse_symbol('CM'), 'inversion': 0})[2], 0)
|
||||
self.assertEqual(generator.guitar.chord_requirements({**generator.harmony.parse_symbol('CM'), 'inversion': 1})[2], 4)
|
||||
self.assertTrue(any(p.fret > 12 for c in generator.guitar.chord_positions(*generator.guitar.chord_requirements(generator.harmony.parse_symbol('CM'))) for p in c))
|
||||
self.assertTrue(all(1 <= p.fret <= 20 for p in candidate))
|
||||
if requirements.basses:
|
||||
self.assertIn(min(p.midi for p in candidate) % 12, requirements.basses)
|
||||
self.assertIn(2, generator.guitar.chord_requirements(generator.harmony.parse_symbol('Cmadd9')).required)
|
||||
self.assertNotIn(7, generator.guitar.chord_requirements(generator.harmony.parse_symbol('C7')).required)
|
||||
self.assertEqual(generator.guitar.chord_requirements(generator.harmony.parse_symbol('CM')).foundation, (0, 7))
|
||||
self.assertEqual(generator.guitar.chord_requirements({**generator.harmony.parse_symbol('CM'), 'inversion': 0}).basses, (0,))
|
||||
self.assertEqual(generator.guitar.chord_requirements({**generator.harmony.parse_symbol('CM'), 'inversion': 1}).basses, (4,))
|
||||
self.assertTrue(any(p.fret > 12 for c in generator.guitar.chord_positions(generator.guitar.chord_requirements(generator.harmony.parse_symbol('CM'))) for p in c))
|
||||
def test_root_or_fifth_is_required_for_every_generated_profile(self):
|
||||
for profile in ('', 'acoustic', 'electric'):
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol('C7'), profile)
|
||||
for classes in ({0, 4, 10}, {4, 7, 10}, {0, 4, 7, 10}):
|
||||
self.assertTrue(requirements.covers(classes))
|
||||
self.assertFalse(requirements.covers({4, 10}))
|
||||
candidates = generator.guitar.chord_positions(requirements, prune=False, profile=profile)
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all({p.midi % 12 for p in shape} & {0, 7} for shape in candidates))
|
||||
self.assertEqual({frozenset({p.midi % 12 for p in shape} & {0, 7}) for shape in candidates}, {frozenset({0}), frozenset({7}), frozenset({0, 7})})
|
||||
def test_altered_and_removed_fifths_keep_their_semantics(self):
|
||||
for symbol, fifth in (('Cdim7', 6), ('Caug7', 8)):
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol(symbol), 'electric')
|
||||
self.assertEqual(requirements.foundation, (0, fifth))
|
||||
self.assertIn(fifth, requirements.required)
|
||||
self.assertNotIn(7, requirements.allowed)
|
||||
for omitted, remaining in ((1, 7), (5, 0)):
|
||||
chord = generator.harmony.parse_symbol('C7')
|
||||
chord['degrees'].append({'value': omitted, 'alter': 0, 'type': 'subtract'})
|
||||
for profile in ('acoustic', 'electric'):
|
||||
requirements = generator.guitar.chord_requirements(chord, profile)
|
||||
self.assertEqual(requirements.foundation, (remaining,))
|
||||
self.assertTrue(all(remaining in {p.midi % 12 for p in c} for c in generator.guitar.chord_positions(requirements, profile=profile)))
|
||||
chord['degrees'].append({'value': 1, 'alter': 0, 'type': 'subtract'})
|
||||
with self.assertRaisesRegex(ValueError, 'root or fifth'):
|
||||
generator.guitar.chord_requirements(chord)
|
||||
def test_explicit_degree_chord_uses_the_same_root_or_fifth_rule(self):
|
||||
standard = generator.harmony.parse_symbol('C7')
|
||||
explicit = {**standard, 'kind': 'other', 'degrees': [{'value': d, 'alter': a, 'type': 'add'} for d, a in generator.harmony.degrees(standard).items()]}
|
||||
for profile in ('', 'acoustic', 'electric'):
|
||||
self.assertEqual(generator.guitar.chord_requirements(standard, profile), generator.guitar.chord_requirements(explicit, profile))
|
||||
def test_melody_placement_preserves_notes_timing_and_sound(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', 2, 'half', octave=4) + tests.test_musicxml.xml_note('F', 2, 'half', octave=4)))
|
||||
before = copy.deepcopy(source)
|
||||
@@ -161,7 +215,7 @@ class GuitarTests(unittest.TestCase):
|
||||
generator.guitar.place_guitar(document)
|
||||
self.assertEqual(before, document)
|
||||
def test_source_contract_and_unplayable_music_fail(self):
|
||||
cases = [(tests.test_musicxml.xml_note(octave=7, alter=1), '', '0-20'), (tests.test_musicxml.xml_note(alter='0.5'), '', '0-20'),
|
||||
cases = [(tests.test_musicxml.xml_note(octave=7, alter=1), '', '1-20'), (tests.test_musicxml.xml_note(alter='0.5'), '', '1-20'),
|
||||
(tests.test_musicxml.xml_note(), '<staff-details><capo>1</capo></staff-details>', 'capo')]
|
||||
for content, attributes, error in cases:
|
||||
with self.subTest(error=error), self.assertRaisesRegex(ValueError, error):
|
||||
@@ -187,13 +241,13 @@ class GuitarTests(unittest.TestCase):
|
||||
output.mkdir()
|
||||
(output / 'last-good').write_text('preserved')
|
||||
args = argparse.Namespace(data_dir=root / 'data', output_dir=output, static_assets_dir=pathlib.Path('package/assets'), check=False)
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part electric:.*0-20'):
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part electric:.*1-20'):
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
|
||||
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||
harmony = '<harmony><root><root-step>C</root-step></root><kind>dominant</kind><degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree><degree><degree-value>11</degree-value><degree-alter>1</degree-alter><degree-type>add</degree-type></degree></harmony>'
|
||||
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score(harmony + tests.test_musicxml.xml_note()))
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part acoustic:.*measure 1.*no acoustic grip.*0-20'):
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part acoustic:.*measure 1.*no acoustic grip.*1-20'):
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
|
||||
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||
|
||||
Reference in New Issue
Block a user