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