@@ -131,7 +131,10 @@ class AcousticTests(unittest.TestCase):
|
||||
combined = builder(source, 'acoustic')
|
||||
self.assertEqual(sounding(separate), sounding(combined))
|
||||
generator.model.validate_document(combined)
|
||||
self.assertTrue(all(len(m['voices']) == 1 for m in combined['staves'][0]['measures']))
|
||||
for measure in combined['staves'][0]['measures']:
|
||||
self.assertIn(len(measure['voices']), (1, 2))
|
||||
if len(measure['voices']) == 2:
|
||||
self.assertEqual({v['id'] for v in measure['voices']}, {'upper', 'bass'})
|
||||
def test_engraved_chord_has_one_stem_and_no_competing_rests(self):
|
||||
document = acoustic_document([([(0, 4)], [(1, 2), (3, 1)])])
|
||||
self.combine(document)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Independent rhythm contracts, including real overlapping guitar voices."""
|
||||
import copy
|
||||
import dataclasses
|
||||
import fractions
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.arrange.accompaniment
|
||||
import generator.comping
|
||||
import generator.comping_rhythm
|
||||
import generator.fretboard
|
||||
import generator.harmonic_time
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.production
|
||||
import generator.score
|
||||
import generator.solo.analysis
|
||||
import generator.solo.decoder
|
||||
import generator.solo.harmonic
|
||||
import generator.solo.learned
|
||||
import generator.vocabulary
|
||||
import tests.solo_fixtures
|
||||
|
||||
def sounding(document):
|
||||
return [(m['number'], v['id'], n['onset'], n['duration'], [(p['letter'], p['alter'], p['octave'], p['duration'], p['tablature'], bool(p.get('tie_from')), bool(p.get('tie_to'))) for p in n['pitches']]) for m in document['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] if n['pitches']]
|
||||
|
||||
class LayeredComping(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.source = tests.solo_fixtures.song(count=6, chords=('C7', 'C7', 'FM7', 'FM7', 'Dm7', 'G7'))
|
||||
self.resources = generator.vocabulary.load()
|
||||
def test_candidates_preserve_all_counts_and_articulations(self):
|
||||
m = self.source['staves'][0]['measures'][0]
|
||||
policy = self.resources['accompaniment']
|
||||
for lane, expected in (('electric', {0, 1, 2}), ('bass', {1, 2}), ('upper', {1, 2, 3})):
|
||||
candidates = generator.comping_rhythm.lane_candidates(m, m['harmonies'][0], lane, (), policy, 'test')
|
||||
self.assertEqual({len(c.events) for c in candidates}, expected)
|
||||
self.assertTrue(any(e.end - e.start <= fractions.Fraction(1, 2) for c in candidates for e in c.events) if lane != 'bass' else True)
|
||||
self.assertTrue(any(e.end - e.start >= 2 for c in candidates for e in c.events))
|
||||
if lane == 'bass':
|
||||
self.assertTrue(all(c.events[0].start == 0 for c in candidates))
|
||||
def test_ranges_downbeats_and_overlap(self):
|
||||
canonical = generator.comping.Canonical(self.source, 'acoustic', 'light')
|
||||
carried = generator.accompaniment.carried_chords(self.source['staves'][0]['measures'])
|
||||
for i, (m, prior) in enumerate(zip(self.source['staves'][0]['measures'], carried)):
|
||||
result = canonical(m, prior, i)
|
||||
voices = {v['id']: v['notes'] for v in result['voices']}
|
||||
self.assertEqual(set(voices), {'bass', 'upper'})
|
||||
self.assertTrue(voices['bass'][0]['pitches'])
|
||||
self.assertEqual(generator.model.fraction(voices['bass'][0]['onset']), 0)
|
||||
for lane, high in (('bass', 2), ('upper', 3)):
|
||||
attacks = [n for n in voices[lane] if n['pitches'] and not any(p.get('tie_from') for p in n['pitches'])]
|
||||
self.assertTrue(1 <= len(attacks) <= high)
|
||||
self.assertTrue(any(fractions.Fraction(r['overlap_beats']) > 0 for r in canonical.rhythm_report))
|
||||
self.assertTrue(all(abs(sum(r['contributions'].values()) - r['selection_cost']) < 1e-10 for r in canonical.rhythm_report))
|
||||
def test_combined_crowding_favors_shared_attacks(self):
|
||||
m = self.source['staves'][0]['measures'][0]
|
||||
candidates = generator.comping_rhythm.candidates(m, None, 'acoustic', (), 'joint')
|
||||
self.assertTrue(any(dict(c.contributions)['combined_crowding'] == 0 for c in candidates))
|
||||
self.assertTrue(any(len({e.start for e in c.events}) < len(c.events) for c in candidates))
|
||||
shared = (generator.comping_rhythm.Attack('bass', fractions.Fraction(), fractions.Fraction(2)), generator.comping_rhythm.Attack('upper', fractions.Fraction(), fractions.Fraction(1)))
|
||||
separated = (shared[0], dataclasses.replace(shared[1], start=fractions.Fraction(1, 2)))
|
||||
self.assertLess(generator.comping_rhythm.crowding(shared), generator.comping_rhythm.crowding(separated))
|
||||
def test_harmonic_changes_share_one_measure_budget(self):
|
||||
m = copy.deepcopy(self.source['staves'][0]['measures'][0])
|
||||
m['harmonies'] = [{**generator.harmony.parse_symbol(s), 'id': str(i), 'onset': generator.model.rational(t)} for i, (s, t) in enumerate((('C7', 0), ('FM7', 1), ('G7', 2)))]
|
||||
for part in ('acoustic', 'electric'):
|
||||
choices = generator.comping_rhythm.candidates(m, None, part, (), 'changes')
|
||||
for c in choices:
|
||||
for lane, high in (('bass', 2), ('upper', 3), ('electric', 2)):
|
||||
count = sum(e.lane == lane for e in c.events)
|
||||
self.assertLessEqual(count, high + 1)
|
||||
if count > high:
|
||||
self.assertIn((lane, 'extra attack for harmonic changes'), c.exceptions)
|
||||
for e in c.events:
|
||||
self.assertLessEqual(e.end, 1 if e.start < 1 else 2 if e.start < 2 else 4)
|
||||
if part == 'electric':
|
||||
self.assertTrue(all(c.exceptions for c in choices))
|
||||
def test_no_chord_is_silent_and_partial_measure_is_bounded(self):
|
||||
m = copy.deepcopy(self.source['staves'][0]['measures'][0])
|
||||
m['harmonies'][0]['kind'] = 'none'
|
||||
self.assertTrue(all(not c.events for c in generator.comping_rhythm.candidates(m, None, 'acoustic', (), 'silence')))
|
||||
m['harmonies'][0] = {**generator.harmony.parse_symbol('C7'), 'id': 'chord', 'onset': generator.model.rational(0)}
|
||||
m['duration'] = generator.model.rational(fractions.Fraction(1, 2))
|
||||
self.assertTrue(all(0 <= e.start < e.end <= fractions.Fraction(1, 2) for c in generator.comping_rhythm.candidates(m, None, 'acoustic', (), 'pickup') for e in c.events))
|
||||
def test_two_other_meters_preserve_full_bar_bounds(self):
|
||||
for meter in (3, 5):
|
||||
source = tests.solo_fixtures.song(meter=meter, count=2, chords=('C7',))
|
||||
for part in ('acoustic', 'electric'):
|
||||
canonical = generator.comping.Canonical(source, part, 'meter')
|
||||
for rhythm in canonical.rhythms.values():
|
||||
self.assertTrue(all(0 <= e.start < e.end <= meter for e in rhythm.events))
|
||||
def test_melody_changes_the_objective_without_changing_seed(self):
|
||||
e = generator.comping_rhythm.Attack('electric', fractions.Fraction(1), fractions.Fraction(3, 2))
|
||||
m = self.source['staves'][0]['measures'][0]
|
||||
harmony = generator.comping_rhythm.spans(m, None)
|
||||
busy = tuple((fractions.Fraction(i), fractions.Fraction(i + 1)) for i in range(4))
|
||||
rest = ((fractions.Fraction(), fractions.Fraction(1, 2)),)
|
||||
first = dict(generator.comping_rhythm.lane_score((e,), 'electric', busy, harmony, fractions.Fraction(4)))
|
||||
second = dict(generator.comping_rhythm.lane_score((e,), 'electric', rest, harmony, fractions.Fraction(4)))
|
||||
self.assertGreater(first['melody_support'], second['melody_support'])
|
||||
self.assertNotEqual(generator.comping_rhythm.candidates(m, None, 'electric', busy, 'same'), generator.comping_rhythm.candidates(m, None, 'electric', rest, 'same'))
|
||||
def test_tempo_changes_metadata_but_not_accompaniment_music(self):
|
||||
changed = copy.deepcopy(self.source)
|
||||
changed['metadata']['tempo'] = 240
|
||||
changed['staves'][0]['measures'][0]['tempos'] = [{'onset': generator.model.rational(0), 'bpm': generator.model.rational(240), 'written': {'type': 'quarter', 'dots': 0}}]
|
||||
a, b = map(generator.production.SourceAnalysis.read, (self.source, changed))
|
||||
self.assertEqual(a.accompaniment_key(), b.accompaniment_key())
|
||||
for role in ('acoustic_accompaniment', 'electric_accompaniment'):
|
||||
first = generator.production.generate_role(a, role, pathlib.Path('data'))
|
||||
second = generator.production.generate_role(b, role, pathlib.Path('data'))
|
||||
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):
|
||||
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;
|
||||
# no generated solo participates in these accompaniment decisions.
|
||||
for part in ('acoustic', 'electric'):
|
||||
empty = copy.deepcopy(results[part + '_accompaniment'])
|
||||
for m in empty['document']['staves'][0]['measures']:
|
||||
for v in m['voices']:
|
||||
for n in v['notes']:
|
||||
n.update(kind='rest', pitches=[])
|
||||
results[part + '_solo'] = empty
|
||||
assembled = generator.production.assemble(source, results)
|
||||
self.assertTrue(any(len(m['voices']) == 2 for m in assembled['acoustic']['staves'][0]['measures']))
|
||||
audit = generator.arrange.accompaniment.audit(self.source, assembled)
|
||||
self.assertGreater(audit['acoustic']['reused_bars'], 0)
|
||||
for doc in assembled.values():
|
||||
generator.model.validate_document(doc)
|
||||
generator.fretboard.validate_document(doc)
|
||||
def test_combined_voices_reject_conflicting_strings_and_fret_spans(self):
|
||||
for string, fret in ((5, 3), (2, 10)):
|
||||
doc = tests.solo_fixtures.song(count=1, chords=('C',))
|
||||
staff = doc['staves'][0]
|
||||
staff['tablature'] = True
|
||||
m = staff['measures'][0]
|
||||
pitches = [{'letter': 'c', 'alter': 0, 'octave': 4, 'tablature': {'string': 5, 'fret': 3}}, {'letter': 'c', 'alter': 0, 'octave': 4, 'tablature': {'string': string, 'fret': fret}}]
|
||||
m['voices'] = [{'id': str(i), 'notes': [generator.model.make_event(str(i), fractions.Fraction(i), fractions.Fraction(2), 'note', [p])]} for i, p in enumerate(pitches)]
|
||||
with self.assertRaisesRegex(ValueError, 'overlapping guitar voices'):
|
||||
generator.fretboard.validate_document(doc)
|
||||
def test_sustained_join_is_one_attack_and_keeps_position(self):
|
||||
source = tests.solo_fixtures.song(count=2, chords=('C7',))
|
||||
canonical = generator.comping.Canonical(source, 'electric', 'tie')
|
||||
inputs = [(m, p, i) for i, (m, p) in enumerate(zip(source['staves'][0]['measures'], generator.accompaniment.carried_chords(source['staves'][0]['measures'])))]
|
||||
chord = inputs[0][0]['harmonies'][0]
|
||||
grip = generator.comping.voicings(generator.comping.semantic_key(chord), 'electric')[0]
|
||||
for m, p, i in inputs:
|
||||
note = generator.model.make_event('held' + str(i), fractions.Fraction(), fractions.Fraction(4), 'note', generator.fretboard.chord_pitches(chord, grip))
|
||||
note['voice'] = 'electric'
|
||||
canonical.cache[canonical.key(m, p, i)]['voices'] = [{'id': 'electric', 'notes': [note]}]
|
||||
canonical._rhythm_reports[canonical.key(m, p, i)]['layers']['electric'].update(attacks=1, tied_continuations=0)
|
||||
canonical.connect_holds(inputs)
|
||||
a, b = [canonical.cache[canonical.key(*args)]['voices'][0]['notes'][0] for args in inputs]
|
||||
for p, q in zip(a['pitches'], b['pitches']):
|
||||
self.assertEqual(p['tie_to'], q['id'])
|
||||
self.assertEqual(q['tie_from'], p['id'])
|
||||
self.assertEqual(p['tablature'], q['tablature'])
|
||||
self.assertEqual(canonical.rhythm_report[1]['layers']['electric']['attacks'], 0)
|
||||
|
||||
class SoloRhythmConditioning(unittest.TestCase):
|
||||
def test_melody_rhythm_and_progression_change_learned_probabilities(self):
|
||||
examples = []
|
||||
for held, future, duration in ((False, 'FM7', 1), (True, 'FM7', 2), (False, 'Dm7', 3)):
|
||||
source = tests.solo_fixtures.song(count=2, chords=('C7', future))
|
||||
if held:
|
||||
m = source['staves'][0]['measures'][0]
|
||||
m['voices'][0]['notes'] = [generator.model.make_event('held', fractions.Fraction(), fractions.Fraction(4), 'note', [{'letter': 'c', 'alter': 0, 'octave': 4}])]
|
||||
analysis = generator.solo.analysis.analyze(source)
|
||||
harmony = generator.solo.harmonic.Harmony(generator.harmonic_time.read(source['staves'][0]))
|
||||
examples.append({'analysis': analysis, 'harmony': harmony, 'events': [(fractions.Fraction(), fractions.Fraction(duration), 64), (fractions.Fraction(duration), fractions.Fraction(4), 67)]})
|
||||
model = generator.solo.learned.Model(generator.solo.learned.fit(examples, {'purpose': 'independently authored conditional-rhythm test'}))
|
||||
probabilities = []
|
||||
for example in examples:
|
||||
decoder = generator.solo.decoder.Decoder(example['analysis'], example['harmony'], model, 'acoustic', fractions.Fraction(), fractions.Fraction(4), seed='fixed')
|
||||
harmonic, root, phase, source, _ = decoder.context(fractions.Fraction())
|
||||
context = generator.solo.learned.contexts(harmonic, phase, source, ())
|
||||
probabilities.append(model.probability(generator.solo.learned.token(fractions.Fraction(1), 64, None, root), context))
|
||||
self.assertGreater(probabilities[0], probabilities[1])
|
||||
self.assertGreater(probabilities[0], probabilities[2])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Accompaniment geometry and whole-form movement, using authored small cases."""
|
||||
import copy
|
||||
import fractions
|
||||
import itertools
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.comping
|
||||
import generator.fretboard
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.production
|
||||
import generator.score
|
||||
import generator.vocabulary
|
||||
import tests.solo_fixtures
|
||||
|
||||
def options(symbol, part):
|
||||
return generator.comping.voicings(generator.comping.semantic_key(generator.harmony.parse_symbol(symbol)), part)
|
||||
|
||||
def path_cost(path, costs, part, left=(), right=(), close=False):
|
||||
total = [sum(costs[i][v][j] for i, v in enumerate(path)) for j in range(4)]
|
||||
total[1] += sum(generator.comping.movement(part, a, b) for a, b in zip((left, *path), path))
|
||||
total[1] += generator.comping.movement(part, path[-1], path[0] if close else right)
|
||||
return tuple(total)
|
||||
|
||||
class CompingVoicings(unittest.TestCase):
|
||||
def test_acoustic_geometry_complete_triads_and_component_fret_limits(self):
|
||||
for symbol in ('C', 'Cm', 'F#', 'Bbm', 'C7', 'Cm6', 'Em7b5', 'A7b9', 'C7/E', 'C/D'):
|
||||
with self.subTest(chord=symbol):
|
||||
chord = generator.harmony.parse_symbol(symbol)
|
||||
required = generator.comping.requirements(chord, 'acoustic')
|
||||
for grip in options(symbol, 'acoustic'):
|
||||
self.assertIn(tuple(p.string_number for p in grip), ((5, 4, 3, 2), (6, 4, 3, 2)))
|
||||
self.assertLessEqual(grip.bass.fret, 12)
|
||||
self.assertTrue(all(1 <= p.fret <= 20 for p in grip))
|
||||
self.assertTrue(generator.fretboard.has_fret_anchor(grip[1:]))
|
||||
self.assertTrue(generator.fretboard.within_fret_span(grip))
|
||||
self.assertEqual(grip.bass.midi, min(p.midi for p in grip))
|
||||
self.assertTrue(required.covers({p.midi % 12 for p in grip}))
|
||||
self.assertTrue(generator.fretboard.playable_attack((p, fractions.Fraction(1)) for p in grip))
|
||||
if symbol in ('C', 'Cm', 'F#', 'Bbm'):
|
||||
self.assertEqual({p.midi % 12 for p in grip}, set(required.allowed))
|
||||
def test_bass_thirds_only_for_plain_major_and_minor(self):
|
||||
for symbol, expected in (('C', {0, 4, 7}), ('Cm', {0, 3, 7}), ('C7', {0, 7}), ('Cm6', {0, 7}), ('Em7b5', {4, 10})):
|
||||
with self.subTest(chord=symbol):
|
||||
required = generator.comping.requirements(generator.harmony.parse_symbol(symbol), 'acoustic')
|
||||
self.assertEqual(set(required.basses), expected)
|
||||
self.assertEqual({v.bass.midi % 12 for v in options(symbol, 'acoustic')}, expected)
|
||||
self.assertEqual(set(generator.comping.requirements(generator.harmony.parse_symbol('Cdim'), 'acoustic').basses), {0, 6})
|
||||
def test_explicit_slash_or_inversion_overrides_default_bass(self):
|
||||
inversion = generator.harmony.parse_symbol('C7')
|
||||
inversion['inversion'] = 1
|
||||
for chord, expected in ((generator.harmony.parse_symbol('C7/E'), 4), (generator.harmony.parse_symbol('C/D'), 2), (inversion, 4)):
|
||||
self.assertEqual(generator.comping.requirements(chord, 'acoustic').basses, (expected,))
|
||||
grips = generator.comping.voicings(generator.comping.semantic_key(chord), 'acoustic')
|
||||
self.assertEqual({v.bass.midi % 12 for v in grips}, {expected})
|
||||
def test_electric_uses_melody_voicing_families_and_chord_semantics(self):
|
||||
for symbol in ('C', 'Cm', 'C7', 'CmM7', 'Csus', 'Cdim7', 'Caug7', 'C7b9', 'C13', 'C/E'):
|
||||
with self.subTest(chord=symbol):
|
||||
required = generator.fretboard.chord_requirements(generator.harmony.parse_symbol(symbol), respect_bass=False)
|
||||
melody = generator.guitar.chord_positions(required, profile='electric', prune=False)
|
||||
self.assertEqual(set(options(symbol, 'electric')), set(melody))
|
||||
for grip in options(symbol, 'electric'):
|
||||
self.assertIn(tuple(p.string_number for p in grip), generator.guitar.ELECTRIC_STRINGS)
|
||||
self.assertTrue(required.covers({p.midi % 12 for p in grip}))
|
||||
# Bass responsibility belongs to the acoustic voicing, as in electric melody.
|
||||
self.assertEqual(options('C/E', 'electric'), options('C', 'electric'))
|
||||
def test_impossible_grip_reports_source_location_without_relaxation(self):
|
||||
source = tests.solo_fixtures.song(count=1, chords=('C5',))
|
||||
for part in ('acoustic', 'electric'):
|
||||
with self.subTest(part=part), self.assertRaisesRegex(ValueError, 'source measure 1, beat 1: C5: no legal'):
|
||||
generator.comping.Canonical(source, part, 'test')
|
||||
def test_complete_path_beats_greedy_and_matches_exhaustive_search(self):
|
||||
for part in ('acoustic', 'electric'):
|
||||
with self.subTest(part=part):
|
||||
chords = ('C', 'C', 'Dm7')
|
||||
layers = tuple(options(s, part) for s in chords)
|
||||
costs = tuple({v: generator.comping.local_cost(part, generator.comping.semantic_key(generator.harmony.parse_symbol(s)), v, 55) for v in layer} for s, layer in zip(chords, layers))
|
||||
path, cost = generator.comping.select_grips(layers, costs, part, 'test')
|
||||
greedy = []
|
||||
for i, layer in enumerate(layers):
|
||||
greedy.append(min(layer, key=lambda v: (path_cost((*greedy, v), costs[:i + 1], part), v)))
|
||||
self.assertLess(cost, path_cost(greedy, costs, part))
|
||||
self.assertEqual(cost, min(path_cost(p, costs, part) for p in itertools.product(*layers)))
|
||||
self.assertEqual(cost, path_cost(path, costs, part))
|
||||
self.assertEqual((path, cost), generator.comping.select_grips(tuple(tuple(reversed(l)) for l in layers), costs, part, 'test'))
|
||||
def test_repeat_join_and_neighbor_boundaries_participate_in_selection(self):
|
||||
for part in ('acoustic', 'electric'):
|
||||
layers = tuple(options(s, part) for s in ('C', 'Dm7', 'G7'))
|
||||
costs = tuple({v: (0, 0, 0, 0) for v in layer} for layer in layers)
|
||||
for endpoints in ({'close': True}, {'left': options('Am7', part)[0], 'right': options('FM7', part)[-1]}):
|
||||
with self.subTest(part=part, endpoints=endpoints):
|
||||
path, cost = generator.comping.select_grips(layers, costs, part, 'test', **endpoints)
|
||||
self.assertEqual(cost, min(path_cost(p, costs, part, **endpoints) for p in itertools.product(*layers)))
|
||||
self.assertEqual(cost, path_cost(path, costs, part, **endpoints))
|
||||
def test_rest_does_not_reset_the_previous_hand_position(self):
|
||||
source = tests.solo_fixtures.song(count=3, chords=('C', 'C', 'Dm7'))
|
||||
source['staves'][0]['measures'][1]['harmonies'][0]['kind'] = 'none'
|
||||
for part in ('acoustic', 'electric'):
|
||||
canonical = generator.comping.Canonical(source, part, 'rest')
|
||||
self.assertEqual({i for i, _, _ in canonical.order}, {0, 2})
|
||||
expected = generator.comping.movement(part, canonical.order[0][2], canonical.order[1][2])
|
||||
self.assertEqual(next(r['movement'] for r in canonical.report if r['source_index'] == 2), expected)
|
||||
def test_harmony_variants_are_independent_of_request_order(self):
|
||||
source = tests.solo_fixtures.song(count=3, chords=('C7', 'FM7', 'G7'))
|
||||
measures = source['staves'][0]['measures']
|
||||
carried = generator.accompaniment.carried_chords(measures)
|
||||
variants = []
|
||||
for index, symbol in ((1, 'Dm7'), (2, 'Am7')):
|
||||
changed = copy.deepcopy(measures[index])
|
||||
changed['harmonies'][0] = {**generator.harmony.parse_symbol(symbol), 'id': 'variant', 'onset': generator.model.rational(0)}
|
||||
variants.append((changed, carried[index], index))
|
||||
for part in ('acoustic', 'electric'):
|
||||
a, b = (generator.comping.Canonical(source, part, 'variants') for _ in range(2))
|
||||
forward = {args[2]: a(*args) for args in variants}
|
||||
reverse = {args[2]: b(*args) for args in reversed(variants)}
|
||||
self.assertEqual(forward, reverse)
|
||||
self.assertEqual(a.report, b.report)
|
||||
self.assertEqual(a.base, b.base)
|
||||
self.assertEqual(a(measures[0], carried[0], 0), b(measures[0], carried[0], 0))
|
||||
def test_register_preference_does_not_change_rhythm(self):
|
||||
resources = generator.vocabulary.load()
|
||||
source = tests.solo_fixtures.song(count=4)
|
||||
measures = source['staves'][0]['measures']
|
||||
carried = generator.accompaniment.carried_chords(measures)
|
||||
def rhythm(canonical):
|
||||
return [[(n['onset'], n['duration'], n['written'], n['kind']) for n in generator.score.measure_notes(canonical(m, p, i))] for i, (m, p) in enumerate(zip(measures, carried))]
|
||||
for part in ('acoustic', 'electric'):
|
||||
with generator.vocabulary.using(resources, part + '_accompaniment', {'center': 45}):
|
||||
low = generator.comping.Canonical(source, part, 'fixed-rhythm')
|
||||
with generator.vocabulary.using(resources, part + '_accompaniment', {'center': 70}):
|
||||
high = generator.comping.Canonical(source, part, 'fixed-rhythm')
|
||||
self.assertEqual(rhythm(low), rhythm(high))
|
||||
def test_accompaniment_cache_matches_clean_generation(self):
|
||||
source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=3))
|
||||
for part in ('acoustic', 'electric'):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
role = part + '_accompaniment'
|
||||
cold = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=directory)
|
||||
warm = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=directory)
|
||||
clean = generator.production.generate_role(source, role, pathlib.Path('data'))
|
||||
self.assertTrue(warm['cache_hit'])
|
||||
self.assertEqual(cold['document'], warm['document'])
|
||||
self.assertEqual(cold['document'], clean['document'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -46,20 +46,22 @@ class GuitarPartTests(unittest.TestCase):
|
||||
self.assertEqual(set(requirements.allowed), {0, 7})
|
||||
self.assertFalse(generator.acoustic_voicing.candidates(requirements))
|
||||
self.assertFalse(generator.guitar.chord_positions(requirements, profile='electric'))
|
||||
def test_sparse_acoustic_uses_compact_playable_chords(self):
|
||||
def test_acoustic_uses_bass_and_three_upper_notes(self):
|
||||
result = self.section(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
|
||||
generator.model.validate_document(result)
|
||||
generator.fretboard.validate_document(result)
|
||||
sounding = [n for m in result['staves'][0]['measures'] for n in generator.score.measure_notes(m) if n['pitches']]
|
||||
self.assertTrue(sounding)
|
||||
self.assertTrue(all(2 <= len(n['pitches']) <= 3 for n in sounding))
|
||||
self.assertTrue(all(len(n['pitches']) in (1, 3) for n in sounding))
|
||||
for n in sounding:
|
||||
self.assertIn(sorted(p['tablature']['string'] for p in n['pitches']), ([2, 3, 4], [5], [6]))
|
||||
def test_explicit_bass_is_preserved_for_triads_and_extended_chords(self):
|
||||
for symbol, expected in (('CM/E', 4), ('C7b9/E', 4)):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
chord = generator.harmony.parse_symbol(symbol)
|
||||
source['staves'][0]['measures'][0]['harmonies'] = [{**chord, 'id': 'h1', 'onset': generator.model.rational(0)}]
|
||||
result = tests.arrangement_fixtures.section_document(source, 'acoustic')
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] if v['id'] == 'bass' for n in v['notes']]
|
||||
self.assertEqual({min(generator.model.pitch_number(p) for p in n['pitches']) % 12 for n in notes if n['pitches']}, {expected})
|
||||
def test_long_melody_chords_and_short_notes(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 2, 'half', octave=4) + tests.test_musicxml.xml_note('G', 1, 'quarter', octave=4) + tests.test_musicxml.xml_note('C', 1, 'quarter', octave=5)
|
||||
|
||||
@@ -125,7 +125,7 @@ class IndependentRoles(unittest.TestCase):
|
||||
def signature(m):
|
||||
return [(n['onset'], n['duration'], [(generator.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']))
|
||||
self.assertTrue(all(len(n['pitches']) in (0, 1, 3) for m in measures for v in m['voices'] for n in v['notes']))
|
||||
|
||||
class TypedExpressionContracts(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user