Construction

This commit is contained in:
khodowany
2026-10-07 23:45:17 -05:00
parent 3a01b4b6bd
commit 841a3f974e
8 changed files with 236 additions and 80 deletions
+16 -10
View File
@@ -35,12 +35,13 @@ class AcousticPairs(unittest.TestCase):
self.assertEqual(tuple(e.start for e in events), tuple(map(F, onsets)))
def test_pairing_uses_attack_spacing_not_written_duration(self):
events = self.assign(('0', '1/2'), ('1', '1'))
self.assertEqual([(e.lane, e.end) for e in events], [('bass', F(1)), ('upper', F(3, 2))])
# Bass keeps ringing, without another bass attack under the upper chord.
self.assertGreater(events[0].end, events[1].start)
def test_same_strings_release_but_different_strings_can_ring(self):
self.assertEqual([(e.lane, e.end) for e in events], [('bass', F(1, 2)), ('upper', F(3, 2))])
self.assertEqual(events[0].end, events[1].start)
def test_each_attack_releases_previous_sound_in_both_directions(self):
events = self.assign(('0', '1/2', '1'), ('2', '2', '1'))
self.assertEqual([(e.lane, e.end) for e in events], [('bass', F(1)), ('upper', F(1)), ('combined', F(2))])
self.assertEqual([(e.lane, e.end) for e in events], [('bass', F(1, 2)), ('upper', F(1)), ('combined', F(2))])
events = self.assign(('0', '1/2', '1', '3/2'), ('2', '2', '2', '2'))
self.assertEqual([(e.lane, e.end) for e in events], [('bass', F(1, 2)), ('upper', F(1)), ('bass', F(3, 2)), ('upper', F(7, 2))])
events = self.assign(('0', '1/2'), ('1', '3'))
self.assertEqual(events[-1].end, F(7, 2))
def test_chord_change_breaks_pair_but_repeated_label_does_not(self):
@@ -66,17 +67,17 @@ class AcousticPairs(unittest.TestCase):
{'onset': str(t), 'duration': '1', 'layer': 'acoustic'} for t in (F(beats) - F(1, 2), F(beats))]}
events, _ = generator.comping_rhythm.fit(pattern, flat, None, 'acoustic')
self.assertEqual([e.lane for e in events], ['bass', 'upper'])
self.assertEqual(events[0].end, F(beats) + F(1, 2))
def test_anticipated_pair_plays_same_future_harmony(self):
self.assertEqual(events[0].end, F(beats))
def test_release_rebinds_expired_anticipation_to_direct_harmony(self):
source = tests.solo_fixtures.song(count=2, chords=('C7', 'FM7'))
flat = generator.comping_rhythm.flatten(tuple((m, None, i) for i, m in enumerate(source['staves'][0]['measures'])))
pattern = {'lengths': ['4', '4'], 'events': [
{'onset': '7/2', 'duration': '1', 'layer': 'acoustic', 'anticipate': True},
{'onset': '4', 'duration': '1', 'layer': 'acoustic'}]}
events, _ = generator.comping_rhythm.fit(pattern, flat, None, 'acoustic', anticipate=True)
self.assertEqual([e.lane for e in events], ['bass', 'upper'])
self.assertEqual(events[0].harmonic_time, 4)
self.assertGreater(events[0].end, 4)
self.assertEqual([e.lane for e in events], ['combined', 'combined'])
self.assertIsNone(events[0].harmonic_time)
self.assertEqual(events[0].end, 4)
# A participation break prevents pairing; an expired anticipation must
# revert to the current harmony rather than keep an unresolved label.
raw, _ = generator.comping_rhythm.fit(pattern, flat, None, 'acoustic', anticipate=True, assign=False)
@@ -84,6 +85,11 @@ class AcousticPairs(unittest.TestCase):
self.assertEqual([e.lane for e in broken], ['combined', 'combined'])
self.assertIsNone(broken[0].harmonic_time)
self.assertEqual(broken[0].end, 4)
# With no intervening attack the bounded anticipated hold stays intact.
pattern['events'].pop()
held, _ = generator.comping_rhythm.fit(pattern, flat, None, 'acoustic', anticipate=True)
self.assertEqual(held[0].harmonic_time, 4)
self.assertEqual(held[0].end, F(9, 2))
def resources(self):
resources = copy.deepcopy(generator.vocabulary.load())
policy = resources['accompaniment']['instruments']['acoustic']