Generation
This commit is contained in:
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 55 KiB |
+24
-17
@@ -161,7 +161,7 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
item = generator.accompaniment.context(measure, None)
|
||||
bar = quarter_bar(4, {0, 1})
|
||||
self.assertEqual(generator.accompaniment.sounding_spans(bar, item), ())
|
||||
self.assertEqual(generator.accompaniment.rank(bar, [(item, ())], False)[0], 1)
|
||||
self.assertEqual(generator.accompaniment.rank(bar, [(item, ())], 'upper')[0], 1)
|
||||
def test_melody_context_uses_audible_hidden_notes_but_not_cues(self):
|
||||
measure = melody_measures(1)[0]
|
||||
notes = measure['voices'][0]['notes']
|
||||
@@ -177,7 +177,7 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()
|
||||
measures = tests.test_musicxml.parse(xml_bars([content] * 4))['staves'][0]['measures']
|
||||
plan = generator.accompaniment.plan_section(measures, library)
|
||||
self.assertEqual(plan.acoustic, (sounding,) * 4)
|
||||
self.assertEqual(plan.acoustic_upper, (sounding,) * 4)
|
||||
self.assertEqual(plan.electric, (silent,) * 4)
|
||||
def test_section_repeats_four_bar_motif_with_at_most_one_beat_variation(self):
|
||||
for beats in (2, 3, 4, 5):
|
||||
@@ -185,7 +185,7 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
before = copy.deepcopy(measures)
|
||||
library = tests.rhythm_fixtures.library()
|
||||
plan = generator.accompaniment.plan_section(measures, library)
|
||||
for part in (plan.acoustic, plan.electric):
|
||||
for part in (plan.acoustic_bass, plan.acoustic_upper, plan.electric):
|
||||
with self.subTest(beats=beats, part=part):
|
||||
self.assertEqual(len(part), 12)
|
||||
self.assertEqual(part[:4], part[4:8])
|
||||
@@ -201,7 +201,7 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
measures = tests.test_musicxml.parse(xml_bars([content] * 4))['staves'][0]['measures']
|
||||
plan = generator.accompaniment.plan_section(measures, tests.rhythm_fixtures.library())
|
||||
for index in range(3):
|
||||
acoustic, electric = set(plan.acoustic[index].attacks()), set(plan.electric[index].attacks())
|
||||
acoustic, electric = set(plan.acoustic_upper[index].attacks()), set(plan.electric[index].attacks())
|
||||
self.assertTrue(acoustic)
|
||||
self.assertTrue(electric)
|
||||
self.assertFalse(acoustic & electric)
|
||||
@@ -212,11 +212,13 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
plan = generator.accompaniment.plan_section(measures, tests.rhythm_fixtures.library())
|
||||
for index, measure in enumerate(measures):
|
||||
melody = {generator.model.fraction(n['onset']) for n in measure['voices'][0]['notes']}
|
||||
for bars in (plan.acoustic, plan.electric):
|
||||
for bars in (plan.acoustic_bass, plan.acoustic_upper, plan.electric):
|
||||
events = generator.accompaniment.realize(measure, bars[index], None)
|
||||
attacks = {generator.model.fraction(n['onset']) for n in events if n['kind'] == 'note'}
|
||||
self.assertTrue(attacks, (index, bars))
|
||||
self.assertTrue(attacks & melody)
|
||||
if bars is not plan.electric:
|
||||
self.assertTrue(attacks, (index, bars))
|
||||
self.assertTrue(attacks <= melody)
|
||||
self.assertTrue(any(bar.attacks() for bar in plan.electric))
|
||||
def test_section_and_local_meter_changes_restart_the_motif(self):
|
||||
library = generator.rhythm.Library(tuple(generator.rhythm.Snippet(f'{beats}-4/pair', f'{beats}/4', (quarter_bar(beats, {0}), quarter_bar(beats, {1}))) for beats in (3, 4)))
|
||||
segments = [melody_measures(3), melody_measures(2, beats=3), melody_measures(3), melody_measures(4)]
|
||||
@@ -226,9 +228,10 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
start = 0
|
||||
for segment in segments:
|
||||
expected = generator.accompaniment.plan_section(segment, library)
|
||||
self.assertEqual(plan.acoustic[start:start + len(segment)], expected.acoustic)
|
||||
self.assertEqual(plan.acoustic_upper[start:start + len(segment)], expected.acoustic_upper)
|
||||
self.assertEqual(plan.acoustic_bass[start:start + len(segment)], expected.acoustic_bass)
|
||||
self.assertEqual(plan.electric[start:start + len(segment)], expected.electric)
|
||||
self.assertEqual(plan.acoustic[start].attacks(), (0,))
|
||||
self.assertEqual(plan.acoustic_upper[start].attacks(), (0,))
|
||||
start += len(segment)
|
||||
def test_mid_measure_meter_changes_and_unsupported_meters_fail(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(duration=1, kind='quarter') + '<attributes><time><beats>3</beats><beat-type>4</beat-type></time></attributes>' + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>')
|
||||
@@ -248,8 +251,8 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
||||
contexts = [generator.accompaniment.context(m, chord) for m, chord in zip(measures, previous)]
|
||||
self.assertEqual([item.intervals for item in contexts], [((0, 4),), ((0, 2),), ()])
|
||||
plan = generator.accompaniment.plan_section(measures, tests.rhythm_fixtures.library(), initial=initial)
|
||||
self.assertTrue(generator.accompaniment.sounding_attacks(plan.acoustic[0], contexts[0]))
|
||||
self.assertEqual(generator.accompaniment.sounding_attacks(plan.acoustic[2], contexts[2]), ())
|
||||
self.assertTrue(generator.accompaniment.sounding_attacks(plan.acoustic_upper[0], contexts[0]))
|
||||
self.assertEqual(generator.accompaniment.sounding_attacks(plan.acoustic_upper[2], contexts[2]), ())
|
||||
|
||||
class AccompanimentRealizationTests(unittest.TestCase):
|
||||
def test_harmonic_gating_removes_dangling_beams(self):
|
||||
@@ -294,7 +297,8 @@ class AccompanimentRealizationTests(unittest.TestCase):
|
||||
self.assertEqual(events[1]['written'], {'type': 'quarter', 'dots': 0})
|
||||
library = generator.rhythm.Library((generator.rhythm.Snippet('pickup', '4/4', (bar,)),))
|
||||
plan = generator.accompaniment.plan_section([measure], library)
|
||||
self.assertEqual(plan.acoustic, (bar,))
|
||||
self.assertEqual(plan.acoustic_upper, (bar,))
|
||||
self.assertEqual(plan.acoustic_bass, (bar,))
|
||||
self.assertEqual(plan.electric, (bar,))
|
||||
|
||||
class TiedAccompanimentTests(unittest.TestCase):
|
||||
@@ -326,7 +330,8 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
bars = library.for_meter('4/4')[0].bars
|
||||
self.assertEqual(bars[1].attacks(), ())
|
||||
plan = generator.accompaniment.plan_section(source['staves'][0]['measures'], library)
|
||||
self.assertEqual(plan.acoustic, bars)
|
||||
self.assertEqual(plan.acoustic_upper, bars)
|
||||
self.assertEqual(plan.acoustic_bass, bars)
|
||||
self.assertEqual(plan.electric, bars)
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=library)
|
||||
first, second = tests.test_musicxml.notes(result), tests.test_musicxml.notes(result, 1)
|
||||
@@ -337,11 +342,13 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
self.assertEqual(second[1]['pitches'], [])
|
||||
self.assertEqual(timing(second), expected)
|
||||
self.assert_tied_chord(first[0], second[0])
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for p in first[0]['pitches']}, {0, 4, 7})
|
||||
first_tones = [p for v in result['staves'][0]['measures'][0]['voices'] for n in v['notes'] for p in n['pitches']]
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for p in first_tones}, {0, 4, 7})
|
||||
self.assert_tied_chord(result['staves'][0]['measures'][0]['voices'][1]['notes'][0], result['staves'][0]['measures'][1]['voices'][1]['notes'][0])
|
||||
generator.model.validate_document(result)
|
||||
timeline = generator.playback.compile_timeline(result)
|
||||
self.assertEqual(generator.model.fraction(timeline['duration']), 8)
|
||||
self.assertEqual(len(timeline['events']), len(first[0]['pitches']))
|
||||
self.assertEqual(len(timeline['events']), len(first_tones))
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 and generator.model.fraction(e['duration']) == 4 + continuation_duration for e in timeline['events']))
|
||||
self.assertEqual(source, before)
|
||||
self.assertEqual(library.for_meter('4/4')[0].bars, bars)
|
||||
@@ -354,7 +361,7 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
first, second = tests.test_musicxml.notes(result), tests.test_musicxml.notes(result, 1)
|
||||
self.assertEqual(timing(first), [(0, 4, 'note')])
|
||||
self.assertEqual(timing(second), [(0, 2, 'rest'), (2, 2, 'rest')])
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for p in first[0]['pitches']}, {0, 4, 7})
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for v in result['staves'][0]['measures'][0]['voices'] for n in v['notes'] for p in n['pitches']}, {0, 4, 7})
|
||||
self.assertEqual(second[0]['pitches'], [])
|
||||
for note in first + second:
|
||||
for pitch in note['pitches']:
|
||||
@@ -374,5 +381,5 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
self.assert_tied_chord(first[0], second[0])
|
||||
generator.model.validate_document(result)
|
||||
events = generator.playback.compile_timeline(result)['events']
|
||||
self.assertEqual(len(events), len(first[0]['pitches']))
|
||||
self.assertEqual(len(events), sum(len(n['pitches']) for v in result['staves'][0]['measures'][0]['voices'] for n in v['notes']))
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 and generator.model.fraction(e['duration']) == 5 for e in events))
|
||||
|
||||
+62
-2
@@ -23,6 +23,7 @@ import generator.musicxml
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_accompaniment
|
||||
import tests.test_musicxml
|
||||
|
||||
class EngravingTests(unittest.TestCase):
|
||||
@@ -36,6 +37,65 @@ class EngravingTests(unittest.TestCase):
|
||||
return layout
|
||||
def primitives(self, layout, role):
|
||||
return [p for page in layout.pages for p in page.primitives if p.role == role]
|
||||
def test_tab_omits_rests_but_keeps_standard_notation(self):
|
||||
content = tests.test_musicxml.xml_note('E', 2, 'half', octave=5) + '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
|
||||
document = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
layout = self.layout(document)
|
||||
self.assertEqual(len(self.primitives(layout, 'rest')), 1)
|
||||
self.assertEqual(self.primitives(layout, 'tab-rest'), [])
|
||||
self.assertEqual(len(self.primitives(layout, 'tab')), 1)
|
||||
def assert_paired_pages(self, layout):
|
||||
self.assertTrue(all(len(page.systems) == 2 for page in layout.pages[:-1]))
|
||||
self.assertIn(len(layout.pages[-1].systems), (1, 2))
|
||||
for page in layout.pages:
|
||||
self.assertEqual(len({system.scale for system in page.systems}), 1)
|
||||
for first, second in zip(page.systems, page.systems[1:]):
|
||||
self.assertGreaterEqual(second.box.top - first.box.bottom, layout.style.system_gap - 0.001)
|
||||
def test_guitar_pages_always_pair_systems_including_tall_passages(self):
|
||||
content = tests.test_musicxml.xml_note('E', octave=5)
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 32))
|
||||
document = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
layout = self.layout(document)
|
||||
self.assert_paired_pages(layout)
|
||||
self.assertEqual(layout.style.space, 10)
|
||||
self.assertEqual(layout.style.tab_font_size, 12)
|
||||
wide = tests.test_musicxml.xml_note('E', octave=3) + tests.test_musicxml.xml_note('C', octave=7, prefix='<chord/>')
|
||||
document = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([wide] * 32))
|
||||
for measure in document['staves'][0]['measures']:
|
||||
measure['transpose']['octave_change'] = -1
|
||||
generator.guitar.place_guitar(document)
|
||||
tall = self.layout(document)
|
||||
self.assertGreater(len(tall.pages), 1)
|
||||
self.assert_paired_pages(tall)
|
||||
self.assertTrue(all(system.scale < 1 for page in tall.pages for system in page.systems))
|
||||
self.assertEqual(tall.style, layout.style)
|
||||
def test_vocal_pages_have_two_systems_and_an_odd_final_system(self):
|
||||
content = tests.test_musicxml.xml_note(extra='<lyric><syllabic>single</syllabic><text>doo</text></lyric>')
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 32))
|
||||
style = generator.engraving.types.Style()
|
||||
systems = generator.engraving.layout.layout_systems(source, style, self.fonts)
|
||||
self.assertGreaterEqual(len(systems), 3)
|
||||
pages = generator.engraving.layout.paginate(systems[:3], source, self.fonts, style)
|
||||
self.assertEqual([len(page.systems) for page in pages], [2, 1])
|
||||
self.assert_paired_pages(self.layout(source))
|
||||
def test_two_voice_acoustic_systems_can_share_a_page(self):
|
||||
harmony = '<harmony><root><root-step>E</root-step></root><kind>major</kind></harmony>'
|
||||
content = harmony + tests.test_musicxml.xml_note('G', 2, 'half', alter=1) + tests.test_musicxml.xml_note('B', 2, 'half', prefix='<chord/>') + '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
content += '<backup><duration>4</duration></backup>' + tests.test_musicxml.xml_note('E', extra='<voice>2</voice>')
|
||||
document = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 16))
|
||||
positions = {'e': {'string': 5, 'fret': 7}, 'g': {'string': 4, 'fret': 6}, 'b': {'string': 3, 'fret': 4}}
|
||||
for measure in document['staves'][0]['measures']:
|
||||
measure['transpose']['octave_change'] = -1
|
||||
for voice in measure['voices']:
|
||||
for note in voice['notes']:
|
||||
for pitch in note['pitches']:
|
||||
pitch['tablature'] = positions[pitch['letter']]
|
||||
generator.guitar.place_guitar(document)
|
||||
layout = self.layout(document)
|
||||
self.assert_paired_pages(layout)
|
||||
self.assertTrue(self.primitives(layout, 'rest'))
|
||||
self.assertFalse(self.primitives(layout, 'tab-rest'))
|
||||
def test_notation_fixture_visual_golden(self):
|
||||
layout = self.layout(generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/notation.xml')))
|
||||
self.assertEqual(len(layout.pages), 1)
|
||||
@@ -206,7 +266,7 @@ class EngravingTests(unittest.TestCase):
|
||||
def test_wide_pitch_range_can_break_inside_a_measure_vertically(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=2, kind='half', octave=10) + tests.test_musicxml.xml_note(duration=2, kind='half', octave=-1)))
|
||||
layout = self.layout(source)
|
||||
self.assertEqual(len(layout.pages), 2)
|
||||
self.assertEqual([len(page.systems) for page in layout.pages], [2])
|
||||
self.assertEqual(len(self.primitives(layout, 'head')), 2)
|
||||
def test_cue_grace_and_tremolo_are_drawn(self):
|
||||
grace = '<note><grace slash="yes"/><pitch><step>D</step><octave>4</octave></pitch><type>16th</type></note>'
|
||||
@@ -251,7 +311,7 @@ class EngravingTests(unittest.TestCase):
|
||||
for bar, tab in zip(notation, tabs):
|
||||
for (x1, y1), (x2, y2) in zip(bar.points, tab.points):
|
||||
self.assertAlmostEqual(x1, x2)
|
||||
self.assertAlmostEqual((y1 - baselines['staff']) * 5 / 4, y2 - baselines['tab-staff'])
|
||||
self.assertAlmostEqual((y1 - baselines['staff']) * 5 * layout.style.tab_space / (4 * layout.style.space), y2 - baselines['tab-staff'])
|
||||
def test_adjacent_string_tab_numerals_do_not_overlap(self):
|
||||
content = ''.join(tests.test_musicxml.xml_note(step, octave=octave, prefix='<chord/>' if index else '') for index, (step, octave) in enumerate((('G', 3), ('C', 4), ('E', 4), ('G', 4), ('C', 5), ('E', 5))))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import copy
|
||||
import fractions
|
||||
import unittest
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.arrange.generate
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.parts
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import generator.rhythm
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_accompaniment
|
||||
import tests.test_musicxml
|
||||
import tests.test_parts
|
||||
|
||||
class GuitarPartTests(unittest.TestCase):
|
||||
def practice(self, content, part='electric', divisions=1):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, divisions=divisions))
|
||||
result = generator.practice.generate.practice_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
return result
|
||||
def test_profile_shapes_cover_defining_chord_tones(self):
|
||||
for symbol in ('CM', 'Cm', 'C7', 'CmM7', 'Csus', 'Cdim7', 'Caug7', 'C7b9', 'C13', 'C5', 'CM/E'):
|
||||
for profile in ('acoustic', 'electric'):
|
||||
with self.subTest(symbol=symbol, profile=profile):
|
||||
definition = generator.harmony.parse_symbol(symbol)
|
||||
required, allowed, bass = generator.guitar.chord_requirements(definition, profile)
|
||||
candidates = generator.guitar.chord_positions(required, allowed, bass, profile=profile)
|
||||
self.assertTrue(candidates)
|
||||
for candidate in candidates:
|
||||
strings = tuple(p.string_number for p in candidate)
|
||||
self.assertIn(strings, generator.guitar.ACOUSTIC_STRINGS if profile == 'acoustic' else generator.guitar.ELECTRIC_STRINGS)
|
||||
self.assertTrue(set(required) <= {p.midi % 12 for p in candidate} <= set(allowed))
|
||||
self.assertTrue(generator.guitar.within_fret_span(candidate))
|
||||
chord = generator.harmony.parse_symbol('C7')
|
||||
self.assertNotIn(0, generator.guitar.chord_requirements(chord, 'electric')[0])
|
||||
candidates = generator.guitar.chord_positions(*generator.guitar.chord_requirements(chord, 'electric'), profile='electric')
|
||||
self.assertTrue(any(all(p.midi % 12 != 0 for p in c) for c in candidates))
|
||||
def test_acoustic_has_independent_voices_and_alternating_bass(self):
|
||||
result = self.practice(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
|
||||
measure = result['staves'][0]['measures'][0]
|
||||
upper, bass = measure['voices']
|
||||
self.assertEqual((upper['id'], bass['id']), ('upper', 'bass'))
|
||||
self.assertNotEqual(tests.test_accompaniment.timing(upper['notes']), tests.test_accompaniment.timing(bass['notes']))
|
||||
attacks = [n for n in bass['notes'] if n['pitches']]
|
||||
self.assertEqual([generator.model.pitch_number(n['pitches'][0]) % 12 for n in attacks], [0, 7])
|
||||
for voice in (upper, bass):
|
||||
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in voice['notes']), 4)
|
||||
for note in voice['notes']:
|
||||
self.assertEqual(note['stem'], 'up' if voice is upper else 'down')
|
||||
strings = sorted(p['tablature']['string'] for p in note['pitches'])
|
||||
if strings:
|
||||
if voice is bass:
|
||||
self.assertEqual(len(strings), 1)
|
||||
self.assertIn(strings[0], (5, 6))
|
||||
else:
|
||||
self.assertIn(strings, ([2, 3], [3, 4], [2, 3, 4]))
|
||||
timeline = generator.playback.compile_timeline(result)['events']
|
||||
by_id = {p['id']: p for v in measure['voices'] for n in v['notes'] for p in n['pitches']}
|
||||
for event in timeline:
|
||||
start = generator.model.fraction(event['onset'])
|
||||
simultaneous = [e for e in timeline if generator.model.fraction(e['onset']) <= start < generator.model.fraction(e['onset']) + generator.model.fraction(e['duration'])]
|
||||
positions = [generator.guitar.TabPosition(by_id[e['id']]['tablature']['string'], by_id[e['id']]['tablature']['fret'], int(generator.model.fraction(e['pitch']))) for e in simultaneous]
|
||||
self.assertEqual(len(positions), len({p.string_number for p in positions}))
|
||||
self.assertTrue(generator.guitar.within_fret_span(positions))
|
||||
def test_explicit_bass_and_fifth_fallback(self):
|
||||
for symbol, expected in (('CM/E', 4), ('C7b9', 0)):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
chord = generator.harmony.parse_symbol(symbol)
|
||||
source['staves'][0]['measures'][0]['harmonies'] = [{**chord, 'id': 'h1', 'onset': generator.model.rational(0)}]
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
bass = result['staves'][0]['measures'][0]['voices'][1]
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for n in bass['notes'] for p in n['pitches']}, {expected})
|
||||
def test_acoustic_bass_and_upper_chord_can_attack_together(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
prepared = generator.parts.prepare_document(source, practice=True)
|
||||
bar = tests.test_accompaniment.quarter_bar(4, {0, 2})
|
||||
plan = generator.accompaniment.Plan((bar,), (bar,), ())
|
||||
result = generator.practice.generate._practice_document(prepared, 'acoustic', plan)
|
||||
generator.model.validate_document(result)
|
||||
upper, bass = result['staves'][0]['measures'][0]['voices']
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in upper['notes'] if n['pitches']], [0, 2])
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in bass['notes'] if n['pitches']], [0, 2])
|
||||
for onset in (0, 2):
|
||||
pitches = [p for voice in (upper, bass) for n in voice['notes'] if generator.model.fraction(n['onset']) == onset for p in n['pitches']]
|
||||
positions = [generator.guitar.TabPosition(p['tablature']['string'], p['tablature']['fret'], int(generator.model.pitch_number(p) - 12)) for p in pitches]
|
||||
self.assertIn(tuple(sorted((p.string_number for p in positions), reverse=True)), generator.guitar.ACOUSTIC_STRINGS)
|
||||
self.assertTrue(generator.guitar.within_fret_span(positions))
|
||||
def test_long_melody_chords_and_short_notes(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 2, 'half', octave=5) + tests.test_musicxml.xml_note('G', 1, 'quarter', octave=5) + tests.test_musicxml.xml_note('C', 1, 'quarter', octave=6)
|
||||
notes = tests.test_musicxml.notes(self.practice(content))
|
||||
self.assertIn(len(notes[0]['pitches']), (3, 4))
|
||||
self.assertEqual([len(n['pitches']) for n in notes[1:]], [1, 1])
|
||||
self.assertEqual(generator.model.pitch_number(notes[0]['pitches'][0]), 76)
|
||||
self.assertIn(sorted(p['tablature']['string'] for p in notes[0]['pitches']), ([1, 2, 3], [1, 2, 3, 4]))
|
||||
def test_tied_quarters_qualify_and_keep_all_strings(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 1, 'quarter', octave=5, extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 1, 'quarter', octave=5, extra='<tie type="stop"/>') + '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
result = self.practice(content)
|
||||
first, second = tests.test_musicxml.notes(result)[:2]
|
||||
self.assertIn(len(first['pitches']), (3, 4))
|
||||
self.assertEqual(len(first['pitches']), len(second['pitches']))
|
||||
for before, after in zip(first['pitches'], second['pitches']):
|
||||
self.assertEqual(before['tie_to'], after['id'])
|
||||
self.assertEqual(after['tablature'], before['tablature'])
|
||||
self.assertEqual({generator.model.fraction(e['duration']) for e in generator.playback.compile_timeline(result)['events']}, {2})
|
||||
def test_chord_change_reattacks_even_for_final_eighth(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony('F', offset=7) + tests.test_musicxml.xml_note('G', 8, 'whole', octave=5)
|
||||
result = self.practice(content, divisions=2)
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual([generator.model.fraction(n['duration']) for n in notes], [fractions.Fraction(7, 2), fractions.Fraction(1, 2)])
|
||||
for note in notes:
|
||||
self.assertIn(len(note['pitches']), (3, 4))
|
||||
self.assertFalse(any('tie_from' in p or 'tie_to' in p for p in note['pitches']))
|
||||
self.assertEqual(generator.model.pitch_number(note['pitches'][0]), 79)
|
||||
self.assertEqual({generator.model.fraction(e['onset']) for e in generator.playback.compile_timeline(result)['events']}, {0, fractions.Fraction(7, 2)})
|
||||
def test_cross_measure_ties_continue_only_without_a_chord_change(self):
|
||||
for root in ('C', 'F'):
|
||||
with self.subTest(root=root):
|
||||
first = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', octave=5, extra='<tie type="start"/>')
|
||||
second = tests.test_parts.xml_harmony(root) + tests.test_musicxml.xml_note('E', octave=5, extra='<tie type="stop"/>')
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([first, second]))
|
||||
result = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] for n in v['notes']]
|
||||
self.assertTrue(all(len(n['pitches']) in (3, 4) for n in notes))
|
||||
self.assertEqual(bool(notes[0]['pitches'][0].get('tie_to')), root == 'C')
|
||||
self.assertEqual({generator.model.fraction(e['onset']) for e in generator.playback.compile_timeline(result)['events']}, {0} if root == 'C' else {0, 4})
|
||||
def test_split_tuplet_keeps_exact_timing_and_span_end(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony('F', offset=10)
|
||||
for index in range(3):
|
||||
extra = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes><normal-type>half</normal-type></time-modification>'
|
||||
if index != 1:
|
||||
extra += '<notations><tuplet type="' + ('start' if index == 0 else 'stop') + '"/></notations>'
|
||||
content += tests.test_musicxml.xml_note('E', 4, 'half', octave=5, extra=extra)
|
||||
result = self.practice(content, divisions=3)
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual([generator.model.fraction(n['duration']) for n in notes], [fractions.Fraction(4, 3)] * 2 + [fractions.Fraction(2, 3)] * 2)
|
||||
self.assertEqual(result['staves'][0]['tuplets'][0]['end'], notes[-1]['id'])
|
||||
self.assertTrue(all(len(n['pitches']) in (3, 4) for n in notes))
|
||||
self.assertFalse(notes[-1]['pitches'][0].get('tie_from'))
|
||||
def test_no_chord_releases_harmony_without_reattacking_melody(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony(kind='none', offset=2) + tests.test_musicxml.xml_note('E', octave=5)
|
||||
result = self.practice(content)
|
||||
first, second = tests.test_musicxml.notes(result)
|
||||
self.assertIn(len(first['pitches']), (3, 4))
|
||||
self.assertEqual(len(second['pitches']), 1)
|
||||
self.assertEqual(first['pitches'][0]['tie_to'], second['pitches'][0]['id'])
|
||||
timeline = generator.playback.compile_timeline(result)['events']
|
||||
self.assertEqual(sum(generator.model.fraction(e['duration']) == 4 for e in timeline), 1)
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 for e in timeline))
|
||||
def test_unplayable_harmonization_preserves_low_melody(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('C', octave=4)
|
||||
with self.assertLogs('generator.guitar', level='WARNING'):
|
||||
result = self.practice(content)
|
||||
note = tests.test_musicxml.notes(result)[0]
|
||||
self.assertEqual(len(note['pitches']), 1)
|
||||
self.assertEqual(note['pitches'][0]['tablature'], {'string': 5, 'fret': 3})
|
||||
def test_song_melody_only_is_raised_an_octave(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=4)))
|
||||
before = copy.deepcopy(source)
|
||||
for builder in (generator.practice.generate.practice_document, generator.arrange.generate.arrange_document):
|
||||
for part in ('electric', 'vocal'):
|
||||
result = builder(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] if n['pitches']]
|
||||
expected = 76 if part == 'electric' and builder is generator.arrange.generate.arrange_document else 64
|
||||
self.assertEqual({generator.model.pitch_number(n['pitches'][0]) for n in notes}, {expected})
|
||||
self.assertEqual(source, before)
|
||||
def test_electric_stabs_are_capped_and_do_not_continue(self):
|
||||
source = tests.test_accompaniment.melody_measures(1)[0]
|
||||
for continuation in (False, True):
|
||||
bar = generator.rhythm.Bar((generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(4), True, 'whole', continuation=continuation),), fractions.Fraction(4))
|
||||
events = generator.accompaniment.realize(source, bar, None, lane='electric')
|
||||
self.assertEqual(tests.test_accompaniment.timing(events), [(0, 4, 'rest')] if continuation else [(0, 1, 'note'), (1, 3, 'rest')])
|
||||
+4
-3
@@ -73,7 +73,7 @@ class PartGenerationTests(unittest.TestCase):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(xml_harmony() + xml_harmony('F') + tests.test_musicxml.xml_note(duration=duration, kind=kind, extra='<dot/>' if duration == 3 else ''), attributes, implicit=implicit))
|
||||
measure = source['staves'][0]['measures'][0]
|
||||
plan = generator.accompaniment.plan_section([measure], tests.rhythm_fixtures.library(), electric=False)
|
||||
events = generator.accompaniment.realize(measure, plan.acoustic[0], None)
|
||||
events = generator.accompaniment.realize(measure, plan.acoustic_upper[0], None)
|
||||
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in events), duration)
|
||||
sounding = [n for n in events if n['kind'] == 'note']
|
||||
self.assertTrue(sounding)
|
||||
@@ -115,7 +115,7 @@ class PartGenerationTests(unittest.TestCase):
|
||||
self.assertEqual(prepared, before)
|
||||
self.assertEqual(source, original)
|
||||
def test_melody_written_pitch_rhythm_rests_and_cross_bar_ties_are_preserved(self):
|
||||
content = xml_harmony() + xml_harmony('F', offset=3)
|
||||
content = ''
|
||||
content += tests.test_musicxml.xml_note(duration=2, kind='quarter', alter=1, extra='<tie type="start"/>')
|
||||
content += tests.test_musicxml.xml_note(duration=6, kind='half', alter=1, extra='<dot/><tie type="stop"/><tie type="start"/>')
|
||||
root = xml.etree.ElementTree.fromstring(tests.test_musicxml.xml_score(content, divisions=2))
|
||||
@@ -142,7 +142,8 @@ class PartGenerationTests(unittest.TestCase):
|
||||
self.assertEqual(generated[key], original[key])
|
||||
self.assertEqual(len(generated['pitches']), len(original['pitches']))
|
||||
for pitch, tone in zip(original['pitches'], generated['pitches']):
|
||||
self.assertEqual(generator.model.pitch_number(tone), generator.model.pitch_number(pitch))
|
||||
octave = 12 if part == 'electric' and builder is generator.arrange.generate.arrange_document else 0
|
||||
self.assertEqual(generator.model.pitch_number(tone), generator.model.pitch_number(pitch) + octave)
|
||||
self.assertEqual(tone['written'], pitch['written'])
|
||||
for key in ('tie_from', 'tie_to'):
|
||||
self.assertEqual(key in tone, key in pitch)
|
||||
|
||||
Reference in New Issue
Block a user