JacobLinCool's picture
Restore selectable legacy model generations
be60cf4
Raw
History Blame Contribute Delete
4.78 kB
"""TJA emission for slot-mode charts: slot indices ARE the chart.
No quantization happens here — generate_song_slot() already produced exact
lattice positions (measure, slot in 0..95), so this writer just prints them.
Preview rendering and the .tja are identical by construction. Piecewise
grids additionally emit #BPMCHANGE lines between measures.
"""
import numpy as np
from .vocab import SLOTS
CHAR = {"don": "1", "ka": "2", "don_big": "3", "ka_big": "4",
"roll": "5", "roll_big": "6", "balloon": "7"}
def grid_measure_starts(grid, n_measures):
"""Return measure boundary times for a slot grid, extending if needed."""
db = np.asarray(grid.get("downbeats", []), dtype=float)
if len(db) >= n_measures + 1:
return db[: n_measures + 1]
if len(db) >= 2:
step = float(np.median(np.diff(db)))
start = float(db[0])
else:
step = 240.0 / float(grid["bpm"])
start = float(db[0]) if len(db) else 0.0
return start + np.arange(n_measures + 1, dtype=float) * step
def gogo_measure_mask(plan, measure_starts, n_measures):
"""Map plan climax blocks (flag == 2) to TJA measures."""
mask = [False] * n_measures
if not plan:
return mask
starts = np.asarray(measure_starts, dtype=float)
if len(starts) < n_measures + 1:
return mask
for block in plan:
if len(block) < 4 or int(block[3]) != 2:
continue
a, b = float(block[0]), float(block[1])
if b <= a:
continue
for m in range(n_measures):
if starts[m] < b and starts[m + 1] > a:
mask[m] = True
return mask
def append_measure_with_gogo(lines, measure_line, measure_idx, gogo_mask, in_gogo):
"""Append a measure line, opening/closing #GOGO commands at boundaries."""
want_gogo = bool(gogo_mask[measure_idx]) if measure_idx < len(gogo_mask) else False
if in_gogo and not want_gogo:
lines.append("#GOGOEND")
in_gogo = False
if want_gogo and not in_gogo:
lines.append("#GOGOSTART")
in_gogo = True
lines.append(measure_line)
return in_gogo
def write_tja_slots(gen, grid, title, course, level, wave, out_path=None,
balloon_count=10, plan=None):
"""gen: generate_song_slot() result; grid: fit_grid() result (or a dict
with downbeats+bpm). Returns the TJA text (and writes it if out_path)."""
slots = {}
for me, sl, cls in gen["hits_slots"]:
slots.setdefault((me, sl), CHAR[cls])
for m0, s0, m1, s1, typ in gen.get("spans_slots", []):
a = (m0, s0)
while a in slots: # span start yields to hits: next free slot
a = (a[0] + (a[1] + 1) // SLOTS, (a[1] + 1) % SLOTS)
b = (m1, s1)
while b in slots or b <= a:
b = (b[0] + (b[1] + 1) // SLOTS, (b[1] + 1) % SLOTS)
slots[a] = CHAR[typ]
slots[b] = "8"
n_meas = max(gen.get("n_measures", 0),
(max(m for m, _ in slots) + 1) if slots else 1)
# piecewise-tempo grids: emit #BPMCHANGE whenever the per-measure BPM
# (from consecutive fitted barlines) moves; TJA measure lines are unchanged
db = np.asarray(grid.get("downbeats", []), float) if grid.get("piecewise") else None
measure_starts = grid_measure_starts(grid, n_meas)
gogo_mask = gogo_measure_mask(plan, measure_starts, n_meas)
lines = []
cur_bpm = float(grid["bpm"])
in_gogo = False
for m in range(n_meas):
if in_gogo and not gogo_mask[m]:
lines.append("#GOGOEND")
in_gogo = False
if db is not None and m + 1 < len(db):
bpm_m = 240.0 / (db[m + 1] - db[m])
if abs(bpm_m - round(bpm_m)) < 0.05:
bpm_m = float(round(bpm_m))
if abs(bpm_m - cur_bpm) > 0.05:
lines.append(f"#BPMCHANGE {bpm_m:g}")
cur_bpm = bpm_m
in_gogo = append_measure_with_gogo(
lines, "".join(slots.get((m, k), "0") for k in range(SLOTS)) + ",",
m, gogo_mask, in_gogo)
if in_gogo:
lines.append("#GOGOEND")
balloons = [balloon_count] * sum(1 for s in gen.get("spans_slots", [])
if s[4] == "balloon")
offset = float(grid["downbeats"][0]) if len(grid.get("downbeats", [])) else 0.0
tja = "\n".join([
f"TITLE:{title} (SoftChart)", f"BPM:{grid['bpm']:g}", f"WAVE:{wave}",
f"OFFSET:{-offset:.3f}",
f"COURSE:{'Oni' if course == 'oni' else str(course).capitalize()}",
f"LEVEL:{level}",
f"BALLOON:{','.join(map(str, balloons))}" if balloons else "BALLOON:",
"", "#START", *lines, "#END"]) + "\n"
if out_path:
with open(out_path, "w") as f:
f.write(tja)
return tja