Fret Limit
This commit is contained in:
@@ -100,6 +100,8 @@ class EngravingTests(unittest.TestCase):
|
||||
document = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([wide] * 32))
|
||||
for measure in document['staves'][0]['measures']:
|
||||
measure['transpose']['octave_change'] = -1
|
||||
# Keep the tall written range while sounding inside the single-note limit.
|
||||
measure['attributes'].append({'onset': generator.model.rational(2), 'transpose': {**measure['transpose'], 'octave_change': -2}})
|
||||
generator.guitar.place_guitar(document)
|
||||
tall = self.layout(document)
|
||||
self.assertGreater(len(tall.pages), 1)
|
||||
@@ -380,8 +382,8 @@ class EngravingTests(unittest.TestCase):
|
||||
percussion = '<note><unpitched><display-step>C</display-step><display-octave>5</display-octave></unpitched><duration>4</duration><type>whole</type><notehead>x</notehead></note>'
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(percussion, attributes='<clef><sign>percussion</sign></clef>'))
|
||||
self.assertEqual(self.primitives(self.layout(source), 'head')[0].value, 'noteheadXWhole')
|
||||
tab = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('C', octave=6))), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(self.primitives(self.layout(tab), 'tab')[0].value, '20')
|
||||
tab = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=5))), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(self.primitives(self.layout(tab), 'tab')[0].value, '12')
|
||||
def test_generated_tab_shares_columns_and_systems_without_duplicating_music(self):
|
||||
content = ''.join(tests.test_musicxml.xml_note('G', 1, 'eighth') for _ in range(8))
|
||||
source = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 8, divisions=2)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import copy
|
||||
import fractions
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import generator.acoustic
|
||||
import generator.acoustic_voicing
|
||||
import generator.electric
|
||||
import generator.fretboard
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.score
|
||||
import generator.solo.analysis
|
||||
import generator.solo.planning
|
||||
import generator.solo.placement
|
||||
import generator.solo.realization
|
||||
import tests.solo_fixtures
|
||||
import tests.test_acoustic_voicing
|
||||
import tests.test_musicxml
|
||||
import tests.test_solo
|
||||
import tests.test_solo_development
|
||||
|
||||
def position(string, fret):
|
||||
return generator.fretboard.TabPosition(string, fret, dict(generator.fretboard.STANDARD_TUNING)[string] + fret)
|
||||
|
||||
def document(positions):
|
||||
result = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
note = tests.test_musicxml.notes(result)[0]
|
||||
spellings = (('c', 0), ('c', 1), ('d', 0), ('e', -1), ('e', 0), ('f', 0), ('f', 1), ('g', 0), ('a', -1), ('a', 0), ('b', -1), ('b', 0))
|
||||
pitches = []
|
||||
for p in positions:
|
||||
letter, alter = spellings[p.midi % 12]
|
||||
pitches.append({'letter': letter, 'alter': alter, 'octave': p.midi // 12 - 1, 'tablature': {'string': p.string_number, 'fret': p.fret}})
|
||||
note.update(generator.model.make_event(note['id'], fractions.Fraction(), fractions.Fraction(4), 'note', pitches))
|
||||
result['staves'][0]['tablature'] = True
|
||||
return result
|
||||
|
||||
class FretPolicyTests(unittest.TestCase):
|
||||
def test_single_limits_and_alternate_string(self):
|
||||
for midi, expected in ((40, False), (41, True), (72, True), (76, True), (77, False), (84, False)):
|
||||
with self.subTest(midi=midi):
|
||||
positions = generator.fretboard.single_positions_for_pitch(fractions.Fraction(midi))
|
||||
self.assertEqual(bool(positions), expected)
|
||||
self.assertTrue(all(1 <= p.fret <= 12 for p in positions))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('C', octave=5)))
|
||||
generator.guitar.place_guitar(source)
|
||||
self.assertEqual(tests.test_musicxml.notes(source)[0]['pitches'][0]['tablature'], {'string': 1, 'fret': 8})
|
||||
def test_explicit_high_single_and_unreachable_pitch_fail_without_rewriting(self):
|
||||
for p in (position(2, 13), position(1, 13)):
|
||||
source = document((p,))
|
||||
before = copy.deepcopy(source)
|
||||
with self.assertRaisesRegex(ValueError, 'single notes need frets 1-12'):
|
||||
generator.guitar.place_guitar(source)
|
||||
self.assertEqual(source, before)
|
||||
with self.assertRaisesRegex(ValueError, 'single notes require frets 1-12'):
|
||||
generator.fretboard.validate_document(source)
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('F', octave=5)))
|
||||
before = copy.deepcopy(source)
|
||||
with self.assertRaisesRegex(ValueError, 'single notes need frets 1-12'):
|
||||
generator.guitar.place_guitar(source)
|
||||
self.assertEqual(source, before)
|
||||
def test_chord_anchor_and_span_are_hard_constraints(self):
|
||||
for frets, valid in (((12, 14), True), ((13, 14), False), ((12, 15), False), ((0, 2), False)):
|
||||
with self.subTest(frets=frets):
|
||||
source = document(tuple(position(s, f) for s, f in zip((2, 1), frets)))
|
||||
if valid:
|
||||
generator.guitar.place_guitar(source)
|
||||
generator.model.validate_document(source)
|
||||
generator.fretboard.validate_document(source)
|
||||
else:
|
||||
with self.assertRaises(ValueError):
|
||||
generator.guitar.place_guitar(source)
|
||||
with self.assertRaises(ValueError):
|
||||
generator.fretboard.validate_document(source)
|
||||
def test_high_chord_tail_cannot_outlast_anchor(self):
|
||||
source = document((position(2, 12), position(1, 14)))
|
||||
anchor = tests.test_musicxml.notes(source)[0]['pitches'][0]
|
||||
anchor.update(duration=generator.model.rational(2), written=generator.model.written_for(fractions.Fraction(2)))
|
||||
generator.model.validate_document(source)
|
||||
with self.assertRaisesRegex(ValueError, 'held/tied tails'):
|
||||
generator.fretboard.validate_document(source)
|
||||
with self.assertRaisesRegex(ValueError, 'no guitar placement'):
|
||||
generator.guitar.place_guitar(source)
|
||||
def test_notation_ties_preserve_chord_policy_and_fixed_positions(self):
|
||||
source = document((position(2, 12), position(1, 14)))
|
||||
original = tests.test_musicxml.notes(source)[0]
|
||||
split = [generator.model.make_event(f'split_{i}', fractions.Fraction(2 * i), fractions.Fraction(2), 'note', original['pitches']) for i in range(2)]
|
||||
for a, b in zip(split[0]['pitches'], split[1]['pitches']):
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
source['staves'][0]['measures'][0]['voices'][0]['notes'] = split
|
||||
generator.guitar.place_guitar(source)
|
||||
generator.model.validate_document(source)
|
||||
generator.fretboard.validate_document(source)
|
||||
self.assertEqual([p['tablature'] for p in split[0]['pitches']], [p['tablature'] for p in split[1]['pitches']])
|
||||
split[0]['pitches'][0].pop('tie_to')
|
||||
split[1]['pitches'].pop(0)
|
||||
generator.model.validate_document(source)
|
||||
with self.assertRaisesRegex(ValueError, 'held/tied tails'):
|
||||
generator.fretboard.validate_document(source)
|
||||
def test_another_voice_cannot_supply_a_single_notes_anchor(self):
|
||||
source = document((position(2, 12), position(1, 13)))
|
||||
measure = source['staves'][0]['measures'][0]
|
||||
pitches = tests.test_musicxml.notes(source)[0]['pitches']
|
||||
measure['voices'] = [{'id': f'voice_{i}', 'notes': [generator.model.make_event(f'n_{i}', fractions.Fraction(), fractions.Fraction(4), 'note', [p])]} for i, p in enumerate(pitches)]
|
||||
with self.assertRaisesRegex(ValueError, 'single notes require'):
|
||||
generator.fretboard.validate_document(source)
|
||||
with self.assertRaisesRegex(ValueError, 'no guitar placement'):
|
||||
generator.guitar.place_guitar(source)
|
||||
def test_harmonization_fallback_obeys_single_limit(self):
|
||||
for p, valid in ((position(2, 13), False), (position(1, 8), True)):
|
||||
source = document((p,))
|
||||
note = tests.test_musicxml.notes(source)[0]
|
||||
note.update(_harmony=generator.harmony.parse_symbol('CM'), _guitar_profile='electric', _harmonize=True)
|
||||
with unittest.mock.patch('generator.guitar.chord_positions', return_value=()):
|
||||
if valid:
|
||||
with self.assertLogs('generator.guitar', level='WARNING'):
|
||||
generator.guitar.place_guitar(source)
|
||||
self.assertEqual(len(note['pitches']), 1)
|
||||
else:
|
||||
with self.assertRaisesRegex(ValueError, 'single notes need frets 1-12'):
|
||||
generator.guitar.place_guitar(source)
|
||||
def test_acoustic_played_parts_need_independent_anchors(self):
|
||||
source = tests.test_acoustic_voicing.planned_document([[(0, 'CM')]])
|
||||
spans = generator.acoustic.harmony_spans(source)
|
||||
for frets in ((13, 12, 12, 12), (12, 13, 13, 13)):
|
||||
grip = tests.test_acoustic_voicing.voicing(frets)
|
||||
self.assertTrue(generator.acoustic_voicing.playable(grip))
|
||||
with unittest.mock.patch('generator.acoustic_voicing.candidates', return_value=(grip,)):
|
||||
with self.assertRaisesRegex(ValueError, 'played bass notes.*played upper chords'):
|
||||
generator.acoustic_voicing.select(spans)
|
||||
generator.acoustic_voicing.place(source)
|
||||
generator.fretboard.validate_document(source)
|
||||
def test_copied_unanchored_voicing_cannot_be_published(self):
|
||||
source = document(tuple(position(s, 13) for s in (3, 2, 1)))
|
||||
target = document(tuple(position(s, 12) for s in (3, 2, 1)))
|
||||
reference, note = tests.test_musicxml.notes(source)[0], tests.test_musicxml.notes(target)[0]
|
||||
generator.electric.reuse_voicings([generator.electric.VoicingLink(note, (reference,))])
|
||||
generator.model.validate_document(target)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = pathlib.Path(directory) / 'score.json'
|
||||
with self.assertRaisesRegex(ValueError, 'each sounding chord requires an anchor'):
|
||||
generator.score.write_json(output, target)
|
||||
self.assertFalse(output.exists())
|
||||
def test_solo_candidates_and_anchors_are_reachable_before_placement(self):
|
||||
choices = tests.test_solo_development.choices(end=32)
|
||||
options = generator.solo.realization.options(choices, fractions.Fraction(), fractions.Fraction(4), (59, 84))
|
||||
self.assertTrue(options)
|
||||
self.assertLessEqual(max(p.number() for p in options), 76)
|
||||
source = tests.test_solo.song(count=8, chords=('CM7',))
|
||||
plans = generator.solo.planning.plans(generator.solo.analysis.analyze(source), source['staves'][0]['measures'], choices, fractions.Fraction(32), 'electric', tests.solo_fixtures.library())
|
||||
self.assertTrue(plans)
|
||||
for plan in plans:
|
||||
self.assertLessEqual(plan.register[1], 76)
|
||||
self.assertTrue(plan.anchor_options)
|
||||
for anchors in plan.anchor_options:
|
||||
self.assertTrue(all(generator.solo.placement.positions(a.pitch.number()) for a in anchors))
|
||||
motif = generator.solo.planning.new_motif(tests.solo_fixtures.library(), fractions.Fraction(4), choices=choices, register=(70, 84), seed='high')
|
||||
self.assertTrue(all(generator.solo.placement.positions(n.pitch.number()) for n in motif.notes))
|
||||
high = tests.test_solo_development.note(0, 1, 'f', octave=5)
|
||||
self.assertIsNone(generator.solo.placement.place((high,), fractions.Fraction(120)))
|
||||
@@ -86,6 +86,7 @@ class GuitarTests(unittest.TestCase):
|
||||
self.assertTrue(covered <= set(requirements.allowed))
|
||||
self.assertEqual(len(candidate), len({p.string_number for p in candidate}))
|
||||
self.assertTrue(generator.fretboard.within_fret_span(candidate))
|
||||
self.assertTrue(generator.fretboard.has_fret_anchor(candidate))
|
||||
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)
|
||||
@@ -147,7 +148,7 @@ class GuitarTests(unittest.TestCase):
|
||||
document = tests.arrangement_fixtures.section_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(generator.model.fraction(generator.playback.compile_timeline(document)['events'][0]['pitch']), expected + (12 if part == 'electric' else 0))
|
||||
def test_written_middle_c_and_c_sharp_six_have_matching_tab_and_audio(self):
|
||||
for octave, alter, expected, tab in ((4, 0, 48, {'string': 5, 'fret': 3}), (6, 1, 73, {'string': 1, 'fret': 9}), (7, 0, 84, {'string': 1, 'fret': 20})):
|
||||
for octave, alter, expected, tab in ((4, 0, 48, {'string': 5, 'fret': 3}), (6, 1, 73, {'string': 1, 'fret': 9})):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(octave=octave - 1, alter=alter)))
|
||||
document = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
tone = tests.test_musicxml.notes(document)[0]['pitches'][0]
|
||||
@@ -197,10 +198,10 @@ class GuitarTests(unittest.TestCase):
|
||||
generator.guitar.place_guitar(document)
|
||||
self.assertEqual(document, before)
|
||||
def test_proximity_chooses_a_path_without_relocating_tied_notes(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('B', 2, 'half', octave=5) + tests.test_musicxml.xml_note('C', 2, 'half', octave=6)))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('D', 2, 'half', octave=5) + tests.test_musicxml.xml_note('E', 2, 'half', octave=5)))
|
||||
generator.guitar.place_guitar(source)
|
||||
tabs = [n['pitches'][0]['tablature'] for n in tests.test_musicxml.notes(source)]
|
||||
self.assertEqual(tabs, [{'string': 1, 'fret': 19}, {'string': 1, 'fret': 20}])
|
||||
self.assertEqual(tabs, [{'string': 1, 'fret': 10}, {'string': 1, 'fret': 12}])
|
||||
def test_grid_search_matches_exhaustive_costs_and_tie_breaks(self):
|
||||
randomizer = random.Random(17)
|
||||
states = {}
|
||||
@@ -216,7 +217,7 @@ class GuitarTests(unittest.TestCase):
|
||||
candidates.append((state.cost.moved(abs(x - a) + abs(y - b)), index))
|
||||
self.assertEqual(actual[:2], min(candidates))
|
||||
def test_generated_chord_retains_strings_when_a_melody_enters(self):
|
||||
content = tests.test_musicxml.xml_note() + '<backup><duration>4</duration></backup><forward><duration>2</duration></forward>' + tests.test_musicxml.xml_note('C', 2, 'half', octave=7, extra='<voice>2</voice>')
|
||||
content = tests.test_musicxml.xml_note() + '<backup><duration>4</duration></backup><forward><duration>2</duration></forward>' + tests.test_musicxml.xml_note('E', 2, 'half', octave=6, extra='<voice>2</voice>')
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
|
||||
measure = document['staves'][0]['measures'][0]
|
||||
measure['transpose']['octave_change'] = -1
|
||||
@@ -225,9 +226,9 @@ class GuitarTests(unittest.TestCase):
|
||||
chord['_harmony'] = generator.harmony.parse_symbol('CM')
|
||||
generator.guitar.place_guitar(document)
|
||||
generator.model.validate_document(document)
|
||||
self.assertEqual(melody['pitches'][0]['tablature'], {'string': 1, 'fret': 20})
|
||||
self.assertEqual(melody['pitches'][0]['tablature'], {'string': 1, 'fret': 12})
|
||||
self.assertNotIn(1, [p['tablature']['string'] for p in chord['pitches']])
|
||||
self.assertEqual(len(generator.playback.compile_timeline(document)['events']), 4)
|
||||
self.assertEqual(len(generator.playback.compile_timeline(document)['events']), 3)
|
||||
def test_more_than_six_simultaneous_tones_fail_without_omission(self):
|
||||
content = '<backup><duration>4</duration></backup>'.join(tests.test_musicxml.xml_note(extra=f'<voice>{i}</voice>') for i in range(7))
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ class SoloTests(unittest.TestCase):
|
||||
generator.solo.composer.validate(result)
|
||||
self.assertEqual(result.end, meter * 3)
|
||||
self.assertTrue(result.notes())
|
||||
self.assertTrue(all(1 <= n.position.fret <= 20 for n in result.notes()))
|
||||
self.assertTrue(all(1 <= n.position.fret <= 12 for n in result.notes()))
|
||||
self.assertEqual(result, compose(source))
|
||||
def test_motif_recurrence_and_whole_fragment_octave_quote(self):
|
||||
source = song(count=6, chords=('CM7',))
|
||||
|
||||
@@ -101,17 +101,14 @@ class VoicingTests(unittest.TestCase):
|
||||
crossing = grip((12, 13, 14, 14))
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing).effort, 6)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'electric').effort, 10)
|
||||
def test_high_melody_keeps_its_exact_pitch_when_no_anchored_chord_fits(self):
|
||||
def test_high_melody_fails_when_neither_anchored_chord_nor_single_note_fits(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('C', octave=6)))
|
||||
note = tests.test_musicxml.notes(document)[0]
|
||||
note.update(_harmony=generator.harmony.parse_symbol('CM'), _guitar_profile='electric', _harmonize=True)
|
||||
before = copy.deepcopy(note['pitches'])
|
||||
with self.assertLogs('generator.guitar', level='WARNING') as messages:
|
||||
before = copy.deepcopy(document)
|
||||
with self.assertRaisesRegex(ValueError, 'single notes need frets 1-12'):
|
||||
generator.guitar.place_guitar(document)
|
||||
self.assertIn('1 melody harmonizations kept as single notes', messages.output[0])
|
||||
self.assertEqual([{k: v for k, v in p.items() if k != 'tablature'} for p in note['pitches']], before)
|
||||
self.assertEqual(note['pitches'][0]['tablature'], {'string': 1, 'fret': 20})
|
||||
generator.model.validate_document(document)
|
||||
self.assertEqual(document, before)
|
||||
def test_shape_and_movement_share_one_effort_cost(self):
|
||||
shape = grip((1, 2, 3, 3))
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape).effort, 2)
|
||||
|
||||
Reference in New Issue
Block a user