"""Contextual musical validity needs notes and chords, not author annotations.""" import dataclasses import json import fractions import pathlib import unittest import generator.train import generator.training_io import generator.harmonic_time import generator.harmony import generator.solo.harmonic import generator.solo.types import generator.solo.decoder import generator.solo.learned import generator.source import tests.solo_fixtures import generator.musicxml import generator.parts import generator.solo.authored ROOT = pathlib.Path(__file__).resolve().parents[1] class ContextualHarmony(unittest.TestCase): def test_approved_corpus_passes_without_annotations_or_authored_privileges(self): examples, _ = generator.train.load_examples(ROOT / 'data/training/manifest.json') manifest = json.loads((ROOT / 'data/training/manifest.json').read_text()) for entry in manifest['examples']: raw = generator.training_io.read(ROOT / 'data/training' / entry['solo'], '', pitch_convention=entry['pitch_convention']) self.assertEqual(raw['annotations'], []) doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(ROOT / 'data/training' / entry['source'])) h = generator.solo.harmonic.Harmony(generator.harmonic_time.solo_source(doc['staves'][0])) ns = tuple(dataclasses.replace(n, role='ordinary', sources=(), event_id='') for n in generator.solo.authored.sounding(raw)) checked = h.evaluate(ns, complete=True) self.assertTrue(checked.valid, (entry['id'], checked.error)) self.assertTrue(checked.contracts) self.assertEqual(len(examples), 4) def test_passing_to_nonchord_scale_note(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('G6',))) notes = (tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'g'), tests.solo_fixtures.note(fractions.Fraction(1, 2), 1, 'g', alter=1), tests.solo_fixtures.note(1, 2, 'a')) result = h.evaluate(notes) self.assertTrue(result.valid, result.error) self.assertEqual(result.contracts[0].kind, 'passing') self.assertFalse(h.evaluate(notes, infer=False).valid) self.assertFalse(h.evaluate(notes[:-1]).valid) leap = (*notes[:2], dataclasses.replace(notes[2], pitch=generator.solo.types.Pitch('b', fractions.Fraction(), 4))) self.assertFalse(h.evaluate(leap).valid) long = (notes[0], dataclasses.replace(notes[1], end=fractions.Fraction(3, 2)), dataclasses.replace(notes[2], start=fractions.Fraction(3, 2))) self.assertFalse(h.evaluate(long).valid) def test_passing_contract_requires_its_actual_preparation(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('G6',))) a = tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'g', event_id='a') b = tests.solo_fixtures.note(fractions.Fraction(1, 2), 1, 'g', alter=1, event_id='b') c = tests.solo_fixtures.note(1, 2, 'a', event_id='c') contract = generator.solo.types.ExpressionContract('p', 'passing', 'chromatic_passing', (b,), c, 'GM6', preparation=a) self.assertTrue(h.evaluate((a, b, c), expressions=(contract,)).valid) changed = dataclasses.replace(a, end=fractions.Fraction(1, 4)) wrong = dataclasses.replace(contract, preparation=changed) self.assertFalse(h.evaluate((changed, b, c), expressions=(wrong,)).valid) high = dataclasses.replace(c, pitch=dataclasses.replace(c.pitch, octave=5)) self.assertFalse(h.evaluate((a, b, high), expressions=(dataclasses.replace(contract, target=high),)).valid) def test_color_resolution_can_have_intervening_valid_motion(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7',))) notes = (tests.solo_fixtures.note(0, fractions.Fraction(3, 2), 'f', alter=1), tests.solo_fixtures.note(fractions.Fraction(3, 2), 2, 'a'), tests.solo_fixtures.note(2, 4, 'g')) checked = h.evaluate(notes) self.assertTrue(checked.valid, checked.error) self.assertEqual(checked.contracts[0].rule, 'lydian_color') self.assertFalse(h.evaluate(notes[:2]).valid) bad_mid = dataclasses.replace(notes[1], pitch=generator.solo.types.Pitch('g', fractions.Fraction(1), 5)) self.assertFalse(h.evaluate((notes[0], bad_mid, notes[2])).valid) def test_anticipated_stable_and_resolving_colors(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Bbdim7', 'Am7'))) n = tests.solo_fixtures.note(fractions.Fraction(7, 2), 6, 'd') result = h.evaluate((n,)) self.assertTrue(result.valid, result.error) self.assertEqual(result.contracts[0].kind, 'anticipated_color') self.assertFalse(h.evaluate((dataclasses.replace(n, start=fractions.Fraction(2)),)).valid) h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Gm7', 'C7'))) color = tests.solo_fixtures.note(fractions.Fraction(7, 2), fractions.Fraction(11, 2), 'f', alter=1) target = tests.solo_fixtures.note(fractions.Fraction(11, 2), 7, 'g') self.assertTrue(h.evaluate((color, target)).valid) self.assertFalse(h.evaluate((color,)).valid) flat = dataclasses.replace(color, pitch=generator.solo.types.Pitch('g', fractions.Fraction(-1), 4)) self.assertTrue(h.evaluate((flat, target)).valid) def test_repeated_harmony_cannot_reset_color_duration(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7', 'C7'))) color = tests.solo_fixtures.note(3, 6, 'f', alter=1) target = tests.solo_fixtures.note(6, 7, 'g') self.assertFalse(h.evaluate((color, target)).valid) def test_held_color_is_independent_of_notation_partition(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Bbdim7', 'Am7'))) whole = tests.solo_fixtures.note(fractions.Fraction(7, 2), 6, 'd') split = (dataclasses.replace(whole, end=fractions.Fraction(4)), dataclasses.replace(whole, start=fractions.Fraction(4), continuation=True)) self.assertEqual(h.evaluate((whole,)), h.evaluate(split)) self.assertEqual(h.describe((whole,)), h.describe(split)) def test_unsupported_notes_are_not_authorized_by_labels(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7',))) for role in ('ordinary', 'authored', 'quote', 'color', 'Lead in'): self.assertFalse(h.evaluate((tests.solo_fixtures.note(0, 3, 'c', alter=1, role=role),)).valid) missing = generator.solo.harmonic.Harmony(()) self.assertFalse(missing.evaluate((tests.solo_fixtures.note(0, 1, 'c'),)).valid) def test_fully_expired_partial_treatments_remain_invalid(self): h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7',))) notes = (tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'd', alter=1, event_id='a'), tests.solo_fixtures.note(fractions.Fraction(1, 2), 2, 'e', event_id='b')) contract = generator.solo.types.ExpressionContract('a', 'approach', 'below', (notes[0],), notes[1], 'C7') partial = h.evaluate(notes[:1], until=fractions.Fraction(1, 2), expressions=(contract,)) self.assertTrue(partial.valid) self.assertEqual(len(partial.pending), 1) self.assertFalse(h.evaluate(notes[:1], until=fractions.Fraction(2), expressions=(contract,)).valid) def test_pickup_uses_source_form_context_without_overriding_written_harmony(self): doc = tests.solo_fixtures.song(count=3, chords=('C', 'Dm', 'G7')) staff = doc['staves'][0] pickup = staff['measures'][0] pickup['implicit'] = True pickup['harmonies'] = [] self.assertEqual(generator.harmony.symbol(generator.harmonic_time.solo_source(staff)[0].chord), 'G7') self.assertIsNone(generator.harmonic_time.read(staff)[0].chord) pickup['harmonies'] = [dict(generator.harmony.parse_symbol('F'), onset={'numerator': 0, 'denominator': 1})] self.assertEqual(generator.harmony.symbol(generator.harmonic_time.solo_source(staff)[0].chord), 'FM') pickup['harmonies'] = [{'kind': 'none', 'onset': {'numerator': 0, 'denominator': 1}}] self.assertIsNone(generator.harmonic_time.solo_source(staff)[0].chord) pickup['harmonies'] = [] pickup['implicit'] = False self.assertIsNone(generator.harmonic_time.solo_source(staff)[0].chord) class PredictionReachability(unittest.TestCase): def engine(self, chords): doc = tests.solo_fixtures.song(count=2, chords=chords) return generator.solo.decoder.Decoder(generator.source.analyze(doc), generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(chords)), generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8)) def test_joint_resolved_color_reaches_the_real_action_builder(self): d = self.engine(('C7', 'FM7')) n = d.note(fractions.Fraction(), fractions.Fraction(3, 2), 66) actions = list(d.resolutions(n)) self.assertTrue(any([p for _, _, p in events] == [66, 69, 67] for events, _, _ in actions)) for _, notes, contracts in actions: self.assertTrue(d.harmony.evaluate(notes, expressions=contracts, infer=False).valid) def test_anticipated_color_is_a_normal_single_event_option(self): d = self.engine(('Bbdim7', 'Am7')) n = d.note(fractions.Fraction(7, 2), fractions.Fraction(6), 62) support = d.permission(n) self.assertTrue(support) self.assertEqual(support[0].kind, 'anticipated_color') d.expressions_enabled = False d._permissions.clear() self.assertIsNone(d.permission(n)) def test_delayed_action_retains_space_and_intervening_motion(self): d = self.engine(('C7', 'FM7')) n = d.note(fractions.Fraction(), fractions.Fraction(1, 2), 63) actions = list(d.resolutions(n)) self.assertTrue(any(c[0].kind == 'delayed_approach' and any(p is None for _, _, p in events) for events, _, c in actions)) self.assertTrue(any(c[0].kind == 'delayed_approach' and len(notes) == 3 for _, notes, c in actions)) def test_passing_action_uses_incoming_state(self): d = self.engine(('G6', 'G6')) first = d.note(fractions.Fraction(), fractions.Fraction(1, 2), 67) state = d.extend(generator.solo.decoder.State(), (((first.start, first.end, 67),), (first,), ())) actions = [a for a in d.actions(fractions.Fraction(1, 2)) if a[2] and a[2][0].kind == 'passing' and a[0] == ((fractions.Fraction(1, 2), fractions.Fraction(1), 68), (fractions.Fraction(1), fractions.Fraction(2), 69))] self.assertTrue(actions) self.assertIsNotNone(d.extend(state, actions[0])) self.assertIsNone(d.extend(generator.solo.decoder.State(), actions[0])) wrong = d.note(fractions.Fraction(), fractions.Fraction(1, 2), 64) prior = d.extend(generator.solo.decoder.State(), (((wrong.start, wrong.end, 64),), (wrong,), ())) self.assertIsNone(d.extend(prior, actions[0])) if __name__ == '__main__': unittest.main()