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