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()