File size: 3,884 Bytes
ff7b988
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import tempfile
from collections import Counter

from ..utils import run_cmd_sync
from .basic import HumanPitchName
from .internal import Harmony, Key, KeyChanges, Melody, MeterChanges


def theorytab_find_applicable(timed_events, search_event, eps=1e-3):
    candidates = [t for t in timed_events if (search_event["beat"] - t["beat"]) > -eps]
    if len(candidates) == 0:
        raise ValueError()
    return candidates[-1]


def estimate_key_changes(meter_changes, harmony, melody):
    meter_changes = MeterChanges(*meter_changes)
    harmony = Harmony(*harmony)
    melody = Melody(*melody)

    # Compute total num tertiary
    meter = meter_changes[0][1]
    assert meter in [(3, 2, 2), (4, 2, 2)]
    tertiary_per_pulse = meter[1] * meter[2]
    tertiary_per_group = meter[0] * tertiary_per_pulse
    total_num_tertiary = 0 if len(harmony) == 0 else harmony[-1][0] + 1
    total_num_tertiary = max(
        total_num_tertiary, 0 if len(melody) == 0 else sum(melody[-1][:2])
    )
    while total_num_tertiary % tertiary_per_group != 0:
        total_num_tertiary += 1

    # Fake tempo
    ppm = 120
    tertiary_to_ms = lambda t: round((t / tertiary_per_pulse) / (ppm / 60) * 1000)

    # "Beat" events
    lines = []
    for t in range(0, total_num_tertiary + tertiary_per_pulse, tertiary_per_pulse):
        strength = 1
        if t % tertiary_per_group == 0:
            strength = 4
        elif meter == (4, 2, 2) and t % (tertiary_per_pulse * 2) == 0:
            strength = 2
        lines.append(("Beat", tertiary_to_ms(t), strength))

    # "Chord" events
    pc_to_melisma_pc = {pc: (2 + (7 * pc)) % 12 for pc in range(12)}
    for i, (t, c) in enumerate(harmony):
        if i == 0:
            t = 0
        if i + 1 < len(harmony):
            d = harmony[i + 1][0] - t
        else:
            d = total_num_tertiary - t
        lines.append(
            ("Chord", tertiary_to_ms(t), tertiary_to_ms(t + d), pc_to_melisma_pc[c[0]])
        )

    # "Note" events
    for t, d, n in melody:
        lines.append(
            ("Note", tertiary_to_ms(t), tertiary_to_ms(t + d), n.as_midi_pitch())
        )

    parameters = """
verbosity=1
default_profile_value = 1.5
npc_or_tpc_profile=0
scoring_mode = 1
segment_beat_level=3
beat_printout_level=2
romnums=0
romnum_type=0
running=0

%CBMS MODEL
major_profile = 5.0 2.0 3.5 2.0 4.5 4.0 2.0 4.5 2.0 3.5 1.5 4.0
minor_profile = 5.0 2.0 3.5 4.5 2.0 4.0 2.0 4.5 3.5 2.0 1.5 4.0
change_penalty=12

%K-S MODEL
%major_profile = 6.35 2.23 3.48 2.33 4.38 4.09 2.52 5.19 2.39 3.66 2.29 2.88
%minor_profile = 6.33 2.68 3.52 5.38 2.60 3.53 2.54 4.75 3.98 2.69 3.34 3.17
%change_penalty = 2.3

%BAYESIAN MODEL
%major_profile = 0.748 0.060 0.488 0.082 0.670 0.460 0.096 0.715 0.104 0.366 0.057 0.400
%minor_profile = 0.712 0.084 0.474 0.618 0.049 0.460 0.105 0.747 0.404 0.067 0.133 0.330
%change_penalty = 0.002
    """.strip()

    formatted = "\n".join(["\t".join([str(a) for a in l]) for l in lines])
    with tempfile.NamedTemporaryFile() as f, tempfile.NamedTemporaryFile() as p:
        with open(f.name, "w") as f:
            f.write(formatted)
        with open(p.name, "w") as p:
            p.write(parameters)
        res, stdout, stderr = run_cmd_sync(
            f"melisma-key -p {p.name} {f.name}", timeout=60
        )
        if res != 0 or len(stderr) > 0:
            raise Exception(f"{stdout}\n{stderr}".strip())

    key_to_count = Counter()
    for key in stdout.split():
        if key.endswith("m"):
            scale = (2, 1, 2, 2, 1, 2)
            key = key[:-1]
        else:
            scale = (2, 2, 1, 2, 2, 2)
        key = Key(HumanPitchName(key).as_pitch_class(), scale)
        key_to_count[key] += 1
    if len(key_to_count) == 0:
        raise Exception("Failed to estimate key")
    key = sorted(key_to_count.keys(), key=lambda k: key_to_count[k])[-1]
    return KeyChanges((0, key))