+34
-73
@@ -8,6 +8,7 @@ import xml.etree.ElementTree
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.model
|
||||
import generator.parts
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import generator.rhythm
|
||||
@@ -301,85 +302,45 @@ class AccompanimentRealizationTests(unittest.TestCase):
|
||||
self.assertEqual(plan.acoustic_bass, (bar,))
|
||||
self.assertEqual(plan.electric, (bar,))
|
||||
|
||||
class TiedAccompanimentTests(unittest.TestCase):
|
||||
def source(self, root='C', kind='major', offset=0):
|
||||
class DetachedAccompanimentTests(unittest.TestCase):
|
||||
def source(self):
|
||||
first = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(extra='<tie type="start"/>')
|
||||
second = tests.test_parts.xml_harmony(root, kind, offset) + tests.test_musicxml.xml_note(extra='<tie type="stop"/>')
|
||||
second = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(extra='<tie type="stop"/>')
|
||||
return tests.test_musicxml.parse(xml_bars([first, second]))
|
||||
def assert_tied_chord(self, first, continuation):
|
||||
self.assertTrue(first['pitches'])
|
||||
self.assertEqual(len(first['pitches']), len(continuation['pitches']))
|
||||
for previous, current in zip(first['pitches'], continuation['pitches']):
|
||||
self.assertEqual(current['tie_from'], previous['id'])
|
||||
self.assertEqual(previous['tie_to'], current['id'])
|
||||
self.assertEqual(generator.model.pitch_number(current), generator.model.pitch_number(previous))
|
||||
self.assertEqual(current['tablature'], previous['tablature'])
|
||||
for note in (first, continuation):
|
||||
self.assertNotIn('_rhythm_continuation', note)
|
||||
for pitch in note['pitches']:
|
||||
self.assertEqual(pitch['duration'], note['duration'])
|
||||
self.assertEqual(pitch['written'], note['written'])
|
||||
def test_whole_note_tie_keeps_second_bar_audible_without_a_new_attack(self):
|
||||
self.check_tied_rhythm(4)
|
||||
def test_half_note_continuation_preserves_tone_duration_strings_and_following_rest(self):
|
||||
self.check_tied_rhythm(2)
|
||||
def check_tied_rhythm(self, continuation_duration):
|
||||
def test_rhythm_continuations_are_silent_in_all_accompaniment_lanes(self):
|
||||
source = self.source()
|
||||
first, second = source['staves'][0]['measures']
|
||||
bars = tied_library(2).for_meter('4/4')[0].bars
|
||||
for lane in ('bass', 'upper', 'electric'):
|
||||
events = generator.accompaniment.realize(second, bars[1], first['harmonies'][0], lane=lane)
|
||||
self.assertTrue(all(n['kind'] == 'rest' and '_rhythm_continuation' not in n for n in events))
|
||||
def test_continuation_only_bar_does_not_count_as_audible_accompaniment(self):
|
||||
measures = self.source()['staves'][0]['measures']
|
||||
with self.assertRaisesRegex(ValueError, 'no rhythm motif'):
|
||||
generator.accompaniment.plan_section(measures, tied_library(4))
|
||||
def test_planner_chooses_attacks_instead_of_continuation_only_bars(self):
|
||||
source = self.source()
|
||||
library = generator.rhythm.Library(tied_library(4).for_meter('4/4') + tests.rhythm_fixtures.library().for_meter('4/4'))
|
||||
plan = generator.accompaniment.plan_section(source['staves'][0]['measures'], library)
|
||||
for bar in (*plan.acoustic_bass, *plan.acoustic_upper):
|
||||
self.assertTrue(bar.attacks())
|
||||
def test_acoustic_stops_at_barline_without_inventing_continuation_attacks(self):
|
||||
source = self.source()
|
||||
before = copy.deepcopy(source)
|
||||
library = tied_library(continuation_duration)
|
||||
bars = library.for_meter('4/4')[0].bars
|
||||
self.assertEqual(bars[1].attacks(), ())
|
||||
plan = generator.accompaniment.plan_section(source['staves'][0]['measures'], library)
|
||||
self.assertEqual(plan.acoustic_upper, bars)
|
||||
self.assertEqual(plan.acoustic_bass, bars)
|
||||
self.assertEqual(plan.electric, bars)
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=library)
|
||||
first, second = tests.test_musicxml.notes(result), tests.test_musicxml.notes(result, 1)
|
||||
self.assertEqual(timing(first), [(0, 4, 'note')])
|
||||
expected = [(0, continuation_duration, 'note')]
|
||||
if continuation_duration == 2:
|
||||
expected.append((2, 2, 'rest'))
|
||||
self.assertEqual(second[1]['pitches'], [])
|
||||
self.assertEqual(timing(second), expected)
|
||||
self.assert_tied_chord(first[0], second[0])
|
||||
first_tones = [p for v in result['staves'][0]['measures'][0]['voices'] for n in v['notes'] for p in n['pitches']]
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for p in first_tones}, {0, 4, 7})
|
||||
self.assertEqual(len(result['staves'][0]['measures'][0]['voices']), 1)
|
||||
bars = tied_library(2).for_meter('4/4')[0].bars
|
||||
plan = generator.accompaniment.Plan(bars, bars, ())
|
||||
prepared = generator.parts.prepare_document(source, practice=True)
|
||||
result = generator.practice.generate._practice_document(prepared, 'acoustic', plan)
|
||||
generator.model.validate_document(result)
|
||||
self.assertEqual(timing(tests.test_musicxml.notes(result)), [(0, 4, 'note')])
|
||||
self.assertEqual(timing(tests.test_musicxml.notes(result, 1)), [(0, 4, 'rest')])
|
||||
timeline = generator.playback.compile_timeline(result)
|
||||
self.assertEqual(generator.model.fraction(timeline['duration']), 8)
|
||||
self.assertEqual(len(timeline['events']), len(first_tones))
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 and generator.model.fraction(e['duration']) == 4 + continuation_duration for e in timeline['events']))
|
||||
self.assertEqual(source, before)
|
||||
self.assertEqual(library.for_meter('4/4')[0].bars, bars)
|
||||
def test_changed_harmony_breaks_rhythm_ties_without_inventing_attacks(self):
|
||||
for root, kind in (('F', 'major'), ('C', 'minor'), ('C', 'none')):
|
||||
with self.subTest(root=root, kind=kind):
|
||||
source = self.source(root, kind)
|
||||
before = copy.deepcopy(source)
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=tied_library(2))
|
||||
first, second = tests.test_musicxml.notes(result), tests.test_musicxml.notes(result, 1)
|
||||
self.assertEqual(timing(first), [(0, 4, 'note')])
|
||||
self.assertEqual(timing(second), [(0, 4, 'rest')])
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for v in result['staves'][0]['measures'][0]['voices'] for n in v['notes'] for p in n['pitches']}, {0, 4, 7})
|
||||
self.assertEqual(second[0]['pitches'], [])
|
||||
for note in first + second:
|
||||
self.assertTrue(timeline['events'])
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 and generator.model.fraction(e['duration']) == 4 for e in timeline['events']))
|
||||
for measure in result['staves'][0]['measures']:
|
||||
for voice in measure['voices']:
|
||||
for note in voice['notes']:
|
||||
for pitch in note['pitches']:
|
||||
self.assertNotIn('tie_from', pitch)
|
||||
self.assertNotIn('tie_to', pitch)
|
||||
self.assertEqual(pitch['duration'], note['duration'])
|
||||
generator.model.validate_document(result)
|
||||
events = generator.playback.compile_timeline(result)['events']
|
||||
self.assertEqual({(generator.model.fraction(e['onset']), generator.model.fraction(e['duration'])) for e in events}, {(0, 4)})
|
||||
self.assertEqual(source, before)
|
||||
def test_harmony_change_inside_continuation_cuts_tied_sound_and_preserves_rest(self):
|
||||
source = self.source('F', offset=1)
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=tied_library(2))
|
||||
first, second = tests.test_musicxml.notes(result), tests.test_musicxml.notes(result, 1)
|
||||
self.assertEqual(timing(first), [(0, 4, 'note')])
|
||||
self.assertEqual(timing(second), [(0, 1, 'note'), (1, 3, 'rest')])
|
||||
self.assert_tied_chord(first[0], second[0])
|
||||
generator.model.validate_document(result)
|
||||
events = generator.playback.compile_timeline(result)['events']
|
||||
self.assertEqual(len(events), sum(len(n['pitches']) for v in result['staves'][0]['measures'][0]['voices'] for n in v['notes']))
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 and generator.model.fraction(e['duration']) == 5 for e in events))
|
||||
self.assertEqual(source, before)
|
||||
|
||||
+25
-28
@@ -46,6 +46,11 @@ def acoustic_document(bars):
|
||||
|
||||
class AcousticTests(unittest.TestCase):
|
||||
def combine(self, document):
|
||||
generator.acoustic.prepare(document)
|
||||
generator.model.validate_document(document)
|
||||
prepared = copy.deepcopy(document)
|
||||
generator.acoustic.prepare(document)
|
||||
self.assertEqual(document, prepared)
|
||||
before = sounding(document)
|
||||
generator.acoustic.combine(document)
|
||||
generator.model.validate_document(document)
|
||||
@@ -57,6 +62,8 @@ class AcousticTests(unittest.TestCase):
|
||||
self.assertNotIn('beams', note)
|
||||
for pitch in note['pitches']:
|
||||
self.assertNotIn('stem', pitch)
|
||||
self.assertNotIn('tie_from', pitch)
|
||||
self.assertNotIn('tie_to', pitch)
|
||||
once = copy.deepcopy(document)
|
||||
generator.acoustic.combine(document)
|
||||
self.assertEqual(document, once)
|
||||
@@ -65,37 +72,25 @@ class AcousticTests(unittest.TestCase):
|
||||
notes = self.combine(acoustic_document([([(0, 2), (3, 1)], [(0, 2), (3, 1)])]))[0]
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 2, 'note'), (2, 1, 'rest'), (3, 1, 'note')])
|
||||
self.assertEqual([len(n['pitches']) for n in notes], [3, 0, 3])
|
||||
def test_sustained_bass_ties_through_upper_attacks_and_releases(self):
|
||||
def test_bass_releases_at_next_upper_attack_without_reattacking(self):
|
||||
notes = self.combine(acoustic_document([([(0, 4)], [(1, 1), (3, 1)])]))[0]
|
||||
self.assertTrue(all(n['kind'] == 'note' for n in notes))
|
||||
self.assertEqual([len(n['pitches']) for n in notes], [1, 3, 1, 3])
|
||||
for first, second in zip(notes, notes[1:]):
|
||||
self.assertEqual(first['pitches'][0]['tie_to'], second['pitches'][0]['id'])
|
||||
self.assertEqual(first['pitches'][0]['tablature'], second['pitches'][0]['tablature'])
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 1, 'note'), (1, 1, 'note'), (2, 1, 'rest'), (3, 1, 'note')])
|
||||
self.assertEqual([len(n['pitches']) for n in notes], [1, 2, 0, 2])
|
||||
def test_repeated_pitch_is_an_attack_not_a_tie(self):
|
||||
notes = self.combine(acoustic_document([([(0, 2), (2, 2)], [(0, 4)])]))[0]
|
||||
self.assertEqual(len(notes), 2)
|
||||
self.assertNotIn('tie_to', notes[0]['pitches'][0])
|
||||
self.assertNotIn('tie_from', notes[1]['pitches'][0])
|
||||
for first, second in zip(notes[0]['pitches'][1:], notes[1]['pitches'][1:]):
|
||||
self.assertEqual(first['tie_to'], second['id'])
|
||||
def test_existing_tie_boundary_does_not_add_a_redundant_event(self):
|
||||
document = acoustic_document([([(0, 1), (1, 1)], [(2, 2)])])
|
||||
first, second = document['staves'][0]['measures'][0]['voices'][0]['notes'][:2]
|
||||
first['pitches'][0]['tie_to'] = second['pitches'][0]['id']
|
||||
second['pitches'][0]['tie_from'] = first['pitches'][0]['id']
|
||||
notes = self.combine(document)[0]
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 2, 'note'), (2, 2, 'note')])
|
||||
def test_cross_measure_ties_preserve_both_rhythms(self):
|
||||
self.assertEqual([len(n['pitches']) for n in notes], [3, 1])
|
||||
def test_simultaneous_tones_release_at_shortest_planned_duration(self):
|
||||
notes = self.combine(acoustic_document([([(0, 4)], [(0, 1)])]))[0]
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 1, 'note'), (1, 3, 'rest')])
|
||||
self.assertEqual(len(notes[0]['pitches']), 3)
|
||||
def test_barline_releases_notes_before_next_measures_attacks(self):
|
||||
document = acoustic_document([([(0, 4)], [(2, 2)]), ([(0, 2)], [(0, 1), (2, 1)])])
|
||||
first, second = document['staves'][0]['measures']
|
||||
for lane in range(2):
|
||||
old = next(n for n in first['voices'][lane]['notes'] if n['pitches'])
|
||||
new = second['voices'][lane]['notes'][0]
|
||||
for a, b in zip(old['pitches'], new['pitches']):
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
notes = self.combine(document)
|
||||
self.assertTrue(all('tie_from' in p for p in notes[1][0]['pitches']))
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes[0]), [(0, 2, 'note'), (2, 2, 'note')])
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes[1]), [(0, 1, 'note'), (1, 1, 'rest'), (2, 1, 'note'), (3, 1, 'rest')])
|
||||
self.assertEqual(notes[1][-1]['kind'], 'rest')
|
||||
def test_silence_collapses_to_a_single_measure_rest(self):
|
||||
notes = self.combine(acoustic_document([([], [])]))[0]
|
||||
@@ -109,21 +104,23 @@ class AcousticTests(unittest.TestCase):
|
||||
voice['notes'] = voice['notes'][:1]
|
||||
notes = self.combine(document)[0]
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 1, 'note')])
|
||||
def test_five_eighths_uses_conventional_tied_notes_not_a_tuplet(self):
|
||||
def test_five_eighths_shortens_to_one_note_without_ties_or_reattacks(self):
|
||||
notes = self.combine(acoustic_document([([(0, fractions.Fraction(5, 2))], [])]))[0]
|
||||
self.assertEqual([n['written'] for n in notes], [{'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': 'quarter', 'dots': 1}])
|
||||
self.assertEqual(notes[0]['pitches'][0]['tie_to'], notes[1]['pitches'][0]['id'])
|
||||
self.assertEqual([n['written'] for n in notes], [{'type': 'half', 'dots': 0}, {'type': 'half', 'dots': 0}])
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 2, 'note'), (2, 2, 'rest')])
|
||||
def test_unrelated_voices_and_solo_slashes_are_unchanged(self):
|
||||
document = acoustic_document([([(0, 4)], [(0, 4)])])
|
||||
for part in ('electric', 'vocal', None):
|
||||
document['staves'][0]['arrangement_part_type'] = part
|
||||
before = copy.deepcopy(document)
|
||||
generator.acoustic.prepare(document)
|
||||
generator.acoustic.combine(document)
|
||||
self.assertEqual(document, before)
|
||||
document['staves'][0]['arrangement_part_type'] = 'acoustic'
|
||||
measure = document['staves'][0]['measures'][0]
|
||||
measure['voices'] = [{'id': 'voice-arranged', 'notes': [generator.model.make_event('slash', fractions.Fraction(), fractions.Fraction(4), 'note') | {'voice': 'voice-arranged', 'notehead': 'slash'}]}]
|
||||
before = copy.deepcopy(document)
|
||||
generator.acoustic.prepare(document)
|
||||
generator.acoustic.combine(document)
|
||||
self.assertEqual(document, before)
|
||||
def test_song_and_practice_integration_preserve_playback(self):
|
||||
@@ -137,7 +134,7 @@ class AcousticTests(unittest.TestCase):
|
||||
generator.model.validate_document(combined)
|
||||
self.assertTrue(all(len(m['voices']) == 1 for m in combined['staves'][0]['measures']))
|
||||
def test_engraved_chord_has_one_stem_and_no_competing_rests(self):
|
||||
document = acoustic_document([([(0, 4)], [(1, 1), (3, 1)])])
|
||||
document = acoustic_document([([(0, 4)], [(1, 2), (3, 1)])])
|
||||
self.combine(document)
|
||||
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
|
||||
layout = generator.engraving.layout.layout_score(document, generator.engraving.types.Style(), fonts)
|
||||
@@ -145,4 +142,4 @@ class AcousticTests(unittest.TestCase):
|
||||
self.assertFalse([p for p in primitives if p.role in ('rest', 'tab-rest')])
|
||||
for note in document['staves'][0]['measures'][0]['voices'][0]['notes']:
|
||||
self.assertEqual(len([p for p in primitives if p.role == 'stem' and note['id'] in p.sources]), 1)
|
||||
self.assertTrue([p for p in primitives if p.role == 'tie'])
|
||||
self.assertFalse([p for p in primitives if p.role in ('tie', 'tab-tie')])
|
||||
|
||||
@@ -132,14 +132,14 @@ class GuitarTests(unittest.TestCase):
|
||||
states = {}
|
||||
for index in range(40):
|
||||
positions = tuple(generator.guitar.TabPosition(s, randomizer.randrange(21), 0) for s in randomizer.sample(range(1, 7), 3))
|
||||
states[index] = generator.guitar.Placement(tuple(randomizer.randrange(8) for _ in range(3)), tuple((str(i), p) for i, p in enumerate(positions)), None, ())
|
||||
states[index] = generator.guitar.Placement(generator.guitar.VoicingCost(*(randomizer.randrange(8) for _ in range(6))), tuple((str(i), p) for i, p in enumerate(positions)), None, ())
|
||||
grid = generator.guitar.nearest_positions(states)
|
||||
for x, row in enumerate(grid):
|
||||
for y, actual in enumerate(row):
|
||||
candidates = []
|
||||
for index, state in enumerate(states.values()):
|
||||
a, b = generator.guitar.position_center(tuple(p for _, p in state.active))
|
||||
candidates.append(((state.cost[0] + abs(x - a) + abs(y - b), *state.cost[1:]), index))
|
||||
candidates.append((state.cost.moved(abs(x - a) + abs(y - b)), index))
|
||||
self.assertEqual(actual[:2], min(candidates))
|
||||
def test_generated_chord_retains_strings_when_a_melody_enters(self):
|
||||
content = tests.test_musicxml.xml_note() + '<backup><duration>4</duration></backup><forward><duration>2</duration></forward>' + tests.test_musicxml.xml_note('C', 2, 'half', octave=7, extra='<voice>2</voice>')
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import copy
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
import generator.arrange.generate
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.musicxml
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
import tests.test_parts
|
||||
|
||||
def grip(frets, strings=(4, 3, 2, 1)):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
return tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in zip(strings, frets))
|
||||
|
||||
def ranked(symbol, melody=(), profile='electric', prune=False):
|
||||
definition = generator.harmony.parse_symbol(symbol)
|
||||
required, allowed, bass = generator.guitar.chord_requirements(definition, profile)
|
||||
root = int((generator.model.NATURAL[definition['root']['letter']] + generator.model.alteration(definition['root']['alter'])) % 12)
|
||||
candidates = generator.guitar.chord_positions(required, allowed, bass, prune=prune, profile=profile, melody=melody, root=root)
|
||||
return sorted(candidates, key=lambda shape: (generator.guitar.voicing_cost(shape, profile, allowed, root, melody), shape))
|
||||
|
||||
class VoicingTests(unittest.TestCase):
|
||||
def test_requested_shapes_win_without_a_shape_dictionary(self):
|
||||
for symbol, melody, expected in (('FM7', 69, (3, 5, 5, 5)), ('Gm7', 67, (3, 3, 3, 3))):
|
||||
with self.subTest(symbol=symbol):
|
||||
self.assertEqual(ranked(symbol, (melody,))[0], grip(expected))
|
||||
def test_four_strings_precede_melody_height_and_root_presence(self):
|
||||
candidates = ranked('CM7', (64,))
|
||||
four = next(c for c in candidates if len(c) == 4 and max(p.midi for p in c) > 64)
|
||||
three = next(c for c in candidates if len(c) == 3 and max(p.midi for p in c) == 64)
|
||||
self.assertLess(candidates.index(four), candidates.index(three))
|
||||
candidates = ranked('FM7', (69,))
|
||||
rootless = next(c for c in candidates if len(c) == 4 and 5 not in {p.midi % 12 for p in c})
|
||||
rooted = next(c for c in candidates if len(c) == 3 and 5 in {p.midi % 12 for p in c})
|
||||
self.assertLess(candidates.index(rootless), candidates.index(rooted))
|
||||
def test_melody_on_top_precedes_root_among_four_string_shapes(self):
|
||||
candidates = ranked('C7b9', (64,))
|
||||
best = candidates[0]
|
||||
self.assertEqual(len(best), 4)
|
||||
self.assertEqual(max(p.midi for p in best), 64)
|
||||
self.assertNotIn(0, {p.midi % 12 for p in best})
|
||||
self.assertTrue(any(0 in {p.midi % 12 for p in c} and max(p.midi for p in c) > 64 for c in candidates))
|
||||
def test_root_precedes_physical_cost_without_covering_the_melody(self):
|
||||
self.assertEqual(ranked('CM7', (67,))[0], grip((2, 4, 1, 3)))
|
||||
rooted = generator.guitar.voicing_cost(grip((2, 4, 1, 3)), 'electric', (0, 4, 7, 11), 0, (67,))
|
||||
rootless = generator.guitar.voicing_cost(grip((2, 0, 0, 3)), 'electric', (0, 4, 7, 11), 0, (67,))
|
||||
self.assertGreater(rooted.effort, rootless.effort)
|
||||
self.assertLess(rooted, rootless)
|
||||
def test_essential_extensions_are_never_relaxed_to_add_the_root(self):
|
||||
for symbol in ('C7b9', 'FM7#11', 'C13', 'Cdim7'):
|
||||
definition = generator.harmony.parse_symbol(symbol)
|
||||
required, allowed, _ = generator.guitar.chord_requirements(definition, 'electric')
|
||||
for candidate in ranked(symbol):
|
||||
classes = {p.midi % 12 for p in candidate}
|
||||
self.assertTrue(set(required) <= classes <= set(allowed))
|
||||
def test_explicitly_removed_root_is_not_restored_or_penalized(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony(kind='major-seventh') + tests.test_musicxml.xml_note('E', octave=5)))
|
||||
measure = document['staves'][0]['measures'][0]
|
||||
chord = measure['harmonies'][0]
|
||||
chord['degrees'].append({'value': 1, 'alter': 0, 'type': 'subtract'})
|
||||
note = measure['voices'][0]['notes'][0]
|
||||
measure['transpose']['octave_change'] = -1
|
||||
note.update(_harmony=chord, _guitar_profile='electric', _harmonize=True)
|
||||
_, tones = generator.guitar.guitar_events(document)
|
||||
candidates = list(generator.guitar.attack_candidates([note], tones, {}, {}, False))
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all(c.cost.missing_root == 0 and all(p.midi % 12 != 0 for p in c.shape) for c in candidates))
|
||||
def test_lower_frets_and_small_spans_have_explicit_costs(self):
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
low = generator.guitar.voicing_cost(grip((3, 3, 3, 3)), profile)
|
||||
high = generator.guitar.voicing_cost(grip((15, 15, 15, 15)), profile)
|
||||
self.assertLess(low, high)
|
||||
self.assertEqual(high.effort - low.effort, 24)
|
||||
shape = grip((0, 2, 3, 4))
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape).effort, 12)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'acoustic').effort, 26)
|
||||
def test_acoustic_simpler_three_note_shape_can_beat_four_notes(self):
|
||||
candidates = ranked('C7', profile='acoustic')
|
||||
best = candidates[0]
|
||||
self.assertEqual(len(best), 3)
|
||||
self.assertTrue(any(len(c) == 4 for c in candidates))
|
||||
required, _, _ = generator.guitar.chord_requirements(generator.harmony.parse_symbol('C7'), 'acoustic')
|
||||
self.assertTrue(set(required) <= {p.midi % 12 for p in best})
|
||||
def test_nearby_position_can_balance_lower_fret_preference(self):
|
||||
low, nearby, previous = (grip((f,), (1,)) for f in (3, 4, 5))
|
||||
self.assertEqual(generator.guitar.voicing_cost(low).moved(generator.guitar.movement(previous, low)), generator.guitar.voicing_cost(nearby).moved(generator.guitar.movement(previous, nearby)))
|
||||
def test_candidate_pruning_uses_full_cost_and_preserves_each_center(self):
|
||||
for symbol, profile, melody in (('FM7', 'electric', (69,)), ('C7', 'acoustic', ()), ('Gm7', 'electric', (67,))):
|
||||
best = {}
|
||||
for shape in ranked(symbol, melody, profile):
|
||||
best.setdefault(generator.guitar.position_center(shape), shape)
|
||||
self.assertEqual(set(ranked(symbol, melody, profile, prune=True)), set(best.values()))
|
||||
def test_tie_continuations_do_not_charge_a_second_voicing_cost(self):
|
||||
note = tests.test_musicxml.xml_note('E', 2, 'half', octave=5, extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 2, 'half', octave=5, extra='<tie type="stop"/>')
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(note))
|
||||
events, tones = generator.guitar.guitar_events(document)
|
||||
states = {(): generator.guitar.Placement(generator.guitar.VoicingCost(), (), None, ())}
|
||||
first = generator.guitar.advance_positions([events[0][3]], events[0][0], tones, states, False)
|
||||
second = generator.guitar.advance_positions([events[1][3]], events[1][0], tones, first, False)
|
||||
self.assertEqual(sorted(p.cost for p in first.values()), sorted(p.cost for p in second.values()))
|
||||
def test_empty_and_positioned_states_are_compared_by_the_same_grid_cost(self):
|
||||
empty = generator.guitar.Placement(generator.guitar.VoicingCost(effort=20), (), None, ())
|
||||
placed = generator.guitar.Placement(generator.guitar.VoicingCost(), (('tone', grip((3,), (1,))[0]),), None, ())
|
||||
grid = generator.guitar.nearest_positions({(): empty, 1: placed})
|
||||
self.assertIs(grid[6][0][2], placed)
|
||||
self.assertIs(grid[40][10][2], empty)
|
||||
def test_optimized_passage_search_matches_exhaustive_candidate_ranking(self):
|
||||
contents = ''.join(tests.test_musicxml.xml_note('E', 1, 'quarter', octave=5) for _ in range(4))
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(contents))
|
||||
measure = document['staves'][0]['measures'][0]
|
||||
for note, symbol in zip(measure['voices'][0]['notes'], ('FM7', 'Gm7', 'C7b9', 'FM7')):
|
||||
note.update(pitches=[], _harmony=generator.harmony.parse_symbol(symbol), _guitar_profile='electric')
|
||||
events, tones = generator.guitar.guitar_events(document)
|
||||
states = {(): generator.guitar.Placement(generator.guitar.VoicingCost(), (), None, ())}
|
||||
for onset, _, _, note in events:
|
||||
expected = {}
|
||||
for candidate in generator.guitar.attack_candidates([note], tones, {}, {}, False):
|
||||
choices = []
|
||||
for index, previous in enumerate(states.values()):
|
||||
distance = generator.guitar.movement(tuple(p for _, p in previous.active), candidate.shape)
|
||||
choices.append((previous.cost.moved(distance).plus(candidate.cost), index))
|
||||
expected[candidate.active] = min(choices)
|
||||
actual = generator.guitar.advance_positions([note], onset, tones, states, False)
|
||||
predecessors = {id(p): i for i, p in enumerate(states.values())}
|
||||
self.assertEqual({key: (p.cost, predecessors[id(p.previous)]) for key, p in actual.items()}, expected)
|
||||
states = actual
|
||||
def test_real_autumn_leaves_melody_has_expected_shapes_and_unchanged_notes(self):
|
||||
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn-leaves.xml'))
|
||||
before = copy.deepcopy(source)
|
||||
result = generator.arrange.generate.arrange_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
section, checked = '', set()
|
||||
for measure in result['staves'][0]['measures']:
|
||||
section = measure.get('section_label') or section
|
||||
if section != 'Guitar Melody':
|
||||
continue
|
||||
original = next(m for m in source['staves'][0]['measures'] if m['number'] == measure['number'])
|
||||
for note in measure['voices'][0]['notes']:
|
||||
if not note['pitches']:
|
||||
continue
|
||||
onset = generator.model.fraction(note['onset'])
|
||||
authored = next(n for n in original['voices'][0]['notes'] if generator.model.fraction(n['onset']) <= onset < generator.model.fraction(n['onset']) + generator.model.fraction(n['duration']))
|
||||
self.assertEqual(generator.model.pitch_number(note['pitches'][0]), generator.model.pitch_number(authored['pitches'][0]) + 12)
|
||||
if measure['number'] not in ('4', '12', '22', '24') or len(note['pitches']) == 1:
|
||||
continue
|
||||
expected = (3, 3, 3, 3) if measure['number'] == '22' else (3, 5, 5, 5)
|
||||
frets = tuple(p['tablature']['fret'] for p in sorted(note['pitches'], key=lambda p: -p['tablature']['string']))
|
||||
self.assertEqual(frets, expected)
|
||||
self.assertEqual(generator.model.pitch_number(note['pitches'][0]), max(generator.model.pitch_number(p) for p in note['pitches']))
|
||||
checked.add(measure['number'])
|
||||
self.assertEqual(checked, {'4', '12', '22', '24'})
|
||||
self.assertEqual(source, before)
|
||||
Reference in New Issue
Block a user