@@ -344,7 +344,7 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
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.assert_tied_chord(result['staves'][0]['measures'][0]['voices'][1]['notes'][0], result['staves'][0]['measures'][1]['voices'][1]['notes'][0])
|
||||
self.assertEqual(len(result['staves'][0]['measures'][0]['voices']), 1)
|
||||
generator.model.validate_document(result)
|
||||
timeline = generator.playback.compile_timeline(result)
|
||||
self.assertEqual(generator.model.fraction(timeline['duration']), 8)
|
||||
@@ -360,7 +360,7 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
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, 2, 'rest'), (2, 2, 'rest')])
|
||||
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:
|
||||
@@ -377,7 +377,7 @@ class TiedAccompanimentTests(unittest.TestCase):
|
||||
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, 1, 'rest'), (2, 2, 'rest')])
|
||||
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']
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import copy
|
||||
import fractions
|
||||
import pathlib
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import generator.acoustic
|
||||
import generator.arrange.generate
|
||||
import generator.engraving.fonts
|
||||
import generator.engraving.layout
|
||||
import generator.engraving.types
|
||||
import generator.model
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_accompaniment
|
||||
import tests.test_musicxml
|
||||
import tests.test_parts
|
||||
|
||||
def sounding(document):
|
||||
return sorted(tuple(generator.model.fraction(event[key]) for key in ('onset', 'duration', 'pitch')) for event in generator.playback.compile_timeline(document)['events'])
|
||||
|
||||
def acoustic_document(bars):
|
||||
document = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * len(bars)))
|
||||
staff = document['staves'][0]
|
||||
staff.update(arrangement_part_type='acoustic', tablature=True)
|
||||
for measure, (bass, upper) in zip(staff['measures'], bars):
|
||||
measure['transpose']['octave_change'] = -1
|
||||
measure['voices'] = []
|
||||
for lane, attacks, positions in (('bass', bass, (('c', 5, 3),)), ('upper', upper, (('e', 4, 2), ('g', 3, 0)))):
|
||||
notes, cursor = [], fractions.Fraction()
|
||||
for onset, duration in attacks:
|
||||
onset, duration = fractions.Fraction(onset), fractions.Fraction(duration)
|
||||
if onset > cursor:
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}-{lane}-{len(notes)}', cursor, onset - cursor, 'rest'))
|
||||
pitches = [{'letter': letter, 'alter': generator.model.rational(0), 'octave': 4, 'tablature': {'string': string, 'fret': fret}} for letter, string, fret in positions]
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}-{lane}-{len(notes)}', onset, duration, 'note', pitches))
|
||||
cursor = onset + duration
|
||||
if cursor < 4:
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}-{lane}-{len(notes)}', cursor, 4 - cursor, 'rest'))
|
||||
for note in notes:
|
||||
note.update(voice=lane, stem='down' if lane == 'bass' else 'up')
|
||||
measure['voices'].append({'id': lane, 'notes': notes})
|
||||
generator.model.validate_document(document)
|
||||
return document
|
||||
|
||||
class AcousticTests(unittest.TestCase):
|
||||
def combine(self, document):
|
||||
before = sounding(document)
|
||||
generator.acoustic.combine(document)
|
||||
generator.model.validate_document(document)
|
||||
self.assertEqual(sounding(document), before)
|
||||
self.assertTrue(all(len(m['voices']) == 1 for m in document['staves'][0]['measures']))
|
||||
for measure in document['staves'][0]['measures']:
|
||||
for note in measure['voices'][0]['notes']:
|
||||
self.assertNotIn('stem', note)
|
||||
self.assertNotIn('beams', note)
|
||||
for pitch in note['pitches']:
|
||||
self.assertNotIn('stem', pitch)
|
||||
once = copy.deepcopy(document)
|
||||
generator.acoustic.combine(document)
|
||||
self.assertEqual(document, once)
|
||||
return [m['voices'][0]['notes'] for m in document['staves'][0]['measures']]
|
||||
def test_simultaneous_attacks_share_one_chord(self):
|
||||
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):
|
||||
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'])
|
||||
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):
|
||||
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(notes[1][-1]['kind'], 'rest')
|
||||
def test_silence_collapses_to_a_single_measure_rest(self):
|
||||
notes = self.combine(acoustic_document([([], [])]))[0]
|
||||
self.assertEqual(tests.test_accompaniment.timing(notes), [(0, 4, 'rest')])
|
||||
self.assertTrue(notes[0]['measure_rest'])
|
||||
def test_pickup_keeps_its_actual_duration(self):
|
||||
document = acoustic_document([([(0, 1)], [(0, 1)])])
|
||||
measure = document['staves'][0]['measures'][0]
|
||||
measure.update(duration=generator.model.rational(1), implicit=True)
|
||||
for voice in measure['voices']:
|
||||
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):
|
||||
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'])
|
||||
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.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.combine(document)
|
||||
self.assertEqual(document, before)
|
||||
def test_song_and_practice_integration_preserve_playback(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
rhythms = tests.rhythm_fixtures.library()
|
||||
for builder in (generator.arrange.generate.arrange_document, generator.practice.generate.practice_document):
|
||||
with unittest.mock.patch('generator.acoustic.combine'):
|
||||
separate = builder(source, 'acoustic', rhythms=rhythms)
|
||||
combined = builder(source, 'acoustic', rhythms=rhythms)
|
||||
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']))
|
||||
def test_engraved_chord_has_one_stem_and_no_competing_rests(self):
|
||||
document = acoustic_document([([(0, 4)], [(1, 1), (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)
|
||||
primitives = [p for page in layout.pages for p in page.primitives]
|
||||
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'])
|
||||
@@ -60,7 +60,7 @@ class EngravingTests(unittest.TestCase):
|
||||
self.assert_paired_pages(layout)
|
||||
self.assertEqual(layout.style.space, 10)
|
||||
self.assertEqual(layout.style.tab_font_size, 12)
|
||||
wide = tests.test_musicxml.xml_note('E', octave=3) + tests.test_musicxml.xml_note('C', octave=7, prefix='<chord/>')
|
||||
wide = tests.test_musicxml.xml_note('E', 2, 'half', octave=3) + tests.test_musicxml.xml_note('C', 2, 'half', octave=7)
|
||||
document = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([wide] * 32))
|
||||
for measure in document['staves'][0]['measures']:
|
||||
measure['transpose']['octave_change'] = -1
|
||||
|
||||
+15
-1
@@ -27,8 +27,22 @@ class GuitarTests(unittest.TestCase):
|
||||
self.assertFalse(generator.guitar.positions_for_pitch(fractions.Fraction(85)))
|
||||
self.assertFalse(generator.guitar.positions_for_pitch(fractions.Fraction(129, 2)))
|
||||
self.assertTrue(generator.guitar.within_fret_span((generator.guitar.TabPosition(4, 11, 61), generator.guitar.TabPosition(3, 12, 67), generator.guitar.TabPosition(2, 14, 73))))
|
||||
self.assertTrue(generator.guitar.within_fret_span((generator.guitar.TabPosition(6, 0, 40), generator.guitar.TabPosition(1, 20, 84))))
|
||||
self.assertFalse(generator.guitar.within_fret_span((generator.guitar.TabPosition(6, 0, 40), generator.guitar.TabPosition(1, 20, 84))))
|
||||
self.assertFalse(generator.guitar.within_fret_span((generator.guitar.TabPosition(4, 10, 60), generator.guitar.TabPosition(1, 20, 84))))
|
||||
def test_open_strings_count_in_span_and_proximity(self):
|
||||
for frets, valid in (((0, 2, 3), True), ((0, 4), True), ((0, 5), False), ((12, 0, 11), False), ((0, 0), True)):
|
||||
with self.subTest(frets=frets):
|
||||
positions = tuple(generator.guitar.TabPosition(6 - i, fret, 0) for i, fret in enumerate(frets))
|
||||
self.assertEqual(generator.guitar.within_fret_span(positions), valid)
|
||||
self.assertEqual(generator.guitar.compactness(positions), max(frets) - min(frets))
|
||||
self.assertEqual(generator.guitar.position_center(positions)[0], min(frets) + max(frets))
|
||||
self.assertTrue(generator.guitar.within_fret_span(()))
|
||||
def test_held_open_string_cannot_mix_with_high_frets(self):
|
||||
held = tests.test_musicxml.xml_note('E', octave=2)
|
||||
high = tests.test_musicxml.xml_note('C', 2, 'half', octave=6, extra='<voice>2</voice>')
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(held + '<backup><duration>4</duration></backup><forward><duration>2</duration></forward>' + high))
|
||||
with self.assertRaisesRegex(ValueError, 'no guitar placement'):
|
||||
generator.guitar.place_guitar(document)
|
||||
def test_theory_controls_omissions_and_bass(self):
|
||||
for symbol in ('CM', 'G7b9', 'FM7#11/A', 'CmM7', 'Cdim7', 'C13', 'Csus', 'Cmadd9'):
|
||||
with self.subTest(symbol=symbol):
|
||||
|
||||
+19
-23
@@ -39,25 +39,21 @@ class GuitarPartTests(unittest.TestCase):
|
||||
self.assertNotIn(0, generator.guitar.chord_requirements(chord, 'electric')[0])
|
||||
candidates = generator.guitar.chord_positions(*generator.guitar.chord_requirements(chord, 'electric'), profile='electric')
|
||||
self.assertTrue(any(all(p.midi % 12 != 0 for p in c) for c in candidates))
|
||||
def test_acoustic_has_independent_voices_and_alternating_bass(self):
|
||||
def test_acoustic_combines_independent_rhythms_and_alternating_bass(self):
|
||||
result = self.practice(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
|
||||
measure = result['staves'][0]['measures'][0]
|
||||
upper, bass = measure['voices']
|
||||
self.assertEqual((upper['id'], bass['id']), ('upper', 'bass'))
|
||||
self.assertNotEqual(tests.test_accompaniment.timing(upper['notes']), tests.test_accompaniment.timing(bass['notes']))
|
||||
attacks = [n for n in bass['notes'] if n['pitches']]
|
||||
self.assertEqual([generator.model.pitch_number(n['pitches'][0]) % 12 for n in attacks], [0, 7])
|
||||
for voice in (upper, bass):
|
||||
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in voice['notes']), 4)
|
||||
for note in voice['notes']:
|
||||
self.assertEqual(note['stem'], 'up' if voice is upper else 'down')
|
||||
strings = sorted(p['tablature']['string'] for p in note['pitches'])
|
||||
if strings:
|
||||
if voice is bass:
|
||||
self.assertEqual(len(strings), 1)
|
||||
self.assertIn(strings[0], (5, 6))
|
||||
else:
|
||||
self.assertIn(strings, ([2, 3], [3, 4], [2, 3, 4]))
|
||||
self.assertEqual(len(measure['voices']), 1)
|
||||
notes = measure['voices'][0]['notes']
|
||||
bass = [(n['onset'], p) for n in notes for p in n['pitches'] if p['tablature']['string'] >= 5 and 'tie_from' not in p]
|
||||
upper = {generator.model.fraction(n['onset']) for n in notes for p in n['pitches'] if p['tablature']['string'] <= 4 and 'tie_from' not in p}
|
||||
self.assertNotEqual({generator.model.fraction(onset) for onset, _ in bass}, upper)
|
||||
self.assertEqual([generator.model.pitch_number(p) % 12 for _, p in bass], [0, 7])
|
||||
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in notes), 4)
|
||||
for note in notes:
|
||||
self.assertNotIn('stem', note)
|
||||
strings = sorted(p['tablature']['string'] for p in note['pitches'] if p['tablature']['string'] <= 4)
|
||||
if strings:
|
||||
self.assertIn(strings, ([2, 3], [3, 4], [2, 3, 4]))
|
||||
timeline = generator.playback.compile_timeline(result)['events']
|
||||
by_id = {p['id']: p for v in measure['voices'] for n in v['notes'] for p in n['pitches']}
|
||||
for event in timeline:
|
||||
@@ -72,8 +68,8 @@ class GuitarPartTests(unittest.TestCase):
|
||||
chord = generator.harmony.parse_symbol(symbol)
|
||||
source['staves'][0]['measures'][0]['harmonies'] = [{**chord, 'id': 'h1', 'onset': generator.model.rational(0)}]
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
bass = result['staves'][0]['measures'][0]['voices'][1]
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for n in bass['notes'] for p in n['pitches']}, {expected})
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for n in notes for p in n['pitches'] if p['tablature']['string'] >= 5}, {expected})
|
||||
def test_acoustic_bass_and_upper_chord_can_attack_together(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
prepared = generator.parts.prepare_document(source, practice=True)
|
||||
@@ -81,11 +77,11 @@ class GuitarPartTests(unittest.TestCase):
|
||||
plan = generator.accompaniment.Plan((bar,), (bar,), ())
|
||||
result = generator.practice.generate._practice_document(prepared, 'acoustic', plan)
|
||||
generator.model.validate_document(result)
|
||||
upper, bass = result['staves'][0]['measures'][0]['voices']
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in upper['notes'] if n['pitches']], [0, 2])
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in bass['notes'] if n['pitches']], [0, 2])
|
||||
self.assertEqual(len(result['staves'][0]['measures'][0]['voices']), 1)
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in notes if n['pitches']], [0, 2])
|
||||
for onset in (0, 2):
|
||||
pitches = [p for voice in (upper, bass) for n in voice['notes'] if generator.model.fraction(n['onset']) == onset for p in n['pitches']]
|
||||
pitches = [p for n in notes if generator.model.fraction(n['onset']) == onset for p in n['pitches']]
|
||||
positions = [generator.guitar.TabPosition(p['tablature']['string'], p['tablature']['fret'], int(generator.model.pitch_number(p) - 12)) for p in pitches]
|
||||
self.assertIn(tuple(sorted((p.string_number for p in positions), reverse=True)), generator.guitar.ACOUSTIC_STRINGS)
|
||||
self.assertTrue(generator.guitar.within_fret_span(positions))
|
||||
|
||||
Reference in New Issue
Block a user