Files
product-vamp/tests/test_solo_passages.py
T
khodowany 0e68f5364b Cleanup
2026-10-04 18:48:02 -05:00

102 lines
6.4 KiB
Python

import dataclasses, fractions, unittest
import generator.harmony, generator.model, generator.scales.selection, generator.scales.timeline
import generator.solo.harmonic, generator.solo.types
import tests.scale_fixtures
from tests.solo_fixtures import note,choices
F=fractions.Fraction
def anchor(role='terminal', pitch='e', earliest=2, latest=2, duration=2, onset=False):
return generator.solo.types.Anchor('arrival', F(earliest), F(latest), note(0, 1, pitch).pitch, frozenset({4}), (55, 79), F(duration), role, 'C7', onset, F(4) if role == 'terminal' else None)
class HarmonicPassageTests(unittest.TestCase):
def test_same_collection_distinguishes_chord_function(self):
harmony = generator.solo.harmonic.Harmony(choices(('Gm7', 'C7', 'FM7', 'BbM7')))
self.assertEqual([s.roles.basic for s in harmony.spans], [frozenset(v) for v in ({7, 10, 2, 5}, {0, 4, 7, 10}, {5, 9, 0, 4}, {10, 2, 5, 9})])
def test_semantic_degrees_keep_alterations_omissions_suspensions_and_slash_bass(self):
for symbol in ('C7b5', 'C9', 'Csus4', 'C7no5', 'C/E', 'C/D'):
chord = generator.harmony.parse_symbol(symbol)
roles = generator.solo.harmonic.roles(chord)
self.assertFalse(roles.basic & roles.specified)
self.assertNotIn(2, generator.solo.harmonic.roles(generator.harmony.parse_symbol('C/D')).basic)
self.assertIn(2, generator.solo.harmonic.roles(generator.harmony.parse_symbol('C9')).specified)
self.assertIn(5, generator.solo.harmonic.roles(generator.harmony.parse_symbol('Csus4')).basic)
self.assertNotIn(7, generator.solo.harmonic.roles(generator.harmony.parse_symbol('C7no5')).basic)
def test_ordinary_long_destination_requires_chord_member(self):
harmony = generator.solo.harmonic.Harmony(choices(('C7',)))
self.assertTrue(harmony.evaluate((note(0, 2, 'e'),)).valid)
self.assertFalse(harmony.evaluate((note(0, 2, 'f'),)).valid)
self.assertLess(harmony.evaluate((note(0, 1, 'e'),)).exposure, harmony.evaluate((note(0, 1, 'f'),)).exposure)
def test_connected_passing_motion_is_permitted(self):
harmony = generator.solo.harmonic.Harmony(choices(('C7',)))
notes = (note(0, F(1, 2), 'g'), note(F(1, 2), 1, 'f'), note(1, 2, 'e'))
self.assertEqual(harmony.connected_motion(notes, 1), 'passing')
self.assertTrue(harmony.evaluate(notes).valid)
self.assertEqual(harmony.evaluate(notes).exposure, 0)
def test_common_tone_hold_needs_no_reattack(self):
harmony = generator.solo.harmonic.Harmony(choices(('Em7b5', 'A7'), scale=tests.scale_fixtures.library().scale('harmonic_minor', 1, 'D')))
self.assertTrue(harmony.evaluate((note(2, 6, 'g'),)).valid)
def test_held_tension_detected_without_scale_change(self):
harmony = generator.solo.harmonic.Harmony(choices(('Gm7', 'C7')))
notes = (note(2, 6, 'f'),)
self.assertFalse(harmony.evaluate(notes).valid)
self.assertEqual([v['function'] for v in harmony.describe(notes)], ['basic', 'mapped_motion'])
def test_prepared_suspension_requires_actual_resolution(self):
harmony = generator.solo.harmonic.Harmony(choices(('Gm7', 'C7')))
treatment = generator.solo.types.HarmonicTreatment('suspension', 'suspension', 65, F(4), F(5), 64, F(5))
notes = (note(2, F(9, 2), 'f'), note(F(9, 2), 6, 'e'))
self.assertTrue(harmony.evaluate(notes, (treatment,), complete=True).valid)
partial = (note(2, F(9, 2), 'f'),)
result = harmony.evaluate(partial, (treatment,), until=F(9, 2))
self.assertTrue(result.valid)
self.assertEqual(result.pending, (treatment,))
self.assertFalse(harmony.evaluate(partial, (treatment,), until=6, complete=True).valid)
self.assertFalse(harmony.evaluate((note(4, F(9, 2), 'f'), note(F(9, 2), 6, 'e')), (treatment,), complete=True).valid)
self.assertFalse(harmony.evaluate((note(2, F(9, 2), 'f'), note(F(9, 2), 6, 'g')), (treatment,), complete=True).valid)
def test_symbol_specified_color_requires_a_plan(self):
harmony = generator.solo.harmonic.Harmony(choices(('C9',)))
notes = (note(0, 2, 'd'),)
color = generator.solo.types.HarmonicTreatment('ninth', 'color', 62, F(0), F(2))
self.assertFalse(harmony.evaluate(notes).valid)
self.assertTrue(harmony.evaluate(notes, (color,)).valid)
ordinary = generator.solo.harmonic.Harmony(choices(('C7',)))
self.assertFalse(ordinary.evaluate(notes, (color,)).valid)
def test_quotation_tension_preserved_and_reported(self):
harmony = generator.solo.harmonic.Harmony(choices(('C7',)))
notes = (note(0, 2, 'f', role='quote'),)
self.assertTrue(harmony.evaluate(notes).valid)
report = harmony.describe(notes)
self.assertTrue(report[0]['protected'])
self.assertEqual(report[0]['function'], 'mapped_motion')
def test_intermediate_arrival_does_not_require_closure(self):
notes = (note(0, 1, 'g'), note(1, 2, 'e'), note(2, 3, 'f'), note(3, 4, 'g'))
middle = anchor('intermediate', earliest=1, latest=1, duration=1)
self.assertTrue(generator.solo.harmonic.anchors_valid((middle,), notes, F(4), complete=True)[0])
def test_terminal_touch_then_departure_is_not_closure(self):
terminal = dataclasses.replace(anchor(), duration=F(1, 2), hold_until=None)
notes = (note(2, F(5, 2), 'e'), note(F(5, 2), 4, 'g'))
self.assertFalse(generator.solo.harmonic.anchors_valid((terminal,), notes, F(4), complete=True, closure='closed')[0])
self.assertTrue(generator.solo.harmonic.anchors_valid((anchor(),), (note(2, 4, 'e'),), F(4), complete=True, closure='closed')[0])
def test_existing_hold_can_satisfy_anchor_only_when_allowed(self):
notes = (note(0, 4, 'e'),)
self.assertIsNotNone(generator.solo.harmonic.anchor_note(anchor(), notes))
self.assertIsNone(generator.solo.harmonic.anchor_note(anchor(onset=True), notes))
def test_scale_index_round_trips_negative_and_octave_crossings(self):
for scale in (tests.scale_fixtures.library().scale('major', 6, 'D'), tests.scale_fixtures.library().scale('harmonic_minor', 1, 'D')):
for index in range(-21, 28):
self.assertEqual(generator.solo.types.scale_index(scale, generator.solo.types.scale_pitch(scale, index).number()), index)
scale = tests.scale_fixtures.library().scale('major', 6, 'D')
self.assertEqual(generator.solo.types.scale_index(scale, 65) - generator.solo.types.scale_index(scale, 64), 1)