forked from nikita/muzika-gromche
Reworked state management system, automatic wrapping of timestamps and spans
Add lyrics for MoyaZhittya
This commit is contained in:
parent
d13c617895
commit
601ecf8887
|
@ -74,11 +74,43 @@ namespace MuzikaGromche
|
|||
Language = Language.ENGLISH,
|
||||
WindUpTimer = 34.53f,
|
||||
Bars = 8,
|
||||
LoopOffset = 32,
|
||||
BeatsOffset = 0.0f,
|
||||
ColorTransitionIn = 0.25f,
|
||||
ColorTransitionOut = 0.25f,
|
||||
ColorTransitionEasing = Easing.OutExpo,
|
||||
Palette = Palette.Parse(["#A3A3A3", "#BE3D39", "#5CBC69", "#BE3D39", "#BABC5C", "#BE3D39", "#5C96BC", "#BE3D39"]),
|
||||
FlickerLightsTimeSeries = [-100.5f, -99.5f, -92.5f, -91.5f, -76.5f, -75.5f, -60.5f, -59.5f, -37f, -36f, -4.5f, -3.5f, 27.5f, 28.5f],
|
||||
Lyrics = [
|
||||
(-84, "This ain't a song for the broken-hearted"),
|
||||
(-68, "No silent prayer for the faith-departed"),
|
||||
(-52, "I ain't gonna be"),
|
||||
(-48, "I ain't gonna be\njust a face in the crowd"),
|
||||
(-45, "YOU'RE"),
|
||||
(-44, "you're GONNA"),
|
||||
(-43.5f, "you're gonna HEAR"),
|
||||
(-43, "you're gonna hear\nMY"),
|
||||
(-42, "you're gonna hear\nmy VOICE"),
|
||||
(-41, "WHEN I"),
|
||||
(-40, "When I SHOUT IT"),
|
||||
(-39, "When I shout it\nOUT LOUD"),
|
||||
(-34, "IT'S MY"),
|
||||
(-32, "IT'S MY\nLIIIIIFE"),
|
||||
(-28, "And it's now or never"),
|
||||
(-22, "I ain't gonna"),
|
||||
(-20, "I ain't gonna\nlive forever"),
|
||||
(-14, "I just want to live"),
|
||||
(-10, "I just want to live\nwhile I'm alive"),
|
||||
( -2, "IT'S MY"),
|
||||
( 0, "IT'S MY\nLIIIIIFE"),
|
||||
( 2, "My heart is like"),
|
||||
( 4, "My heart is like\nan open highway"),
|
||||
( 10, "Like Frankie said,"),
|
||||
( 12, "Like Frankie said,\n\"I did it my way\""),
|
||||
( 18, "I just want to live"),
|
||||
( 22, "I just want to live\nwhile I'm alive"),
|
||||
( 30, "IT'S MY"),
|
||||
],
|
||||
},
|
||||
new Track
|
||||
{
|
||||
|
@ -254,6 +286,7 @@ namespace MuzikaGromche
|
|||
}
|
||||
|
||||
public static Track CurrentTrack;
|
||||
public static BeatTimeState BeatTimeState;
|
||||
|
||||
public static void SetLightColor(Color color)
|
||||
{
|
||||
|
@ -268,6 +301,17 @@ namespace MuzikaGromche
|
|||
SetLightColor(Color.white);
|
||||
}
|
||||
|
||||
public static bool LocalPlayerCanHearMusic(EnemyAI jester)
|
||||
{
|
||||
var player = GameNetworkManager.Instance.localPlayerController;
|
||||
if (player == null || !player.isInsideFactory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var distance = Vector3.Distance(player.transform.position, jester.transform.position);
|
||||
return distance < jester.creatureVoice.maxDistance;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
string text = Info.Location.TrimEnd((PluginInfo.PLUGIN_NAME + ".dll").ToCharArray());
|
||||
|
@ -394,6 +438,10 @@ namespace MuzikaGromche
|
|||
// How many beats the loop segment has. The default strategy is to switch color of lights on each beat.
|
||||
public int Beats;
|
||||
|
||||
// Number of beats between WindUpTimer and where looped segment starts (not the loop audio).
|
||||
public int LoopOffset = 0;
|
||||
public float LoopOffsetInSeconds => LoopOffset / Beats * LoadedLoop.length;
|
||||
|
||||
// Shorthand for four beats
|
||||
public int Bars
|
||||
{
|
||||
|
@ -407,9 +455,6 @@ namespace MuzikaGromche
|
|||
public AudioClip LoadedStart;
|
||||
public AudioClip LoadedLoop;
|
||||
|
||||
// This does not account for the timestamp when Jester has actually popped
|
||||
public float FixedLoopDelay => LoadedStart.length - WindUpTimer;
|
||||
|
||||
// How often this track should be chosen, relative to the sum of weights of all tracks.
|
||||
public SyncedEntry<int> Weight;
|
||||
|
||||
|
@ -432,7 +477,7 @@ namespace MuzikaGromche
|
|||
}
|
||||
|
||||
// Offset of beats, in seconds. Bigger offset => colors will change later.
|
||||
public float Offset => BeatsOffset / Beats * LoadedLoop.length;
|
||||
public float BeatsOffsetInSeconds => BeatsOffset / Beats * LoadedLoop.length;
|
||||
|
||||
// Duration of color transition, measured in beats.
|
||||
public float _ColorTransitionIn = 0.25f;
|
||||
|
@ -459,7 +504,304 @@ namespace MuzikaGromche
|
|||
set => _ColorTransitionEasing = value;
|
||||
}
|
||||
|
||||
public float CalculateBeat(AudioSource start, AudioSource loop)
|
||||
public float[] FlickerLightsTimeSeries = [];
|
||||
|
||||
public float[] LyricsTimeSeries { get; private set; }
|
||||
public string[] LyricsLines { get; private set; }
|
||||
public (float, string)[] Lyrics
|
||||
{
|
||||
set
|
||||
{
|
||||
var dict = new SortedDictionary<float, string>();
|
||||
foreach (var (beat, text) in value)
|
||||
{
|
||||
dict.Add(beat, text);
|
||||
}
|
||||
LyricsTimeSeries = [.. dict.Keys];
|
||||
LyricsLines = [.. dict.Values];
|
||||
}
|
||||
}
|
||||
|
||||
public Palette _Palette = Palette.DEFAULT;
|
||||
public Palette Palette
|
||||
{
|
||||
get => Config.PaletteOverride ?? _Palette;
|
||||
set => _Palette = value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct BeatTimestamp
|
||||
{
|
||||
// Number of beats in the loop audio segment.
|
||||
public readonly int LoopBeats;
|
||||
public readonly float HalfLoopBeats => LoopBeats / 2f;
|
||||
|
||||
// Whether negative time should wrap around. Positive time past LoopBeats always wraps around.
|
||||
public readonly bool IsLooping;
|
||||
|
||||
// Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative.
|
||||
public readonly float Beat;
|
||||
|
||||
public BeatTimestamp(int loopBeats, bool isLooping, float beat)
|
||||
{
|
||||
LoopBeats = loopBeats;
|
||||
IsLooping = isLooping || beat >= HalfLoopBeats;
|
||||
Beat = isLooping || beat >= LoopBeats ? Mod.Positive(beat, LoopBeats) : beat;
|
||||
}
|
||||
|
||||
public static BeatTimestamp operator +(BeatTimestamp self, float delta)
|
||||
{
|
||||
if (delta < -self.HalfLoopBeats && self.Beat > self.HalfLoopBeats /* implied: */ && self.IsLooping)
|
||||
{
|
||||
// Warning: you can't meaningfully subtract more than half of the loop
|
||||
// from a looping timestamp whose Beat is past half of the loop,
|
||||
// because the resulting IsLooping is unknown.
|
||||
// Shouldn't be needed though, as deltas are usually short enough.
|
||||
// But don't try to chain many short negative deltas!
|
||||
}
|
||||
return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta);
|
||||
}
|
||||
|
||||
public static BeatTimestamp operator -(BeatTimestamp self, float delta)
|
||||
{
|
||||
return self + -delta;
|
||||
}
|
||||
|
||||
public readonly BeatTimestamp Floor()
|
||||
{
|
||||
// There is no way it wraps or affects IsLooping state
|
||||
var beat = Mathf.Floor(Beat);
|
||||
return new BeatTimestamp(LoopBeats, IsLooping, beat);
|
||||
}
|
||||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')} {Beat:N4}/{LoopBeats})";
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct BeatTimeSpan
|
||||
{
|
||||
public readonly int LoopBeats;
|
||||
public readonly float HalfLoopBeats => LoopBeats / 2f;
|
||||
public readonly bool IsLooping;
|
||||
// Open lower bound
|
||||
public readonly float BeatFromExclusive;
|
||||
// Closed upper bound
|
||||
public readonly float BeatToInclusive;
|
||||
|
||||
public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive)
|
||||
{
|
||||
LoopBeats = loopBeats;
|
||||
IsLooping = isLooping || beatToInclusive >= HalfLoopBeats;
|
||||
BeatFromExclusive = wrap(beatFromExclusive);
|
||||
BeatToInclusive = wrap(beatToInclusive);
|
||||
|
||||
float wrap(float beat)
|
||||
{
|
||||
return isLooping || beat >= loopBeats ? Mod.Positive(beat, loopBeats) : beat;
|
||||
}
|
||||
}
|
||||
|
||||
public static BeatTimeSpan Between(BeatTimestamp timestampFromExclusive, BeatTimestamp timestampToInclusive)
|
||||
{
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat);
|
||||
}
|
||||
|
||||
|
||||
public static BeatTimeSpan Between(float beatFromExclusive, BeatTimestamp timestampToInclusive)
|
||||
{
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat);
|
||||
}
|
||||
|
||||
public static BeatTimeSpan Empty = new();
|
||||
|
||||
public readonly BeatTimestamp ToTimestamp()
|
||||
{
|
||||
return new(LoopBeats, IsLooping, BeatToInclusive);
|
||||
}
|
||||
|
||||
// The beat will not be wrapped.
|
||||
public readonly bool ContainsExact(float beat)
|
||||
{
|
||||
return BeatFromExclusive < beat && beat <= BeatToInclusive;
|
||||
}
|
||||
|
||||
public readonly int? GetLastIndex(float[] timeSeries)
|
||||
{
|
||||
if (IsEmpty() || timeSeries == null || timeSeries.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsWrapped())
|
||||
{
|
||||
// Split the search in two non-wrapping searches:
|
||||
// before wrapping (happens earlier) and after wrapping (happens later).
|
||||
|
||||
// Check the "happens later" part first.
|
||||
var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive);
|
||||
var laterIndex = laterSpan.GetLastIndex(timeSeries);
|
||||
if (laterIndex != null)
|
||||
{
|
||||
return laterIndex;
|
||||
}
|
||||
|
||||
// The "happens earlier" part is easy: it's just the last value in the series.
|
||||
var lastIndex = timeSeries.Length - 1;
|
||||
if (timeSeries[lastIndex] > BeatFromExclusive)
|
||||
{
|
||||
return lastIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// BeatFromExclusive might as well be -Infinity
|
||||
|
||||
var index = Array.BinarySearch(timeSeries, BeatToInclusive);
|
||||
if (index > 0 && index < timeSeries.Length && timeSeries[index] > BeatFromExclusive)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore from bitwise complement
|
||||
index = ~index;
|
||||
// index points to the next larger object, i.e. the next event in the series after the BeatToInclusive.
|
||||
// Make it point to one event before that.
|
||||
index -= 1;
|
||||
if (index >= 0 && timeSeries[index] > BeatFromExclusive && timeSeries[index] <= BeatToInclusive)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public readonly float Duration()
|
||||
{
|
||||
return Split().Sum(span => span.BeatToInclusive - span.BeatFromExclusive);
|
||||
}
|
||||
|
||||
public readonly BeatTimeSpan[] Split()
|
||||
{
|
||||
if (IsEmpty())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
else if (IsWrapped())
|
||||
{
|
||||
return [
|
||||
new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon */ -0.001f, beatToInclusive: BeatToInclusive),
|
||||
new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: BeatFromExclusive, beatToInclusive: LoopBeats),
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [this];
|
||||
}
|
||||
}
|
||||
|
||||
public readonly bool IsEmpty()
|
||||
{
|
||||
if (IsLooping)
|
||||
{
|
||||
var to = BeatToInclusive;
|
||||
|
||||
// unwrap if needed
|
||||
if (BeatToInclusive < BeatFromExclusive)
|
||||
{
|
||||
to = BeatToInclusive + LoopBeats;
|
||||
}
|
||||
|
||||
// Due to audio offset changes, `to` may shift before `from`, so unwrapping it would result in a very large span
|
||||
return to - BeatFromExclusive > HalfLoopBeats;
|
||||
}
|
||||
else
|
||||
{
|
||||
// straightforward requirement that time is monotonic
|
||||
return BeatFromExclusive > BeatToInclusive;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly bool IsWrapped()
|
||||
{
|
||||
return IsLooping && !IsEmpty() && BeatToInclusive < BeatFromExclusive;
|
||||
}
|
||||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
|
||||
}
|
||||
}
|
||||
|
||||
public class BeatTimeState
|
||||
{
|
||||
// The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet.
|
||||
private bool hasStarted = false;
|
||||
|
||||
// The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+Loop/2.
|
||||
private bool windUpOffsetIsLooping = false;
|
||||
|
||||
// The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+LoopOffset+Loop/2.
|
||||
private bool loopOffsetIsLooping = false;
|
||||
|
||||
private readonly Track track;
|
||||
|
||||
private float loopOffsetBeat = float.NegativeInfinity;
|
||||
|
||||
public BeatTimeState(Track track)
|
||||
{
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
public List<BaseEvent> Update(AudioSource start, AudioSource loop)
|
||||
{
|
||||
hasStarted |= start.time != 0;
|
||||
if (hasStarted)
|
||||
{
|
||||
var loopTimestamp = UpdateStateForLoopOffset(start, loop);
|
||||
var loopOffsetSpan = BeatTimeSpan.Between(loopOffsetBeat, loopTimestamp);
|
||||
|
||||
// Do not go back in time
|
||||
if (!loopOffsetSpan.IsEmpty())
|
||||
{
|
||||
var windUpOffsetTimestamp = UpdateStateForWindUpOffset(start, loop);
|
||||
loopOffsetBeat = loopTimestamp.Beat;
|
||||
var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp);
|
||||
Debug.Log($"MuzikaGromche looping(loop)={loopOffsetIsLooping} looping(windUp)={windUpOffsetIsLooping} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} events={string.Join(",", events)}");
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Events other than colors start rotating at 0=WindUpTimer+LoopOffset.
|
||||
private BeatTimestamp UpdateStateForLoopOffset(AudioSource start, AudioSource loop)
|
||||
{
|
||||
var offset = BaseOffset() + track.LoopOffsetInSeconds;
|
||||
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, loopOffsetIsLooping);
|
||||
loopOffsetIsLooping |= timestamp.IsLooping;
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
// Colors start rotating at 0=WindUpTimer
|
||||
private BeatTimestamp UpdateStateForWindUpOffset(AudioSource start, AudioSource loop)
|
||||
{
|
||||
var offset = BaseOffset();
|
||||
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, windUpOffsetIsLooping);
|
||||
windUpOffsetIsLooping |= timestamp.IsLooping;
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
private float BaseOffset()
|
||||
{
|
||||
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds + track.WindUpTimer;
|
||||
}
|
||||
|
||||
BeatTimestamp GetTimestampRelativeToGivenOffset(AudioSource start, AudioSource loop, float offset, bool isLooping)
|
||||
{
|
||||
// If popped, calculate which beat the music is currently at.
|
||||
// In order to do that we should choose one of two strategies:
|
||||
|
@ -472,38 +814,80 @@ namespace MuzikaGromche
|
|||
// NOTE 2: There is a weird state when Jester has popped and chases a player:
|
||||
// Start/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that.
|
||||
|
||||
float elapsed = 0f;
|
||||
elapsed -= Config.AudioOffset.Value;
|
||||
elapsed -= Offset;
|
||||
|
||||
elapsed += start.isPlaying && start.time != 0f
|
||||
var timeFromTheVeryStart = start.isPlaying && start.time != 0f
|
||||
// [1] Start source is still playing
|
||||
? start.time - WindUpTimer
|
||||
? start.time
|
||||
// [2] Start source has finished
|
||||
: loop.time + FixedLoopDelay;
|
||||
: track.LoadedStart.length + loop.time;
|
||||
|
||||
var normalized = Mod.Positive(elapsed / LoadedLoop.length, 1f);
|
||||
float adjustedTimeFromOffset = timeFromTheVeryStart - offset;
|
||||
|
||||
var beat = normalized * (float)Beats;
|
||||
#if DEBUG
|
||||
var color = ColorAtBeat(beat);
|
||||
var adjustedTimeNormalized = adjustedTimeFromOffset / track.LoadedLoop.length;
|
||||
|
||||
var beat = adjustedTimeNormalized * track.Beats;
|
||||
|
||||
// Let it infer the isLooping flag from the beat
|
||||
var timestamp = new BeatTimestamp(track.Beats, isLooping, beat);
|
||||
|
||||
#if DEBUG && false
|
||||
var color = ColorFromPaletteAtTimestamp(timestamp);
|
||||
Debug.LogFormat("MuzikaGromche t={0,10:N4} d={1,7:N4} Start[{2}{3,8:N4} ==0f? {4}] Loop[{5}{6,8:N4}] norm={7,6:N4} beat={8,7:N4} color={9}",
|
||||
Time.realtimeSinceStartup, Time.deltaTime,
|
||||
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
||||
(loop.isPlaying ? '+' : ' '), loop.time,
|
||||
normalized, beat, color);
|
||||
Time.realtimeSinceStartup, Time.deltaTime,
|
||||
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
||||
(loop.isPlaying ? '+' : ' '), loop.time,
|
||||
adjustedTimeNormalized, beat, color);
|
||||
#endif
|
||||
return beat;
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Palette _Palette = Palette.DEFAULT;
|
||||
public Palette Palette
|
||||
private List<BaseEvent> GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp)
|
||||
{
|
||||
get => Config.PaletteOverride ?? _Palette;
|
||||
set => _Palette = value;
|
||||
List<BaseEvent> events = [];
|
||||
|
||||
{
|
||||
var colorEvent = GetColorEvent(windUpOffsetTimestamp);
|
||||
if (colorEvent != null)
|
||||
{
|
||||
events.Add(colorEvent);
|
||||
}
|
||||
}
|
||||
|
||||
if (loopOffsetSpan.GetLastIndex(track.FlickerLightsTimeSeries) != null)
|
||||
{
|
||||
events.Add(new FlickerLightsEvent());
|
||||
}
|
||||
|
||||
// TODO: quick editor
|
||||
// loopOffsetSpan.GetLastIndex(Config.LyricsTimeSeries)
|
||||
if (Config.DisplayLyrics.Value)
|
||||
{
|
||||
var index = loopOffsetSpan.GetLastIndex(track.LyricsTimeSeries);
|
||||
if (index != null)
|
||||
{
|
||||
var text = track.LyricsLines[(int)index];
|
||||
events.Add(new LyricsEvent(text));
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
public Color ColorAtBeat(float beat)
|
||||
private SetLightsColorEvent GetColorEvent(BeatTimestamp windUpOffsetTimestamp)
|
||||
{
|
||||
if (windUpOffsetTimestamp.Beat < -track.ColorTransitionIn)
|
||||
{
|
||||
// TODO: Maybe fade out?
|
||||
}
|
||||
else
|
||||
{
|
||||
var color = ColorFromPaletteAtTimestamp(windUpOffsetTimestamp);
|
||||
return new SetLightsColorEvent(color);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Color ColorFromPaletteAtTimestamp(BeatTimestamp timestamp)
|
||||
{
|
||||
// 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)
|
||||
|
@ -522,35 +906,36 @@ namespace MuzikaGromche
|
|||
//
|
||||
// Otherwise there is no transition running at this time.
|
||||
const float currentClipLength = 1f;
|
||||
var currentClipStart = Mathf.Floor(beat);
|
||||
// var currentClipSpan = BeatTimespan timestamp.Floor()
|
||||
var currentClipStart = timestamp.Floor();
|
||||
var currentClipEnd = currentClipStart + currentClipLength;
|
||||
|
||||
float transitionLength = ColorTransitionIn + ColorTransitionOut;
|
||||
float transitionLength = track.ColorTransitionIn + track.ColorTransitionOut;
|
||||
|
||||
if (Config.EnableColorAnimations.Value)
|
||||
{
|
||||
if (transitionLength > /* epsilon */ 0.01)
|
||||
{
|
||||
if (beat - currentClipStart < ColorTransitionOut)
|
||||
if (BeatTimeSpan.Between(currentClipStart, timestamp).Duration() < track.ColorTransitionOut)
|
||||
{
|
||||
return ColorTransition(currentClipStart);
|
||||
}
|
||||
else if (currentClipEnd - beat < ColorTransitionIn)
|
||||
else if (BeatTimeSpan.Between(timestamp, currentClipEnd).Duration() < track.ColorTransitionIn)
|
||||
{
|
||||
return ColorTransition(currentClipEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
// default
|
||||
return ColorAtWholeBeat(beat);
|
||||
return ColorAtWholeBeat(timestamp);
|
||||
|
||||
Color ColorTransition(float clipsBoundary)
|
||||
Color ColorTransition(BeatTimestamp clipsBoundary)
|
||||
{
|
||||
var transitionStart = clipsBoundary - ColorTransitionIn;
|
||||
var transitionEnd = clipsBoundary + ColorTransitionOut;
|
||||
var x = (beat - transitionStart) / transitionLength;
|
||||
var t = Mathf.Clamp(ColorTransitionEasing.Eval(x), 0f, 1f);
|
||||
if (ColorTransitionIn == 0.0f)
|
||||
var transitionStart = clipsBoundary - track.ColorTransitionIn;
|
||||
var transitionEnd = clipsBoundary + track.ColorTransitionOut;
|
||||
var x = BeatTimeSpan.Between(transitionStart, timestamp).Duration() / transitionLength;
|
||||
var t = Mathf.Clamp(track.ColorTransitionEasing.Eval(x), 0f, 1f);
|
||||
if (track.ColorTransitionIn == 0.0f)
|
||||
{
|
||||
// Subtract an epsilon, so we don't use the same beat twice
|
||||
transitionStart -= 0.01f;
|
||||
|
@ -558,14 +943,46 @@ namespace MuzikaGromche
|
|||
return Color.Lerp(ColorAtWholeBeat(transitionStart), ColorAtWholeBeat(transitionEnd), t);
|
||||
}
|
||||
|
||||
Color ColorAtWholeBeat(float beat)
|
||||
Color ColorAtWholeBeat(BeatTimestamp timestamp)
|
||||
{
|
||||
int beatIndex = Mod.Positive(Mathf.FloorToInt(beat), Beats);
|
||||
return Mod.Index(Palette.Colors, beatIndex);
|
||||
if (timestamp.Beat >= 0f)
|
||||
{
|
||||
int wholeBeat = Mathf.FloorToInt(timestamp.Beat);
|
||||
return Mod.Index(track.Palette.Colors, wholeBeat);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class BaseEvent;
|
||||
|
||||
public class SetLightsColorEvent(Color color) : BaseEvent
|
||||
{
|
||||
public readonly Color Color = color;
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Color(#{ColorUtility.ToHtmlStringRGB(Color)})";
|
||||
}
|
||||
}
|
||||
|
||||
public class FlickerLightsEvent : BaseEvent
|
||||
{
|
||||
public override string ToString() => "Flicker";
|
||||
}
|
||||
|
||||
public class LyricsEvent(string text) : BaseEvent
|
||||
{
|
||||
public readonly string Text = text;
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Lyrics({Text.Replace("\n", "\\n")})";
|
||||
}
|
||||
}
|
||||
|
||||
// Default C#/.NET remainder operator % returns negative result for negative input
|
||||
// which is unsuitable as an index for an array.
|
||||
public static class Mod
|
||||
|
@ -692,6 +1109,8 @@ namespace MuzikaGromche
|
|||
{
|
||||
public static ConfigEntry<bool> EnableColorAnimations { get; private set; }
|
||||
|
||||
public static ConfigEntry<bool> DisplayLyrics { get; private set; }
|
||||
|
||||
public static ConfigEntry<float> AudioOffset { get; private set; }
|
||||
|
||||
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
||||
|
@ -709,6 +1128,10 @@ namespace MuzikaGromche
|
|||
new ConfigDescription("Smooth light color transitions are known to cause performance issues on some setups.\n\nTurn them off if you experience lag spikes."));
|
||||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(EnableColorAnimations, requiresRestart: false));
|
||||
|
||||
DisplayLyrics = configFile.Bind("General", "Display Lyrics", true,
|
||||
new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music."));
|
||||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(DisplayLyrics, requiresRestart: false));
|
||||
|
||||
AudioOffset = configFile.Bind("General", "Audio Offset", 0f, new ConfigDescription(
|
||||
"Adjust audio offset (in seconds).\n\nIf you are playing with Bluetooth headphones and experiencing a visual desync, try setting this to about negative 0.2.\n\nIf your video output has high latency (like a long HDMI cable etc.), try positive values instead.",
|
||||
new AcceptableValueRange<float>(-0.5f, 0.5f)));
|
||||
|
@ -1059,6 +1482,7 @@ namespace MuzikaGromche
|
|||
|
||||
// ...and start modded music
|
||||
Plugin.CurrentTrack = Plugin.ChooseTrack();
|
||||
Plugin.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack);
|
||||
// Set up custom popup timer, which is shorter than Start audio
|
||||
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
|
||||
|
||||
|
@ -1106,11 +1530,32 @@ namespace MuzikaGromche
|
|||
}
|
||||
|
||||
// Manage the timeline: switch color of the lights according to the current playback/beat position.
|
||||
if (__instance.previousState == 2)
|
||||
if (__instance.previousState == 1 || __instance.previousState == 2)
|
||||
{
|
||||
var beat = Plugin.CurrentTrack.CalculateBeat(start: __instance.farAudio, loop: __instance.creatureVoice);
|
||||
var color = Plugin.CurrentTrack.ColorAtBeat(beat);
|
||||
Plugin.SetLightColor(color);
|
||||
var events = Plugin.BeatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
|
||||
foreach (var ev in events)
|
||||
{
|
||||
switch (ev)
|
||||
{
|
||||
case SetLightsColorEvent e:
|
||||
Plugin.SetLightColor(e.Color);
|
||||
break;
|
||||
|
||||
case FlickerLightsEvent:
|
||||
RoundManager.Instance.FlickerLights(true);
|
||||
RoundManager.Instance.FlickerPoweredLights(true);
|
||||
break;
|
||||
|
||||
case LyricsEvent e:
|
||||
if (Plugin.LocalPlayerCanHearMusic(__instance))
|
||||
{
|
||||
HUDManager.Instance.DisplayTip("[Lyrics]", e.Text);
|
||||
// Don't interrupt the music with constant HUD audio pings
|
||||
HUDManager.Instance.UIAudio.Stop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue