File size: 3,472 Bytes
dee8df0 21c2288 dee8df0 21c2288 dee8df0 6b1f7f2 9249c0d 21c2288 dee8df0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | """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()
|