diff --git a/MuzikaGromche/Plugin.cs b/MuzikaGromche/Plugin.cs index 806f8ca..72be44b 100644 --- a/MuzikaGromche/Plugin.cs +++ b/MuzikaGromche/Plugin.cs @@ -76,6 +76,8 @@ namespace MuzikaGromche Bars = 8, LoopOffset = 32, BeatsOffset = 0.0f, + FadeOutBeat = -35, + FadeOutDuration = 3.3f, ColorTransitionIn = 0.25f, ColorTransitionOut = 0.25f, ColorTransitionEasing = Easing.OutExpo, @@ -479,6 +481,20 @@ namespace MuzikaGromche // Offset of beats, in seconds. Bigger offset => colors will change later. public float BeatsOffsetInSeconds => BeatsOffset / Beats * LoadedLoop.length; + public float _FadeOutBeat = float.NaN; + public float FadeOutBeat + { + get => Config.FadeOutBeatOverride ?? _FadeOutBeat; + set => _FadeOutBeat = value; + } + + public float _FadeOutDuration = 2f; + public float FadeOutDuration + { + get => Config.FadeOutDurationOverride ?? _FadeOutDuration; + set => _FadeOutDuration = value; + } + // Duration of color transition, measured in beats. public float _ColorTransitionIn = 0.25f; public float ColorTransitionIn @@ -504,7 +520,16 @@ namespace MuzikaGromche set => _ColorTransitionEasing = value; } - public float[] FlickerLightsTimeSeries = []; + public float[] _FlickerLightsTimeSeries = []; + public float[] FlickerLightsTimeSeries + { + get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries; + set + { + Array.Sort(value); + _FlickerLightsTimeSeries = value; + } + } public float[] LyricsTimeSeries { get; private set; } public string[] LyricsLines { get; private set; } @@ -846,7 +871,7 @@ namespace MuzikaGromche List events = []; { - var colorEvent = GetColorEvent(windUpOffsetTimestamp); + var colorEvent = GetColorEvent(loopOffsetSpan, windUpOffsetTimestamp); if (colorEvent != null) { events.Add(colorEvent); @@ -873,22 +898,46 @@ namespace MuzikaGromche return events; } - private SetLightsColorEvent GetColorEvent(BeatTimestamp windUpOffsetTimestamp) + private SetLightsColorEvent GetColorEvent(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) { - if (windUpOffsetTimestamp.Beat < -track.ColorTransitionIn) + if (FadeOut(loopOffsetSpan) is Color c1) { - // TODO: Maybe fade out? + return new SetLightsColorEvent(c1); } - else + + if (ColorFromPaletteAtTimestamp(windUpOffsetTimestamp) is Color c2) { - var color = ColorFromPaletteAtTimestamp(windUpOffsetTimestamp); - return new SetLightsColorEvent(color); + return new SetLightsColorEvent(c2); } + return null; } - public Color ColorFromPaletteAtTimestamp(BeatTimestamp timestamp) + private Color? FadeOut(BeatTimeSpan loopOffsetSpan) { + var beat = loopOffsetSpan.BeatToInclusive; + var fadeOutStart = track.FadeOutBeat; + var fadeOutEnd = fadeOutStart + track.FadeOutDuration; + + if (track.FadeOutBeat < beat && beat <= fadeOutEnd) + { + var x = (beat - track.FadeOutBeat) / track.FadeOutDuration; + var t = Mathf.Clamp(Easing.Linear.Eval(x), 0f, 1f); + return Color.Lerp(Color.white, Color.black, t); + } + else + { + return null; + } + } + + public Color? ColorFromPaletteAtTimestamp(BeatTimestamp timestamp) + { + if (timestamp.Beat <= -track.ColorTransitionIn) + { + return null; + } + // Imagine the timeline as a sequence of clips without gaps where each clip is a whole beat long. // Transition is when two adjacent clips need to be combined with some blend function(t) // where t is a factor in range 0..1 expressed as (time - Transition.Start) / Transition.Length; @@ -952,7 +1001,7 @@ namespace MuzikaGromche } else { - return Color.white; + return float.IsNaN(track.FadeOutBeat) ? Color.black : Color.white; } } } @@ -1117,6 +1166,9 @@ namespace MuzikaGromche public static Palette PaletteOverride { get; private set; } = null; + public static float? FadeOutBeatOverride { get; private set; } = null; + public static float? FadeOutDurationOverride { get; private set; } = null; + public static float[] FlickerLightsTimeSeriesOverride { get; private set; } = null; public static float? BeatsOffsetOverride { get; private set; } = null; public static float? ColorTransitionInOverride { get; private set; } = null; public static float? ColorTransitionOutOverride { get; private set; } = null; @@ -1319,6 +1371,9 @@ namespace MuzikaGromche // Declare and initialize early to avoid "Use of unassigned local variable" List<(Action Load, Action Apply)> entries = []; SyncedEntry overrideTimingsSyncedEntry = null; + SyncedEntry fadeOutBeatSyncedEntry = null; + SyncedEntry fadeOutDurationSyncedEntry = null; + SyncedEntry flickerLightsTimeSeriesSyncedEntry = null; SyncedEntry beatsOffsetSyncedEntry = null; SyncedEntry colorTransitionInSyncedEntry = null; SyncedEntry colorTransitionOutSyncedEntry = null; @@ -1336,6 +1391,12 @@ namespace MuzikaGromche overrideTimingsSyncedEntry.Changed += (sender, args) => apply(); overrideTimingsSyncedEntry.SyncHostToLocal(); + fadeOutBeatSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Beat", 0f, + new ConfigDescription("The beat at which to start fading out", new AcceptableValueRange(-1000f, 0))); + fadeOutDurationSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Duration", 0f, + new ConfigDescription("Duration of fading out", new AcceptableValueRange(0, 100))); + flickerLightsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Flicker Lights", "", + new ConfigDescription("Time series of beat offsets when to flicker the lights")); beatsOffsetSyncedEntry = configFile.BindSyncedEntry(section, "Beats Offset", 0f, new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange(-0.5f, 0.5f))); colorTransitionInSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition In", 0.25f, @@ -1346,11 +1407,17 @@ namespace MuzikaGromche new ConfigDescription("Interpolation/easing method to use for color transitions", new AcceptableValueList(Easing.AllNames))); var floatSliderOptions = Default(new FloatSliderOptions()); + LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutBeatSyncedEntry.Entry, floatSliderOptions)); + LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationSyncedEntry.Entry, floatSliderOptions)); + LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesSyncedEntry.Entry, Default(new TextInputFieldOptions()))); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new TextDropDownConfigItem(colorTransitionEasingSyncedEntry.Entry, Default(new TextDropDownOptions()))); + registerStruct(fadeOutBeatSyncedEntry, t => t._FadeOutBeat, x => FadeOutBeatOverride = x); + registerStruct(fadeOutDurationSyncedEntry, t => t._FadeOutDuration, x => FadeOutDurationOverride = x); + registerArray(flickerLightsTimeSeriesSyncedEntry, t => t._FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true); registerStruct(beatsOffsetSyncedEntry, t => t._BeatsOffset, x => BeatsOffsetOverride = x); registerStruct(colorTransitionInSyncedEntry, t => t._ColorTransitionIn, x => ColorTransitionInOverride = x); registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = x); @@ -1373,6 +1440,34 @@ namespace MuzikaGromche register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null)); void registerClass(SyncedEntry syncedEntry, Func getter, Action setter) where T : class => register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null)); + void registerArray(SyncedEntry syncedEntry, Func getter, Action setter, Func parser, bool sort = false) where T : struct => + register(syncedEntry, + (track) => string.Join(", ", getter(track)), + () => + { + var values = parseStringArray(syncedEntry.Value, parser, sort); + if (values != null) + { + // ensure the entry is sorted and formatted + syncedEntry.LocalValue = string.Join(", ", values); + } + setter.Invoke(overrideTimingsSyncedEntry.Value ? values : null); + }); + + T[] parseStringArray(string str, Func parser, bool sort = false) where T: struct + { + try + { + T[] xs = str.Replace(" ", "").Split(",").Select(parser).ToArray(); + Array.Sort(xs); + return xs; + } + catch (Exception e) + { + Debug.Log($"{nameof(MuzikaGromche)} Unable to parse array: {e}"); + return null; + } + } void load() {