Sepearate Generation
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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'}))
|
||||
@@ -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"/>',
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user