This commit is contained in:
khodowany
2026-10-03 12:44:53 -05:00
parent 59afd78205
commit 04c6ea2e00
25 changed files with 673 additions and 185 deletions
+1 -1
View File
@@ -184,7 +184,7 @@ class ScaleOutputTests(unittest.TestCase):
def test_only_soloing_part_is_annotated_without_revoicing(self):
content = '<harmony><root><root-step>C</root-step></root><kind>major</kind></harmony>' + tests.test_musicxml.xml_note()
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
roles = {'acoustic': ('slash', 'accompaniment'), 'electric': ('rest', 'slash'), 'vocal': ('rest', 'rest')}
roles = {'acoustic': ('slash', 'accompaniment'), 'electric': ('accompaniment', 'slash'), 'vocal': ('rest', 'rest')}
for part in ('acoustic', 'electric', 'vocal'):
before = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library())
result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library(), scales=tests.scale_fixtures.library())
+108
View File
@@ -0,0 +1,108 @@
import copy
import dataclasses
from fractions import Fraction as F
import unittest
import generator.arrange.generate
import generator.arrange.form
import generator.model
import generator.parts
import generator.solo.arrangement
from generator.solo import analysis, profiles, placement, themes, types, harmonic, gesture_rhythm, phrasing
from tests.test_solo import song
from tests.test_solo_development import note
from tests.test_solo_passages import choices
import tests.rhythm_fixtures
def events(measure):
return tuple((generator.model.fraction(n['onset']), generator.model.fraction(n['duration']),
tuple((p['letter'],p['alter']['numerator'],p['octave'],p['tablature']['string'],p['tablature']['fret']) for p in n['pitches']))
for v in measure['voices'] for n in v['notes'])
class ReusableAccompaniment(unittest.TestCase):
def test_corresponding_bars_repeat_rhythm_pitch_and_fingering(self):
source=song(count=12,chords=('CM7','Dm7','G7'))
library=tests.rhythm_fixtures.library()
prepared=generator.parts.prepare_document(source)
plans=generator.arrange.generate.plan_arrangement(prepared,library)
for part in ('acoustic','electric'):
result=generator.arrange.generate._arrange_document(prepared,part,plans)
seen={};offset=0;checked=0
sections=generator.arrange.form.full_arrangement_sections(source['staves'][0]['measures'],part)
for section in sections:
for j,m in enumerate(result['staves'][0]['measures'][offset:offset+section.measure_count]):
if section.role!='accompaniment':continue
key=section.source_measure_index+j
if key in seen:
self.assertEqual(events(m),seen[key]);checked+=1
seen[key]=events(m)
self.assertFalse(any('tie_from' in p or 'tie_to' in p for v in m['voices'] for n in v['notes'] for p in n['pitches']))
offset+=section.measure_count
self.assertGreater(checked,12)
generator.model.validate_document(result)
def test_excerpt_rhythms_retain_form_phase(self):
source=generator.parts.prepare_document(song(count=13,chords=('CM7',)))
plans=generator.arrange.generate.plan_arrangement(source,tests.rhythm_fixtures.library())
for lane in ('acoustic_bass','acoustic_upper','electric'):
full=getattr(plans[1],lane)
self.assertEqual(getattr(plans[0],lane),full[-8:])
self.assertEqual(getattr(plans[6],lane),full[-8:])
self.assertEqual(getattr(plans[3],lane),full)
self.assertEqual(getattr(plans[4],lane),full)
class SoloInstrumentAndIdentity(unittest.TestCase):
def test_role_ranges_and_no_open_strings(self):
acoustic=profiles.for_part('acoustic');electric=profiles.for_part('electric')
self.assertEqual(acoustic.register,(41,71));self.assertEqual(electric.register,(51,76))
self.assertTrue(acoustic.permits(41));self.assertFalse(electric.permits(50))
self.assertFalse(acoustic.permits(72))
for profile in (acoustic,electric):
for midi in range(40,78):
for p in placement.positions(midi,profile):
self.assertIn(p.string_number,profile.strings)
self.assertTrue(1<=p.fret<=12)
self.assertTrue(any(p.string_number==1 for p in placement.positions(65,electric)))
self.assertFalse(any(p.string_number==1 for p in placement.positions(65,acoustic)))
def test_sounding_melody_reference_and_octave_relation(self):
source=analysis.analyze(generator.parts.prepare_document(song(count=4,chords=('CM7',))))
acoustic=profiles.for_part('acoustic',source);electric=profiles.for_part('electric',source)
self.assertEqual(electric.reference-acoustic.reference,12)
self.assertEqual(electric.reference,profiles.weighted_center(source.melody)+12)
self.assertEqual(acoustic.cost(acoustic.reference+4),0)
self.assertGreater(acoustic.cost(acoustic.reference+9),0)
def test_return_keeps_opening_instead_of_later_largest_turn(self):
notes=tuple(note(i,i+1,p,event_id=str(i)) for i,p in enumerate(('d','f','g','f','c','b')))
home=types.RealizedMotif('home','seed',notes,F(6),'introduce','contour',signature=notes[:4])
reference=themes.home_reference(home,'return')
self.assertEqual(reference.notes,notes[:4])
self.assertEqual(reference.treatment,'return')
self.assertEqual(len(themes.home_reference(home,'develop_contour').notes),3)
def test_endings_distinguish_local_cadence_and_handoff(self):
harmony=harmonic.Harmony(choices(('Em7b5','A7','Dm','Dm')))
self.assertEqual(phrasing.ending_effect(F(2),harmony),'local_settle')
self.assertEqual(phrasing.ending_effect(F(9),harmony),'cadential_arrival')
self.assertEqual(phrasing.ending_effect(F(15),harmony,True),'handoff')
anchor=types.Anchor('comma',F(1),F(1),note(0,1,'g').pitch,frozenset({7}),(41,76),F(2),'terminal','Em7b5',hold_until=F(3))
self.assertEqual(set(phrasing.next_destination(anchor,harmony)),{2,5,9})
def test_rhythm_revision_connects_without_changing_attacks_or_arrival(self):
notes=(note(0,1,'g',event_id='a'),note(F(3,2),2,'e',event_id='b'),note(F(5,2),3,'g',event_id='c'),note(3,6,'e',event_id='end'))
anchor=types.Anchor('end',F(3),F(3),notes[-1].pitch,frozenset({4}),(41,76),F(3),'terminal','C7',hold_until=F(6))
draft=types.MelodicDraft('gesture',F(),F(6),notes,(),(),(),(anchor,),None,'develop',sustained_tails=(types.SustainedTail(F(3),F(6),64),))
revised,edits=gesture_rhythm.revise(draft,harmonic.Harmony(choices(('C7','C7'))))
self.assertTrue(edits)
self.assertEqual(tuple(n.pitch for n in revised.notes),tuple(n.pitch for n in notes))
self.assertEqual(revised.notes[-1],notes[-1])
self.assertLess(gesture_rhythm.fragmentation(revised.notes),gesture_rhythm.fragmentation(notes))
protected=dataclasses.replace(draft,protected=tuple(n.event_id for n in notes))
self.assertEqual(gesture_rhythm.revise(protected,harmonic.Harmony(choices(('C7','C7'))))[0].notes,notes)
if __name__=='__main__':unittest.main()
+3
View File
@@ -210,6 +210,9 @@ class SoloTests(unittest.TestCase):
old, new = before[part]['staves'][0]['measures'], parts[part]['staves'][0]['measures']
self.assertEqual(old[:low], new[:low])
self.assertEqual(old[high:], new[high:])
support = 4 if part == 'acoustic' else 3
first, count, _, _ = regions[support]
self.assertEqual(old[first:first+count],new[first:first+count])
for section in (3, 4):
first, count, _, _ = regions[section]
self.assertTrue(any(n['pitches'] for m in new[first:first + count] for n in generator.score.measure_notes(m)))
+12
View File
@@ -24,6 +24,18 @@ def fixture(symbol='C7',target=None,scale=None):
class EnclosureContracts(unittest.TestCase):
def test_existing_upper_can_continue_without_a_new_attack(self):
h,notes,c=fixture('Dm',note(4,8,'d'))
upper=dataclasses.replace(notes[0],start=F(5,2))
contract=dataclasses.replace(c,upper=upper,approach_start=F(3))
self.assertTrue(h.evaluate((upper,*notes[1:]),enclosures=(contract,)).valid)
# Joining a tie must not hide an exposed long non-chord upper neighbor.
long_upper=dataclasses.replace(upper,start=F(1))
invalid=dataclasses.replace(contract,upper=long_upper)
self.assertFalse(h.evaluate((long_upper,*notes[1:]),enclosures=(invalid,)).valid)
split=(dataclasses.replace(upper,end=F(3)),dataclasses.replace(upper,start=F(3),continuation=True,event_id='tie'),*notes[1:])
self.assertEqual(h.evaluate(split,enclosures=(contract,)),h.evaluate((upper,*notes[1:]),enclosures=(contract,)))
def test_worked_examples_and_actual_collection_membership(self):
hm=tests.scale_fixtures.library().scale('harmonic_minor',1,'D')
for symbol,target,scale,expected in (