File size: 6,318 Bytes
b2e4883
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#!/usr/bin/env python3
"""
Record the current script's output as a baseline YAML.

Run this once to create the expected/ files. Re-run only when the user's
contract changes (i.e., when they edit the .gp5 files and want the new
expected counts). NOT part of the test suite itself.
"""
import os
import sys
import collections
import subprocess

import mido
import yaml


REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
GP5_DIR = os.path.join(REPO_ROOT, "gp5_songs")
EXPECTED_DIR = os.path.join(os.path.dirname(__file__), "expected")
SCRIPT = os.path.join(REPO_ROOT, "gp5_to_keyswitched_mid.py")

NOTE_NAMES = {
    17: "SusDown", 18: "SusUp", 20: "PalmDown", 23: "B0(SlideDown)",
    24: "C1(SlideUp)", 26: "D1(Hammer)", 27: "D#1(SlideIn)", 91: "Bend",
    9: "Harm",
}


def keyswitch_counts(midi_path):
    counts = collections.Counter()
    mids = mido.MidiFile(midi_path)
    if len(mids.tracks) < 2:
        return {}
    for msg in mids.tracks[1]:
        if hasattr(msg, "channel") and msg.type == "note_on" and msg.velocity > 0:
            counts[msg.note] += 1
    return {NOTE_NAMES.get(n, str(n)): c for n, c in sorted(counts.items())}


def conductor_facts(midi_path):
    mids = mido.MidiFile(midi_path)
    if not mids.tracks:
        return {}
    facts = {}
    for msg in mids.tracks[0]:
        if msg.type == "set_tempo":
            facts["tempo_bpm"] = round(60_000_000 / msg.tempo)
        elif msg.type == "time_signature":
            facts["numerator"] = msg.numerator
            facts["denominator"] = msg.denominator
    return facts


def first_event_tick(midi_path):
    mid = mido.MidiFile(midi_path)
    if len(mid.tracks) < 2:
        return None
    abs_t = 0
    for msg in mid.tracks[1]:
        abs_t += msg.time
        if hasattr(msg, "channel") and msg.type == "note_on" and msg.velocity > 0:
            return abs_t
    return None


def last_event_tick(midi_path):
    mid = mido.MidiFile(midi_path)
    if len(mid.tracks) < 2:
        return None
    abs_t = 0
    last = 0
    for msg in mid.tracks[1]:
        abs_t += msg.time
        if hasattr(msg, "channel"):
            last = abs_t
    return last


def track_is_bass(track_name):
    return "bass" in track_name.lower()


def song_block(gp5_filename):
    """Run the script for each track in the song and return the expected data."""
    gp5_path = os.path.join(GP5_DIR, gp5_filename)
    song_name = os.path.splitext(gp5_filename)[0]
    songs_subdir = os.path.join(REPO_ROOT, song_name)

    # First pass: run the script for each track to populate the output dir.
    # We always pass --only "<track>" to get a single output file per run.
    track_names = []
    # We need to know the track names. Run the script once in verbose mode
    # and parse the printed output for the list of tracks.
    proc = subprocess.run(
        ["python", SCRIPT, gp5_path],
        capture_output=True, text=True, cwd=REPO_ROOT,
    )
    # Lines like "  Track Name  [track:Track Name] channel N"
    for line in proc.stdout.splitlines():
        line = line.strip()
        if not line or line.startswith("Skipped") or line.startswith("OK:"):
            continue
        if "No guitar" in line:
            continue
        if line.startswith("["):
            continue
        if "[track:" in line:
            name = line.split("[track:")[0].strip()
            channel = int(line.split("channel")[-1].strip())
            track_names.append((name, channel))

    tracks_expected = []
    for idx, (name, channel) in enumerate(track_names):
        is_bass = track_is_bass(name)
        # Re-run for this specific track. If the same name appears more
        # than once, use --only-index to disambiguate.
        cmd = ["python", SCRIPT, gp5_path, "-o", name, "-q"]
        if is_bass:
            cmd.insert(4, "-b")
        same_name_count = sum(1 for n, _ in track_names if n == name)
        if same_name_count > 1:
            # 1-based index of this occurrence.
            only_index = sum(1 for n, _ in track_names[:idx] if n == name) + 1
            cmd += ["--only-index", str(only_index)]
        try:
            subprocess.run(cmd, check=True, cwd=REPO_ROOT)
        except subprocess.CalledProcessError:
            # The run may have produced a multi-track file when
            # only-index wasn't applied. Skip this track.
            print(f"  WARN: {name} (index {idx}) export failed, skipping",
                  file=sys.stderr)
            continue

        # Sanity: the output file should exist. With --only-index, the
        # filename gets a " (N)" suffix when there are multiple matches.
        midi_path = os.path.join(songs_subdir, f"{name}_keyswitched.mid")
        candidate_paths = [midi_path]
        if same_name_count > 1:
            only_index = sum(1 for n, _ in track_names[:idx] if n == name) + 1
            candidate_paths.insert(0, os.path.join(
                songs_subdir, f"{name} ({only_index})_keyswitched.mid"))
        actual_midi_path = None
        for p in candidate_paths:
            if os.path.exists(p):
                actual_midi_path = p
                break
        if actual_midi_path is None:
            print(f"  WARN: expected output missing for {name} (index {idx})",
                  file=sys.stderr)
            continue

        tracks_expected.append({
            "name": name,
            "channel": channel,
            "include_bass": is_bass,
            "midi_path": f"{song_name}/{os.path.basename(actual_midi_path)}",
            "conductor": conductor_facts(actual_midi_path),
            "first_event_tick": first_event_tick(actual_midi_path),
            "last_event_tick": last_event_tick(actual_midi_path),
            "keyswitch_counts": keyswitch_counts(actual_midi_path),
        })

    return {
        "source_gp5": gp5_filename,
        "songs_subdir": song_name,
        "tracks": tracks_expected,
    }


def main():
    gp5_files = sorted(f for f in os.listdir(GP5_DIR) if f.endswith(".gp5"))
    expected = {"songs": [song_block(gp5) for gp5 in gp5_files]}
    out_path = os.path.join(EXPECTED_DIR, "baseline.yaml")
    with open(out_path, "w") as f:
        yaml.dump(expected, f, sort_keys=False, default_flow_style=False)
    print(f"Wrote {out_path}")


if __name__ == "__main__":
    main()