Fix stepmania generator offset
This commit is contained in:
@@ -175,16 +175,17 @@ def convert_to_mp4_video(input_path: str, callback=None) -> str:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
'ffmpeg', '-i', str(input_path),
|
'ffmpeg', '-i', str(input_path),
|
||||||
'-map', '0:v:0',
|
'-map', '0:v:0',
|
||||||
'-c:v', 'libx264',
|
'-c:v', 'libx264',
|
||||||
'-preset', 'medium',
|
'-preset', 'medium',
|
||||||
'-crf', '18',
|
'-crf', '26',
|
||||||
'-pix_fmt', 'yuv420p',
|
'-vf', 'scale=-2:720',
|
||||||
'-an',
|
'-pix_fmt', 'yuv420p',
|
||||||
'-movflags', '+faststart',
|
'-an',
|
||||||
'-y',
|
'-movflags', '+faststart',
|
||||||
str(output_path)
|
'-y',
|
||||||
|
str(output_path)
|
||||||
],
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -596,6 +597,26 @@ class AudioAnalyzer:
|
|||||||
self.onset_strengths = raw / mx if mx > 0 else raw
|
self.onset_strengths = raw / mx if mx > 0 else raw
|
||||||
self._log(f"Found {len(self.onset_times)} onsets", 65)
|
self._log(f"Found {len(self.onset_times)} onsets", 65)
|
||||||
|
|
||||||
|
def get_sm_offset(self) -> float:
|
||||||
|
"""Return a small SM offset that keeps downbeats on measure boundaries."""
|
||||||
|
if not self.bpm or self.bpm <= 0:
|
||||||
|
return -self.first_downbeat
|
||||||
|
beat_period = 60.0 / self.bpm
|
||||||
|
if beat_period <= 0:
|
||||||
|
return -self.first_downbeat
|
||||||
|
|
||||||
|
beats_from_zero = self.first_downbeat / beat_period
|
||||||
|
target_beat = round(beats_from_zero / 4.0) * 4.0
|
||||||
|
target_time = target_beat * beat_period
|
||||||
|
|
||||||
|
# Offset is the time shift so that the first downbeat lands on target_beat.
|
||||||
|
offset = -(self.first_downbeat - target_time)
|
||||||
|
return float(offset)
|
||||||
|
|
||||||
|
def get_chart_time_offset(self) -> float:
|
||||||
|
"""Return the time (s) corresponding to beat 0 in the chart grid."""
|
||||||
|
return -self.get_sm_offset()
|
||||||
|
|
||||||
def get_dominant_band(self, t: float) -> int:
|
def get_dominant_band(self, t: float) -> int:
|
||||||
"""Return dominant frequency band 0-3 at time *t*.
|
"""Return dominant frequency band 0-3 at time *t*.
|
||||||
|
|
||||||
@@ -717,12 +738,12 @@ class StepChartGenerator:
|
|||||||
|
|
||||||
# -- post-processing rules (ergonomic / musical polish) --
|
# -- post-processing rules (ergonomic / musical polish) --
|
||||||
|
|
||||||
def _postprocess(self, measures, subdiv, cfg):
|
def _postprocess(self, measures, subdiv, cfg, offset_time):
|
||||||
"""Apply rules to make charts feel more natural & playable."""
|
"""Apply rules to make charts feel more natural & playable."""
|
||||||
bpm = self.az.bpm
|
bpm = self.az.bpm
|
||||||
spm = 4 * 60.0 / bpm
|
spm = 4 * 60.0 / bpm
|
||||||
spr = spm / subdiv
|
spr = spm / subdiv
|
||||||
offset = self.az.first_downbeat
|
offset = offset_time
|
||||||
|
|
||||||
# ---------- Rule 1: Mute arrows during quiet sections ----------
|
# ---------- Rule 1: Mute arrows during quiet sections ----------
|
||||||
for m_idx, meas in enumerate(measures):
|
for m_idx, meas in enumerate(measures):
|
||||||
@@ -829,8 +850,8 @@ class StepChartGenerator:
|
|||||||
def generate_chart(self, name):
|
def generate_chart(self, name):
|
||||||
cfg = self.CONFIGS[name]
|
cfg = self.CONFIGS[name]
|
||||||
bpm = self.az.bpm
|
bpm = self.az.bpm
|
||||||
# Use the first downbeat as reference, not just beat_times[0]
|
# Use the computed chart time offset (beat 0 reference)
|
||||||
offset = self.az.first_downbeat
|
offset = self.az.get_chart_time_offset()
|
||||||
bpmeas = 4 # beats per measure (4/4)
|
bpmeas = 4 # beats per measure (4/4)
|
||||||
spm = bpmeas * 60.0 / bpm # seconds per measure
|
spm = bpmeas * 60.0 / bpm # seconds per measure
|
||||||
subdiv = cfg['subdiv']
|
subdiv = cfg['subdiv']
|
||||||
@@ -879,7 +900,7 @@ class StepChartGenerator:
|
|||||||
measures.append(mrows)
|
measures.append(mrows)
|
||||||
|
|
||||||
# ---- post-processing ----
|
# ---- post-processing ----
|
||||||
measures = self._postprocess(measures, subdiv, cfg)
|
measures = self._postprocess(measures, subdiv, cfg, offset)
|
||||||
|
|
||||||
# trim trailing empty measures (keep at least 1)
|
# trim trailing empty measures (keep at least 1)
|
||||||
while len(measures) > 1 and all(
|
while len(measures) > 1 and all(
|
||||||
@@ -935,8 +956,8 @@ class SMFileWriter:
|
|||||||
title = Path(self.az.filepath).stem
|
title = Path(self.az.filepath).stem
|
||||||
music = os.path.basename(self.music_file)
|
music = os.path.basename(self.music_file)
|
||||||
video = os.path.basename(self.video_file) if self.video_file else None
|
video = os.path.basename(self.video_file) if self.video_file else None
|
||||||
# Use the first downbeat (properly aligned) for the SM offset
|
# Use a small offset aligned to the nearest measure boundary
|
||||||
offset = -self.az.first_downbeat
|
offset = self.az.get_sm_offset()
|
||||||
preview = self.az.duration * 0.30
|
preview = self.az.duration * 0.30
|
||||||
|
|
||||||
if video:
|
if video:
|
||||||
@@ -1013,8 +1034,8 @@ class App:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.title("StepMania .sm Generator")
|
self.root.title("StepMania .sm Generator")
|
||||||
self.root.geometry("740x620")
|
self.root.geometry("700x750")
|
||||||
self.root.minsize(620, 520)
|
self.root.minsize(600, 700)
|
||||||
|
|
||||||
# variables
|
# variables
|
||||||
self.v_in = tk.StringVar()
|
self.v_in = tk.StringVar()
|
||||||
|
|||||||
Reference in New Issue
Block a user