From 1489942a85da29229bdd209bbb70944cd5b4e15d Mon Sep 17 00:00:00 2001 From: orange-bot Date: Thu, 15 Oct 2026 14:36:05 +0800 Subject: [PATCH] recorder: add a BGM-follows-video clip option and document the existing clip switches The recorder already exposes the pause-screen and failed-attempt switches; the BGM cursor was only pinned to source time for rhythm-sensitive rooms. This adds an opt-in setting that pins every room's clips to the video, and a policy regression test for it (plus a stub so the recorder harness still compiles). --- Source/AutoRecorder.cs | 4 +++- Source/QolSettings.cs | 7 +++++++ Source/QolSettingsOverlay.cs | 8 ++++++++ Tests/Recorder/Stubs.cs | 2 +- Tests/Recording.Policy/Program.cs | 19 +++++++++++++++++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Source/AutoRecorder.cs b/Source/AutoRecorder.cs index 026d9af..09f730f 100644 --- a/Source/AutoRecorder.cs +++ b/Source/AutoRecorder.cs @@ -526,6 +526,7 @@ private static void BeginRun(Level level) { // The selected mode is run-wide; rhythm exceptions are attached to individual clips. reconstructBgm = MicroblocksQolUtilsModule.Settings.BgmMode == BgmRecordingMode.SfxOnlyWithPostMix; recordingRoomName = null; + recordingRoomBgmFollowsVideo = MicroblocksQolUtilsModule.Settings.RecordingBgmFollowsVideo; } private static void StartRunRecording(Level level) { @@ -968,7 +969,8 @@ private static void ObserveRoom(Level level) { DeathReplayPrefix.Add(deathClip); recordingRoomName = room; - recordingRoomBgmFollowsVideo = RhythmMapDetector.IsRhythmSensitive(level.Session.MapData, room); + recordingRoomBgmFollowsVideo = MicroblocksQolUtilsModule.Settings.RecordingBgmFollowsVideo + || RhythmMapDetector.IsRhythmSensitive(level.Session.MapData, room); if (branchActive) { branchStartSeconds = fullTime; branchMusicStart = MusicPosition.Read(); diff --git a/Source/QolSettings.cs b/Source/QolSettings.cs index 3cb0083..6114ebe 100644 --- a/Source/QolSettings.cs +++ b/Source/QolSettings.cs @@ -333,6 +333,13 @@ public AutoRecordingMode AutomaticRecording { [DefaultValue(false)] public bool RecordingKeepFailedAttempts { get; set; } + // Rhythm-sensitive rooms already read the real source time so music keeps its + // on-screen timing. Outside those rooms the BGM is reconstructed across cuts and + // therefore keeps playing while the video jumps; this opt-in asks for the strict + // behaviour (jump with the video) in every room. + [DefaultValue(false)] + public bool RecordingBgmFollowsVideo { get; set; } + [DefaultValue(60)] [SettingRange(30, 120)] public int RecordingFrameRate { get; set; } = 60; diff --git a/Source/QolSettingsOverlay.cs b/Source/QolSettingsOverlay.cs index 45b072a..703282e 100644 --- a/Source/QolSettingsOverlay.cs +++ b/Source/QolSettingsOverlay.cs @@ -1680,6 +1680,10 @@ SettingRow Golden(SettingRow row) { row.IsVisible = () => settings.AutomaticRecording == AutoRecordingMode.Golden; return row; } + SettingRow BgmRebuildOnly(SettingRow row) { + row.IsVisible = () => settings.BgmMode == BgmRecordingMode.SfxOnlyWithPostMix; + return row; + } Group(RecorderSettingsSection.Automatic, EnumRow("自动录制", () => settings.AutomaticRecording, value => settings.AutomaticRecording = value), Golden(EnumRow("成功时录到", () => settings.GoldenRecordingEnd, value => settings.GoldenRecordingEnd = value)), @@ -1696,6 +1700,10 @@ SettingRow Golden(SettingRow row) { Toggle("切面自动存档(保持视频连续)", () => settings.RecordingAutoSaveOnTransition, value => settings.RecordingAutoSaveOnTransition = value), EnumRow("BGM 拼接", () => settings.BgmMode, value => settings.BgmMode = value), + // Only the reconstruction mode has a music cursor that can either follow the + // source time or keep playing across cuts; game-mix BGM already follows the video. + BgmRebuildOnly(Toggle("BGM 跟随视频剪辑", () => settings.RecordingBgmFollowsVideo, + value => settings.RecordingBgmFollowsVideo = value)), Toggle("剪辑冻结帧", () => settings.RecordingRemoveFreezeFrames, value => settings.RecordingRemoveFreezeFrames = value, highlightWhenOn: true), Range("录制帧率", () => settings.RecordingFrameRate, value => settings.RecordingFrameRate = value, 30, 120, 30, value => $"{value} FPS"), diff --git a/Tests/Recorder/Stubs.cs b/Tests/Recorder/Stubs.cs index 923616d..359fd95 100644 --- a/Tests/Recorder/Stubs.cs +++ b/Tests/Recorder/Stubs.cs @@ -77,7 +77,7 @@ public class QolSettings { public AutoRecordingMode AutomaticRecording; public bool AutoRecorderEnabled => AutomaticRecording != AutoRecordingMode.Off; public bool DeathReplayEnabled, RecordingRemoveFreezeFrames, RecordingEditingEnabled = true, - RecordingKeepPausedFrames, RecordingKeepFailedAttempts; + RecordingKeepPausedFrames, RecordingKeepFailedAttempts, RecordingBgmFollowsVideo; public int DeathReplayBufferSeconds=30, RecordingRetentionCount, AutoRecordingRetentionCount, DeathReplayRetentionCount; public string RecordingDirectory=""; public GoldenRecordingEnd GoldenRecordingEnd; diff --git a/Tests/Recording.Policy/Program.cs b/Tests/Recording.Policy/Program.cs index 2e67d2e..52fa474 100644 --- a/Tests/Recording.Policy/Program.cs +++ b/Tests/Recording.Policy/Program.cs @@ -48,6 +48,25 @@ static void SetField(object owner, string name, object value) => owner.GetType() Call("BeginRun", level); Check(!State("reconstructBgm"), "explicit game-mix mode was ignored"); +// Forcing "BGM follows the video" makes every room read source time, so clips split by +// room/music cuts jump with the picture instead of letting the reconstructed BGM run on. +// The default (false) must keep the automatic per-room policy asserted above. +settings.BgmMode = BgmRecordingMode.SfxOnlyWithPostMix; +settings.RecordingBgmFollowsVideo = true; +session.Level = normal.Name; +Call("BeginRun", level); +Check(State("recordingRoomBgmFollowsVideo"), "forced BGM policy was not applied before the first room snapshot"); +foreach (var room in new[] { normal, sensitive, normal, triggered }) { + session.Level = room.Name; + Call("ObserveRoom", level); + Check(State("recordingRoomBgmFollowsVideo"), $"forced BGM policy ignored in room {room.Name}"); +} +settings.RecordingBgmFollowsVideo = false; +session.Level = normal.Name; +Call("BeginRun", level); +Call("ObserveRoom", level); +Check(!State("recordingRoomBgmFollowsVideo"), "clearing the forced BGM policy did not restore the per-room policy"); + // Exercise actual clip creation for BOTH sinks with a deterministic stopped clock. // No native handle, files, hooks, Steam process or game settings are touched. object Recording(string path) {