"""Contracts that prevent vocal ABC leakage and accidental timing expansion.""" from __future__ import annotations import unittest from logic import drum_hits, instrumental_abc, notes_to_abc, validate_duration NATIVE = '''X:1 M:4/4 L:1/16 V: Vocal clef=treble name="Vocal Melody" snm="Vocal" V: Ins clef=treble name="Ins Melody" snm="Inst." K:B % chorus V: Vocal "Bm"z12F,4| V: Ins Z1| V: Vocal Z| V: Ins Z1| ''' class LogicTest(unittest.TestCase): def test_move_melody_to_instrument_without_chord_or_voice(self): result = instrumental_abc(NATIVE) self.assertNotIn("Vocal", result) self.assertNotIn('"Bm"', result) self.assertIn("z12F,4|", result) self.assertIn("V: Ins\nz12F,4|", result) self.assertIn("V: Ins\nZ|", result) self.assertLess(result.index("V: Ins clef"), result.index("K:B")) def test_composition_keeps_chords_but_no_vocal_slot(self): result = instrumental_abc(NATIVE, keep_chords=True) self.assertIn('"Bm"z12F,4|', result) self.assertNotIn("Vocal", result) def test_unknown_layout_fails(self): with self.assertRaises(ValueError): instrumental_abc(NATIVE.replace("Z1|", "z16|", 1)) def test_fallback_abc_is_instrumental_and_has_source_order(self): abc = notes_to_abc([(.08, .52, 60), (.68, 1.1, 60), (1.3, 1.7, 62)], 2.) self.assertEqual(instrumental_abc(abc), abc) self.assertNotIn("Vocal", abc) self.assertIn("V: Ins", abc) self.assertIn("C7", abc) self.assertIn("D6", abc) def test_duration_bounds(self): validate_duration(0.4) validate_duration(35) for seconds in (0.1, 35.1): with self.assertRaises(ValueError): validate_duration(seconds) def test_drums_follow_beats_and_skip_long_melody_pause(self): beats = "".join(f"{i * .5}\t{i % 4 + 1}\t4\t4\n" for i in range(12)) hits = drum_hits(beats, [(0.2, 0.5), (2.2, 5.8)], 6.0) self.assertIn((0.0, 36, 90), hits) self.assertIn((2.0, 36, 90), hits) self.assertIn((2.5, 38, 76), hits) self.assertFalse(any(0.75 <= time <= 1.75 for time, _, _ in hits)) self.assertTrue(all(0 <= time < 6 for time, _, _ in hits)) def test_drums_reject_bad_beat_grid(self): for beats in ("0\t1\t4\t4\n0\t2\t4\t4\n1\t3\t4\t4\n", "0\t1\t5\t4\n.5\t2\t5\t4\n1\t3\t5\t4\n"): with self.assertRaises(ValueError): drum_hits(beats, [(0.1, 1.1)], 2.0) def test_drums_ignore_duplicate_zero_anchor_before_first_downbeat(self): # Seen in the source-time SheetSage2 beat.lab for input-001. beats = ("0\t1\t4\t4\n.44\t1\t4\t4\n.92\t2\t4\t4\n" "1.44\t3\t4\t4\n1.92\t4\t4\t4\n2.44\t1\t4\t4\n") hits = drum_hits(beats, [(.22, 2.5)], 2.6) self.assertFalse(any(time == 0 for time, _, _ in hits)) self.assertIn((.44, 36, 90), hits) def test_drums_ignore_sheet_sage_initial_one_eighth_anchor(self): # Seen in HumTrans F05/M01: 1/8 at zero precedes a real 3/4 or 4/4 grid. beats = ("0.0\t1\t1\t8\n.19\t1\t3\t4\n.67\t2\t3\t4\n" "1.16\t3\t3\t4\n1.67\t1\t3\t4\n") hits = drum_hits(beats, [(.2, 1.8)], 2.0) self.assertFalse(any(time == 0 for time, _, _ in hits)) self.assertIn((.19, 36, 90), hits) if __name__ == "__main__": unittest.main()