Sepearate Generation

This commit is contained in:
khodowany
2026-10-04 09:05:25 -05:00
parent e8ac7b5edd
commit 8fdd736e13
56 changed files with 1739 additions and 341 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ add_test(
)
# Complete deterministic phrase searches make this suite CPU-sensitive;
# leave headroom for concurrent asset generation on development machines.
set_tests_properties(product_vamp_generator PROPERTIES TIMEOUT 300)
set_tests_properties(product_vamp_generator PROPERTIES TIMEOUT 600)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
+19
View File
@@ -0,0 +1,19 @@
"""Small synthetic catalogs for infrastructure tests, never production fallback.
Dependency/process tests separately exercise the real data path without mocks.
"""
from unittest.mock import patch
from generator import production, vocabulary
from tests import rhythm_fixtures, scale_fixtures, solo_fixtures
def install(test):
resources=vocabulary.load()
libraries=(scale_fixtures.library(),solo_fixtures.library(),rhythm_fixtures.library())
tree=production.generate_tree
def serial(*args,**kwargs):
kwargs['jobs']=1
return tree(*args,**kwargs)
test.enterContext(patch('generator.production.generate_tree',side_effect=serial))
test.enterContext(patch('generator.vocabulary.load',return_value=resources))
test.enterContext(patch('generator.production._solo_libraries',return_value=libraries))
test.enterContext(patch('generator.production._resource_key',return_value={'fixture':'independently authored synthetic catalogs'}))
+3 -4
View File
@@ -53,11 +53,11 @@ def held_half_bar(sounding=True):
), fractions.Fraction(4))
class RhythmLibraryTests(unittest.TestCase):
def test_unbracketed_time_modifications_are_rejected(self):
def test_unbracketed_time_modifications_preserve_exact_ratios(self):
extra = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
source = xml_bars([tests.test_musicxml.xml_note(duration=2, kind='quarter', extra=extra) * 6], divisions=3)
with self.assertRaisesRegex(ValueError, 'time modifications'):
self.load({'4_4/1/ratio.xml': source})
library=self.load({'4_4/1/ratio.xml': source})
self.assertTrue(all(e.ratio==fractions.Fraction(2,3) for e in library.for_meter('4/4')[0].bars[0].events))
def load(self, sources):
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
@@ -169,7 +169,6 @@ class RhythmLibraryTests(unittest.TestCase):
'grace': xml_bars(['<note><grace/><pitch><step>D</step><octave>4</octave></pitch><type>eighth</type></note>' + whole]),
'spacer': xml_bars(['<forward><duration>1</duration><voice>1</voice></forward>' + quarter * 3]),
'tremolo': xml_bars([tests.test_musicxml.xml_note(extra='<notations><ornaments><tremolo type="single">1</tremolo></ornaments></notations>')]),
'tuplet': xml_bars([triplet + tests.test_musicxml.xml_note(duration=9, kind='half', extra='<dot/>')], divisions=3),
'malformed XML': '<broken',
'wrong root': '<score-timewise version="4.0"/>',
}
+3
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import json
import pathlib
import tempfile
@@ -10,6 +11,8 @@ import tests.solo_fixtures
import tests.test_musicxml
class CatalogTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
def test_songs_generate_only_authored_key_parts(self):
with tempfile.TemporaryDirectory() as temporary:
source, output = pathlib.Path(temporary) / 'songs', pathlib.Path(temporary) / 'output'
+4 -26
View File
@@ -106,33 +106,11 @@ class ElectricVoicingTests(unittest.TestCase):
before = copy.deepcopy(target)
generator.electric.reuse_voicings(links)
self.assertEqual(target, before)
def test_real_arrangement_reuses_requested_grips_without_changing_melody(self):
def test_arrangement_never_reuses_generated_melody_grips(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml'))
before = copy.deepcopy(source)
rhythms = tests.rhythm_fixtures.library()
with unittest.mock.patch('generator.electric.reuse_voicings'):
independent = generator.arrange.generate.arrange_document(source, 'electric', rhythms=rhythms)
result = generator.arrange.generate.arrange_document(source, 'electric', rhythms=rhythms)
with unittest.mock.patch('generator.electric.reuse_voicings', side_effect=AssertionError('cross-part grip dependency')) as reuse:
result = generator.arrange.generate.arrange_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
generator.model.validate_document(result)
reuse.assert_not_called()
self.assertEqual(source, before)
start, checked = 0, set()
for section in generator.arrange.form.full_arrangement_sections(source['staves'][0]['measures'], 'electric'):
measures = result['staves'][0]['measures'][start:start + section.measure_count]
original = independent['staves'][0]['measures'][start:start + section.measure_count]
start += section.measure_count
if section.role == 'melody':
self.assertEqual(measures, original)
if section.role != 'accompaniment':
continue
for offset, (m, old) in enumerate(zip(measures, original)):
source_number = source['staves'][0]['measures'][section.source_measure_index + offset]['number']
for voice, old_voice in zip(m['voices'], old['voices']):
self.assertEqual([{k: v for k, v in n.items() if k != 'pitches'} for n in voice['notes']], [{k: v for k, v in n.items() if k != 'pitches'} for n in old_voice['notes']])
for n in voice['notes']:
if not n['pitches'] or source_number not in ('4', '12', '22', '24'):
continue
expected = (3, 3, 3, 3) if source_number == '22' else (3, 5, 5, 5)
self.assertEqual(tuple(f for _, f in sorted(shape(n), reverse=True)), expected)
self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in n['pitches']))
checked.add(source_number)
self.assertEqual(checked, {'4', '12', '22', '24'})
+2
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import argparse
import contextlib
import copy
@@ -38,6 +39,7 @@ class EngravingCacheTests(unittest.TestCase):
self.assertIn("if: steps.engraving_cache.outputs.cache-hit != 'true'", workflow)
self.assertIn('key: ${{ steps.engraving_cache.outputs.cache-primary-key }}', workflow)
def setUp(self):
tests.production_fixtures.install(self)
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = pathlib.Path(self.temporary.name)
+8 -12
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import argparse
import copy
import fractions
@@ -24,6 +25,8 @@ import tests.solo_fixtures
import tests.test_musicxml
class GuitarTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
def test_fret_boundaries_and_span(self):
self.assertFalse(generator.fretboard.positions_for_pitch(fractions.Fraction(40)))
self.assertIn(generator.fretboard.TabPosition(6, 1, 41), generator.fretboard.positions_for_pitch(fractions.Fraction(41)))
@@ -265,19 +268,12 @@ class GuitarTests(unittest.TestCase):
output.mkdir()
(output / 'last-good').write_text('preserved')
args = argparse.Namespace(data_dir=root / 'data', output_dir=output, static_assets_dir=pathlib.Path('package/assets'), check=False)
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part electric:.*1-20'):
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml/electric_melody:.*1-20'):
generator.__main__.generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
self.assertEqual((output / 'last-good').read_text(), 'preserved')
harmony = '<harmony><root><root-step>C</root-step></root><kind>dominant</kind><degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree><degree><degree-value>11</degree-value><degree-alter>1</degree-alter><degree-type>add</degree-type></degree></harmony>'
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score(harmony + tests.test_musicxml.xml_note()))
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part acoustic:.*measure 1.*no acoustic grip.*1-20'):
generator.__main__.generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
self.assertEqual((output / 'last-good').read_text(), 'preserved')
harmony = '<harmony><root><root-step>C</root-step></root><kind>power</kind></harmony>'
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score(harmony + tests.test_musicxml.xml_note()))
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part acoustic:.*3 consecutive fret positions'):
generator.__main__.generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
with unittest.mock.patch('generator.comping.voicings', side_effect=ValueError('no legal sparse voicing')), unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()):
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score('<harmony><root><root-step>C</root-step></root><kind>dominant</kind></harmony>'+tests.test_musicxml.xml_note()))
with self.assertRaisesRegex(ValueError, 'no legal sparse voicing'):
generator.__main__.generate_assets(args)
self.assertEqual((output / 'last-good').read_text(), 'preserved')
+196
View File
@@ -0,0 +1,196 @@
"""Dependency and music contracts use synthetic sources, never audition scores."""
import concurrent.futures
import copy
import contextlib
import dataclasses
import json
import multiprocessing
from pathlib import Path
import tempfile
import unittest
from fractions import Fraction as F
from unittest.mock import patch
from generator import production, model, vocabulary
from generator.solo import expressions, types, harmonic, placement, notation, composer, themes
from tests.test_solo import song
from tests.test_solo_passages import choices
from tests.test_solo_development import note
def music(result):
return result['document']
class IndependentRoles(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.source=production.SourceAnalysis.read(song(count=1,chords=('CM7',)))
cls.root=Path('data').resolve()
def test_alone_order_parallel_and_assembly(self):
serial={role:production.generate_role(self.source,role,self.root) for role in production.ROLES}
with concurrent.futures.ProcessPoolExecutor(max_workers=2,mp_context=multiprocessing.get_context('spawn')) as pool:
jobs={role:pool.submit(production._job,(self.source,role,self.root,{},None)) for role in reversed(production.ROLES)}
parallel={role:future.result() for role,future in jobs.items()}
for role in production.ROLES:self.assertEqual(music(serial[role]),music(parallel[role]),role)
for doc in production.assemble(self.source,serial).values():model.validate_document(doc)
def test_role_setting_and_cache_are_independent(self):
with tempfile.TemporaryDirectory() as directory:
a=production.generate_role(self.source,'acoustic_solo',self.root,cache_dir=directory)
b=production.generate_role(self.source,'electric_solo',self.root,{'seed':'changed'},directory)
again=production.generate_role(self.source,'acoustic_solo',self.root,cache_dir=directory)
clean=production.generate_role(self.source,'acoustic_solo',self.root)
self.assertTrue(again['cache_hit']);self.assertEqual(music(a),music(again));self.assertEqual(music(a),music(clean))
self.assertEqual(json.dumps(music(a)),json.dumps(music(again)))
self.assertNotEqual(a['trace']['cache_key'],b['trace']['cache_key'])
def test_optional_inputs_cannot_be_opened_by_composition(self):
original=Path.open
def guarded(path,*args,**kwargs):
self.assertNotIn('audition',str(path));self.assertNotIn('reference',str(path));self.assertNotIn('generated_assets',str(path))
return original(path,*args,**kwargs)
with patch.object(Path,'open',guarded):
result=production.generate_role(self.source,'acoustic_solo',self.root)
self.assertNotIn('reference',json.dumps(result['trace']))
def test_titles_and_export_ids_do_not_supply_musical_identity(self):
document=song(count=1,chords=('CM7',));other=copy.deepcopy(document)
other['metadata'].update(title='A completely different title',composer='Different')
for m in other['staves'][0]['measures']:m['id']='exporter_123';m['number']='99'
self.assertEqual(production.SourceAnalysis.read(document),production.SourceAnalysis.read(other))
def test_reference_presence_and_changes_do_not_change_clean_music(self):
# No repository reference is a fixture or prerequisite. These deliberately
# invalid optional files test the absence of a discovery/fallback branch.
with tempfile.TemporaryDirectory() as directory, contextlib.chdir(directory):
reference_dir=Path(directory)/'build';reference_dir.mkdir()
references=[reference_dir/(song+'-solo-'+str(index)+suffix+'.xml')
for song in ('all-of-me','autumn-leaves') for index in (1,2) for suffix in ('','(4)')]
before=production.generate_role(self.source,'electric_solo',self.root)
for reference in references:reference.write_text('<not-a-production-source/>')
present=production.generate_role(self.source,'electric_solo',self.root)
for reference in references:reference.write_text('changed optional target, still not MusicXML')
changed=production.generate_role(self.source,'electric_solo',self.root)
for reference in references:reference.unlink()
absent=production.generate_role(self.source,'electric_solo',self.root)
self.assertTrue(all(music(before)==music(v) for v in (present,changed,absent)))
def test_original_melody_changes_the_theme_with_unchanged_harmony(self):
source=song(count=4,chords=('CM7',))
changed=copy.deepcopy(source)
for m in changed['staves'][0]['measures']:
for v in m['voices']:
for n in v['notes']:
for p in n['pitches']:p['letter']={'c':'e','d':'f','e':'g','f':'a'}[p['letter']]
a=production.generate_role(production.SourceAnalysis.read(source),'acoustic_solo',self.root)
b=production.generate_role(production.SourceAnalysis.read(changed),'acoustic_solo',self.root)
self.assertNotEqual(a['details']['phrases'][0]['draft']['notes'],b['details']['phrases'][0]['draft']['notes'])
def test_source_tuplet_lanes_survive_assembly(self):
serial={r:production.generate_role(self.source,r,self.root) for r in production.ROLES}
# Exercise a different original lane name, as in the imported Autumn head.
doc=serial['electric_melody']['document'];staff=doc['staves'][0]
raw=tuple(note(F(i,3),F(i+1,3),'c',event_id=str(i)) for i in range(3))
placed=placement.place(raw,F(120))[1]
index=next(i for i,m in enumerate(staff['measures']) if m.get('section_label')=='Guitar Melody')
replacement=notation.measures(staff['measures'][index:index+1],placed,'source_triplet')
notation.replace_region(doc,index,replacement)
for m in staff['measures']:
for v in m['voices']:
v['id']='voice_source'
for n in v['notes']:n['voice']='voice_source'
for t in staff['tuplets']:t['voice']='voice_source'
model.validate_document(doc)
model.validate_document(production.assemble(self.source,serial)['electric'])
def test_sparse_parts_reuse_the_canonical_form(self):
result=production.generate_role(production.SourceAnalysis.read(song(count=4,chords=('Dm7','G7','CM7'))),'acoustic_accompaniment',self.root)
measures=result['document']['staves'][0]['measures']
def signature(m):
return [(n['onset'],n['duration'],[(model.pitch_number(p),p['tablature']) for p in n['pitches']]) for v in m['voices'] for n in v['notes']]
self.assertEqual([signature(m) for m in measures[4:8]],[signature(m) for m in measures[8:12]])
self.assertTrue(all(len(n['pitches'])<=3 for m in measures for v in m['voices'] for n in v['notes']))
class TypedExpressionContracts(unittest.TestCase):
def setUp(self):
self.h=harmonic.Harmony(choices(('C7','FM7')))
def make(self,rule='upper_lower'):
target=note(2,4,'e',event_id='target')
upper=note(1,F(3,2),'f',event_id='upper')
lower=dataclasses.replace(note(F(3,2),2,'d',event_id='lower'),pitch=types.Pitch('d',F(1),4))
c=types.ExpressionContract('gesture','enclosure',rule,(upper,lower),target,'C7')
return (upper,lower,target),c
def test_complete_enclosure_and_orphans(self):
notes,c=self.make()
self.assertTrue(self.h.evaluate(notes,expressions=(c,)).valid)
self.assertFalse(self.h.evaluate(notes).valid)
self.assertFalse(self.h.evaluate(notes[:-1],expressions=(c,)).valid)
changed=(*notes[:-1],dataclasses.replace(notes[-1],pitch=dataclasses.replace(notes[-1].pitch,octave=5)))
self.assertFalse(self.h.evaluate(changed,expressions=(c,)).valid)
def test_anticipation_validates_actual_future_harmony(self):
n=note(F(7,2),6,'a',event_id='early')
c=types.ExpressionContract('anticipation','anticipation','early_chord_tone',(n,),n,'FM7',boundary=F(4))
self.assertTrue(self.h.evaluate((n,),expressions=(c,)).valid)
self.assertFalse(self.h.evaluate((n,)).valid)
self.assertFalse(self.h.evaluate((n,),expressions=(dataclasses.replace(c,boundary=F(3)),)).valid)
def test_optional_color_is_not_a_written_chord_requirement(self):
n=note(0,2,'d',event_id='ninth')
c=types.ExpressionContract('color','color','dominant_ninth',(n,),n,'C7')
self.assertTrue(self.h.evaluate((n,),expressions=(c,)).valid)
self.assertFalse(self.h.evaluate((n,)).valid)
self.assertNotIn(n.pitch.number()%12,self.h.spans[0].roles.basic)
self.assertNotIn(n.pitch.number()%12,self.h.spans[0].roles.specified)
def test_labels_do_not_authorize_unresolved_notes(self):
n=dataclasses.replace(note(0,2,'c',event_id='bad'),pitch=types.Pitch('c',F(1),4),role='color')
self.assertFalse(self.h.evaluate((n,)).valid)
c=types.ExpressionContract('bad','color','anything',(n,),n,'C7')
self.assertFalse(self.h.evaluate((n,),expressions=(c,)).valid)
def test_delayed_approach_allows_only_the_declared_rest(self):
lower=dataclasses.replace(note(1,F(3,2),'d',event_id='lower'),pitch=types.Pitch('d',F(1),4))
target=note(2,4,'e',event_id='target')
c=types.ExpressionContract('delay','approach','delayed_below',(lower,),target,'C7')
self.assertTrue(self.h.evaluate((lower,target),expressions=(c,)).valid)
wrong=dataclasses.replace(c,rule='below')
self.assertFalse(self.h.evaluate((lower,target),expressions=(wrong,)).valid)
def test_altered_dominant_color_requires_function_and_resolution(self):
color=dataclasses.replace(note(0,1,'d',event_id='color'),pitch=types.Pitch('d',F(-1),4))
target=note(1,2,'c',event_id='target')
c=types.ExpressionContract('flat9','color','dominant_flat_ninth',(color,),target,'C7')
self.assertTrue(self.h.evaluate((color,target),expressions=(c,)).valid)
unresolved=harmonic.Harmony(choices(('C7','Dm7')))
self.assertFalse(unresolved.evaluate((color,target),expressions=(c,)).valid)
self.assertFalse(self.h.evaluate((color,),expressions=(c,)).valid)
def test_mapped_semitone_needs_no_expression_exception(self):
from tests import scale_fixtures
h=harmonic.Harmony(choices(('A7','Dm7'),scale=scale_fixtures.library().scale('harmonic_minor',1,'D')))
a=dataclasses.replace(note(0,F(1,2),'c',event_id='csharp'),pitch=types.Pitch('c',F(1),4))
b=note(F(1,2),1,'d',event_id='d')
self.assertTrue(h.evaluate((a,b)).valid)
def test_ties_and_barlines_do_not_restart_triplets(self):
raw=tuple(note(F(7,2)+F(i,3),F(7,2)+F(i+1,3),'c',event_id=str(i)) for i in range(3))
placed=placement.place(raw,F(120))[1]
doc=song(count=2,chords=('CM7',));measures=notation.measures(doc['staves'][0]['measures'],placed,'triplet')
spans=notation.tuplets(measures)
self.assertEqual(len(spans),1)
self.assertTrue(any('tie_from' in p for m in measures for v in m['voices'] for n in v['notes'] for p in n['pitches']))
def test_authored_fast_cells_respect_tempo(self):
fast={c[0] for c in vocabulary.rhythm_cells(180)}
slow={c[0] for c in vocabulary.rhythm_cells(100)}
self.assertNotIn('sixteenth_pickup',fast);self.assertNotIn('triplet_turn',fast)
self.assertIn('sixteenth_pickup',slow);self.assertIn('triplet_turn',slow)
if __name__=='__main__':unittest.main()
+2
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import argparse
import fractions
import pathlib
@@ -22,6 +23,7 @@ import tests.test_musicxml
class MusicModelTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
self.enterContext(unittest.mock.patch('generator.solo.sequences.load_library', return_value=tests.solo_fixtures.library()))
self.enterContext(unittest.mock.patch('generator.scales.library.load_library', return_value=tests.scale_fixtures.library()))
engraving = unittest.mock.patch('generator.engraving.pipeline.engrave_tree')
+3
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import argparse
import copy
import fractions
@@ -26,6 +27,8 @@ def xml_harmony(root='C', kind='major', offset=0):
return f'<harmony><root><root-step>{root}</root-step></root><kind>{kind}</kind><offset>{offset}</offset></harmony>'
class PartGenerationTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
def assert_complete_measure(self, measure):
cursor = fractions.Fraction()
for note in measure['voices'][0]['notes']:
+3
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import argparse
import copy
import fractions
@@ -35,6 +36,8 @@ import tests.rhythm_fixtures
import tests.test_musicxml
class ScoreBundleTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
def test_real_asset_regeneration_removes_pages_and_rolls_back_font_failure(self):
self.enterContext(unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()))
with tempfile.TemporaryDirectory() as temporary:
+4 -4
View File
@@ -57,16 +57,16 @@ class ReusableAccompaniment(unittest.TestCase):
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))
self.assertEqual(acoustic.register,(41,76));self.assertEqual(electric.register,(41,76))
self.assertTrue(acoustic.permits(41));self.assertTrue(electric.permits(50))
self.assertTrue(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)))
self.assertTrue(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',))))
+4 -1
View File
@@ -1,3 +1,4 @@
import tests.production_fixtures
import argparse
import copy
import dataclasses
@@ -55,6 +56,8 @@ def compose(document, part='acoustic', cutoff=None):
return generator.solo.composer.compose(analysis, measures, part, end, scales=tests.scale_fixtures.library(), sequences=tests.solo_fixtures.library(), rhythms=tests.rhythm_fixtures.library())
class SoloTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
def test_catalog_intervals_and_negative_scale_indices(self):
library = generator.solo.sequences.load_library(pathlib.Path('data/sequences'))
self.assertEqual({k: len(v) for k, v in library.paths.items()}, {2: 14, 3: 196, 4: 2744})
@@ -227,7 +230,7 @@ class SoloTests(unittest.TestCase):
output.mkdir()
(output / 'accepted').write_text('previous build')
args = argparse.Namespace(data_dir=root / 'data', output_dir=output, static_assets_dir=pathlib.Path('package/assets'), check=False)
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), unittest.mock.patch('generator.scales.library.load_library', return_value=tests.scale_fixtures.library()), unittest.mock.patch('generator.solo.sequences.load_library', return_value=tests.solo_fixtures.library()), unittest.mock.patch('generator.solo.arrangement.apply', side_effect=ValueError('No exact match within the permitted catalog')):
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), unittest.mock.patch('generator.scales.library.load_library', return_value=tests.scale_fixtures.library()), unittest.mock.patch('generator.solo.sequences.load_library', return_value=tests.solo_fixtures.library()), unittest.mock.patch('generator.solo.composer.compose', side_effect=ValueError('No exact match within the permitted catalog')):
with self.assertRaisesRegex(ValueError, 'conflict.xml.*No exact match'):
generator.__main__.generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['accepted'])
+1 -1
View File
@@ -183,7 +183,7 @@ class DraftCompositionTests(unittest.TestCase):
self.assertIsNotNone(draft)
self.assertEqual((draft.start, draft.end), (phrase.plan.start, phrase.plan.end))
prefix = (dataclasses.replace(previous, position=None),) if previous else ()
self.assertTrue(harmony.evaluate((*prefix, *draft.notes), phrase.state.treatments, until=draft.end, enclosures=draft.enclosures).valid)
self.assertTrue(harmony.evaluate((*prefix, *draft.notes), phrase.state.treatments, until=draft.end, enclosures=draft.enclosures, expressions=draft.expressions).valid)
self.assertTrue(generator.solo.placement.place(draft.notes, self.solo.tempo, previous))
self.assertTrue(draft.transformations)
previous = phrase.state.last
+2 -1
View File
@@ -249,6 +249,7 @@ class EnclosureCompositionAPI(unittest.TestCase):
self.assertEqual([p.anchors for p in decorated.phrases],[p.anchors for p in strict.phrases])
self.assertTrue(harmonic.Harmony(strict.choices).evaluate(strict.notes()).valid)
self.assertTrue(all(not p.draft.enclosures for p in strict.phrases))
self.assertTrue(any(p.draft.enclosures for p in decorated.phrases))
self.assertGreater(decorated.diagnostics["expression_candidates"],0)
self.assertTrue(all(not p.draft.expressions for p in strict.phrases))
if __name__=='__main__':unittest.main()
+7 -5
View File
@@ -22,8 +22,8 @@ class SourceVariationTests(unittest.TestCase):
cls.context=realization.SearchContext()
cls.solo=cls.compose(cls.document,cls.context)
@classmethod
def compose(cls,document,context=None,end=32):
return composer.compose(analysis.analyze(document),document['staves'][0]['measures'],'acoustic',F(end),
def compose(cls,document,context=None,end=32,part='acoustic'):
return composer.compose(analysis.analyze(document),document['staves'][0]['measures'],part,F(end),
scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),context=context,mode='paraphrase')
def test_production_is_traceable_to_complete_source_phrases(self):
self.assertTrue(self.solo.phrases)
@@ -109,12 +109,14 @@ class SourceVariationTests(unittest.TestCase):
shortened=self.compose(self.document,end=F(29))
self.assertTrue(all(n.end<=29 for n in shortened.notes()))
composer.validate(shortened)
def test_pair_selection_produces_valid_distinct_source_variations(self):
def test_batch_selection_preserves_independent_source_variations(self):
a=analysis.analyze(self.document)
pair=composer.compose_pair(a,{part:(self.document['staves'][0]['measures'],F(32),None) for part in ('acoustic','electric')},
scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),mode='paraphrase')
self.assertLess(leads.similarity(*pair),1)
self.assertEqual(pair[0].diagnostics['pairs_compared'],64)
for solo in pair:
alone=self.compose(self.document,part=solo.part)
self.assertEqual(solo.notes(),alone.notes())
self.assertNotIn('pairs_compared',solo.diagnostics)
for solo in pair:
composer.validate(solo)
for phrase in solo.phrases:
+5 -15
View File
@@ -24,21 +24,11 @@ def notes(names):
return tuple(dataclasses.replace(fixtures.note(F(i, 2), F(i + 1, 2), name), event_id=f'n{i}') for i, name in enumerate(names))
class LeadReferenceTests(unittest.TestCase):
def test_accepted_pair_preserves_sounding_pitch_exact_time_and_harmony(self):
reference, = references.load_library(pathlib.Path('data/lead_phrases'))
self.assertEqual((len(reference.notes), len(reference.decorated), reference.duration), (31, 36, 32))
self.assertEqual(reference.notes[0].pitch.number(), 62)
self.assertEqual(reference.notes[1].end - reference.notes[1].start, F(3, 2))
choices = list(passages.choices(tuple(chord for _, chord in reference.harmony)))
minor = tests.scale_fixtures.library().scale('harmonic_minor', 1, 'D')
choices[5] = dataclasses.replace(choices[5], scale=minor)
harmony = harmonic.Harmony(tuple(choices))
for variant in (reference.notes, reference.decorated):
self.assertTrue(harmony.evaluate(variant, complete=True).valid)
placed = generator.solo.placement.place(variant, F(120))
self.assertTrue(placed)
self.assertTrue(all(1 <= n.position.fret <= 12 for n in placed[1]))
self.assertTrue(all(m.context and m.duration in (8, 16) for m in reference.windows()))
def test_authored_sounding_phrase_is_valid_without_audition_files(self):
original=tuple(dataclasses.replace(n,start=n.start*2,end=n.end*2) for n in notes(('c','e','g','e')))
harmony=harmonic.Harmony(passages.choices(('CM7',)))
self.assertTrue(harmony.evaluate(original,complete=True).valid)
self.assertTrue(generator.solo.placement.place(original,F(120)))
def test_feature_selection_finds_the_relationship_not_the_first_three(self):
original = notes(('c', 'd', 'e', 'c', 'g', 'f', 'd'))
feature, = references.identifying_features(original, 'test')