Construction
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Independent rhythm contracts, including real overlapping guitar voices."""
|
||||
"""Independent rhythm contracts and their complete sounding durations."""
|
||||
import copy
|
||||
import dataclasses
|
||||
import fractions
|
||||
@@ -53,14 +53,13 @@ class LayeredComping(unittest.TestCase):
|
||||
self.assertTrue(any(c.events[0].start > 0 for c in choices['acoustic']))
|
||||
self.assertTrue(all(c.events[0].lane in ('bass', 'combined') for c in choices['acoustic'] if c.events))
|
||||
self.assertTrue(any(e.end - e.start > 1 for c in choices['electric'] for e in c.events))
|
||||
def test_separate_lanes_can_overlap_and_short_combined_attacks_are_valid(self):
|
||||
def test_separate_attacks_cannot_overlap_and_combined_attacks_are_valid(self):
|
||||
F = fractions.Fraction
|
||||
attack = generator.comping_rhythm.Attack
|
||||
generator.comping_rhythm.validate_events((attack('bass', F(0), F(1)), attack('upper', F(1), F(3, 2))), 'acoustic')
|
||||
generator.comping_rhythm.validate_events((attack('combined', F(0), F(3, 2)),), 'acoustic')
|
||||
generator.comping_rhythm.validate_events((attack('combined', F(0), F(1)),), 'acoustic')
|
||||
generator.comping_rhythm.validate_events((attack('bass', F(0), F(1)), attack('upper', F(1, 2), F(3, 2))), 'acoustic')
|
||||
for events in ((attack('combined', F(0), F(1)), attack('upper', F(1, 2), F(1))), (attack('bass', F(0), F(1)), attack('bass', F(1, 2), F(1))), (attack('bass', F(0), F(1, 3)),)):
|
||||
for events in ((attack('bass', F(0), F(1)), attack('upper', F(1, 2), F(3, 2))), (attack('combined', F(0), F(1)), attack('upper', F(1, 2), F(1))), (attack('bass', F(0), F(1)), attack('bass', F(1, 2), F(1))), (attack('bass', F(0), F(1, 3)),)):
|
||||
with self.assertRaises(ValueError):
|
||||
generator.comping_rhythm.validate_events(events, 'acoustic')
|
||||
def test_bass_first_keeps_pattern_timing_and_does_not_reset_at_barlines(self):
|
||||
@@ -72,7 +71,10 @@ class LayeredComping(unittest.TestCase):
|
||||
if pattern['lengths'] != ['4', '4']:
|
||||
continue
|
||||
events, _ = generator.comping_rhythm.fit(pattern, flat, None, 'acoustic')
|
||||
self.assertEqual([(e.start, e.end - e.start) for e in events], [(fractions.Fraction(r['onset']), fractions.Fraction(r['duration'])) for r in pattern['events']])
|
||||
self.assertEqual([e.start for e in events], [fractions.Fraction(r['onset']) for r in pattern['events']])
|
||||
for i, (e, r) in enumerate(zip(events, pattern['events'])):
|
||||
intended = e.start + fractions.Fraction(r['duration'])
|
||||
self.assertEqual(e.end, min(intended, events[i + 1].start) if i + 1 < len(events) else intended)
|
||||
if events:
|
||||
self.assertIn(events[0].lane, ('bass', 'combined'))
|
||||
pattern = {'lengths': ['4', '4'], 'events': [{'onset': '0', 'duration': '1', 'layer': 'bass'}, {'onset': '4', 'duration': '1', 'layer': 'upper'}]}
|
||||
@@ -104,7 +106,7 @@ class LayeredComping(unittest.TestCase):
|
||||
pattern = {'id': 'test.delayed', 'family': 'call_response', 'lengths': ['4', '4'], 'events': [
|
||||
{'onset': '1', 'duration': '1/2', 'layer': 'upper'}, {'onset': '2', 'duration': '1', 'layer': 'bass'},
|
||||
{'onset': '5', 'duration': '1', 'layer': 'combined'}]}
|
||||
# A long combined event remains available alongside short overlaps.
|
||||
# A long combined event remains available alongside short attacks.
|
||||
pattern['events'][-1]['duration'] = '2'
|
||||
resources = copy.deepcopy(self.resources)
|
||||
resources['accompaniment']['instruments']['acoustic']['patterns'] = [pattern]
|
||||
@@ -117,6 +119,21 @@ class LayeredComping(unittest.TestCase):
|
||||
first['voices'] = [v for v in first['voices'] if v['id'] != 'bass']
|
||||
with self.assertRaisesRegex(ValueError, 'first acoustic attack'):
|
||||
generator.acoustic.validate_comping(source)
|
||||
def test_completed_score_rejects_bass_under_upper_before_and_after_merge(self):
|
||||
source = tests.solo_fixtures.song(count=1, chords=('C7',))
|
||||
source['staves'][0]['arrangement_part_type'] = 'acoustic'
|
||||
source['staves'][0]['measures'][0]['voices'] = [
|
||||
{'id': 'bass', 'notes': [generator.model.make_event('bass', fractions.Fraction(), fractions.Fraction(1), 'note', [
|
||||
{'letter': 'c', 'alter': 0, 'octave': 3, 'tablature': {'string': 5, 'fret': 3}}])]},
|
||||
{'id': 'upper', 'notes': [generator.model.make_event('upper', fractions.Fraction(1, 2), fractions.Fraction(1), 'note', [
|
||||
{'letter': 'e', 'alter': 0, 'octave': 3, 'tablature': {'string': 4, 'fret': 2}}])]}]
|
||||
with self.assertRaisesRegex(ValueError, 'release before the next attack'):
|
||||
generator.acoustic.validate_comping(source)
|
||||
generator.acoustic.combine(source)
|
||||
# The generic merger preserves imported overlaps faithfully. Generated
|
||||
# accompaniment must fail even when the overlap is encoded using ties.
|
||||
with self.assertRaisesRegex(ValueError, 'release before the next attack'):
|
||||
generator.acoustic.validate_comping(source)
|
||||
def test_harmonic_changes_clip_direct_holds_without_extra_attacks(self):
|
||||
source = copy.deepcopy(self.source)
|
||||
for m in source['staves'][0]['measures'][:2]:
|
||||
@@ -178,7 +195,7 @@ class LayeredComping(unittest.TestCase):
|
||||
self.assertEqual(sounding(first['document']), sounding(second['document']))
|
||||
self.assertEqual(first['details'], second['details'])
|
||||
self.assertNotEqual(first['trace']['cache_key'], second['trace']['cache_key'])
|
||||
def test_assembly_keeps_overlaps_and_canonical_reuse(self):
|
||||
def test_assembly_keeps_releases_and_canonical_reuse(self):
|
||||
source = generator.production.SourceAnalysis.read(self.source)
|
||||
results = {role: generator.production.generate_role(source, role, pathlib.Path('data')) for role in generator.production.ROLES if not role.endswith('_solo')}
|
||||
# Silence in source-defined solo slots is sufficient to test union;
|
||||
|
||||
Reference in New Issue
Block a user