1
0
Fork 0

Add support for fading out, and debug-only config for flickering lights

This commit is contained in:
ivan tkachenko 2025-07-19 18:46:41 +03:00
parent b8824dbbfb
commit 118eecbb59
1 changed files with 105 additions and 10 deletions

View File

@ -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<BaseEvent> 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<Track> Load, Action Apply)> entries = [];
SyncedEntry<bool> overrideTimingsSyncedEntry = null;
SyncedEntry<float> fadeOutBeatSyncedEntry = null;
SyncedEntry<float> fadeOutDurationSyncedEntry = null;
SyncedEntry<string> flickerLightsTimeSeriesSyncedEntry = null;
SyncedEntry<float> beatsOffsetSyncedEntry = null;
SyncedEntry<float> colorTransitionInSyncedEntry = null;
SyncedEntry<float> 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<float>(-1000f, 0)));
fadeOutDurationSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Duration", 0f,
new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(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<float>(-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<string>(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<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action<T> setter) where T : class =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null));
void registerArray<T>(SyncedEntry<string> syncedEntry, Func<Track, T[]> getter, Action<T[]> setter, Func<string, T> 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<T>(string str, Func<string, T> 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()
{