forked from nikita/muzika-gromche
Extrapolate AudioSource playback time to get smoother transitions
AudioSource only updates about 25 times per second, meaning that even at 30 fps some adjacent frames would be calculated as having exact same timestamps and render duplicated colors. At 100+ fps more than 2/3 of the frames would be duplicates. As a drive-by change, split complex logic of BeatTimeState into smaller classes. Most of the time the state needs to maintain some boolean flag which it flips once and stays that way, like HasStarted, IsLooping.
This commit is contained in:
parent
3d0795f04d
commit
69e64397a0
|
@ -0,0 +1,5 @@
|
|||
[*.cs]
|
||||
|
||||
# IDE0290: Use primary constructor
|
||||
# Primary constructors are far from perfect: they can't have readonly fields, while fields can be used anywhere in the class body.
|
||||
csharp_style_prefer_primary_constructors = false
|
|
@ -3,6 +3,7 @@
|
|||
## MuzikaGromche 1337.420.69
|
||||
|
||||
- Fix harmless but annoying errors in BepInEx console output.
|
||||
- Improve smoothness of color animations.
|
||||
|
||||
## MuzikaGromche 1337.69.420 - It's All Connected Edition
|
||||
|
||||
|
|
|
@ -830,11 +830,15 @@ namespace MuzikaGromche
|
|||
// 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)
|
||||
// Additional metadata describing whether this timestamp is based on extrapolated source data.
|
||||
public readonly bool IsExtrapolated;
|
||||
|
||||
public BeatTimestamp(int loopBeats, bool isLooping, float beat, bool isExtrapolated)
|
||||
{
|
||||
LoopBeats = loopBeats;
|
||||
IsLooping = isLooping || beat >= HalfLoopBeats;
|
||||
Beat = isLooping || beat >= LoopBeats ? Mod.Positive(beat, LoopBeats) : beat;
|
||||
IsExtrapolated = isExtrapolated;
|
||||
}
|
||||
|
||||
public static BeatTimestamp operator +(BeatTimestamp self, float delta)
|
||||
|
@ -847,7 +851,7 @@ namespace MuzikaGromche
|
|||
// 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);
|
||||
return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta, self.IsExtrapolated);
|
||||
}
|
||||
|
||||
public static BeatTimestamp operator -(BeatTimestamp self, float delta)
|
||||
|
@ -859,12 +863,12 @@ namespace MuzikaGromche
|
|||
{
|
||||
// There is no way it wraps or affects IsLooping state
|
||||
var beat = Mathf.Floor(Beat);
|
||||
return new BeatTimestamp(LoopBeats, IsLooping, beat);
|
||||
return new BeatTimestamp(LoopBeats, IsLooping, beat, IsExtrapolated);
|
||||
}
|
||||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')} {Beat:N4}/{LoopBeats})";
|
||||
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')} {Beat:N4}/{LoopBeats})";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -877,13 +881,16 @@ namespace MuzikaGromche
|
|||
public readonly float BeatFromExclusive;
|
||||
// Closed upper bound
|
||||
public readonly float BeatToInclusive;
|
||||
// Additional metadata describing whether this timestamp is based on extrapolated source data.
|
||||
public readonly bool IsExtrapolated;
|
||||
|
||||
public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive)
|
||||
public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive, bool isExtrapolated)
|
||||
{
|
||||
LoopBeats = loopBeats;
|
||||
IsLooping = isLooping || beatToInclusive >= HalfLoopBeats;
|
||||
BeatFromExclusive = wrap(beatFromExclusive);
|
||||
BeatToInclusive = wrap(beatToInclusive);
|
||||
IsExtrapolated = isExtrapolated;
|
||||
|
||||
float wrap(float beat)
|
||||
{
|
||||
|
@ -893,20 +900,20 @@ namespace MuzikaGromche
|
|||
|
||||
public static BeatTimeSpan Between(BeatTimestamp timestampFromExclusive, BeatTimestamp timestampToInclusive)
|
||||
{
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat);
|
||||
var isExtrapolated = timestampFromExclusive.IsExtrapolated || timestampToInclusive.IsExtrapolated;
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat, isExtrapolated);
|
||||
}
|
||||
|
||||
|
||||
public static BeatTimeSpan Between(float beatFromExclusive, BeatTimestamp timestampToInclusive)
|
||||
{
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat);
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat, timestampToInclusive.IsExtrapolated);
|
||||
}
|
||||
|
||||
public static BeatTimeSpan Empty = new();
|
||||
|
||||
public readonly BeatTimestamp ToTimestamp()
|
||||
{
|
||||
return new(LoopBeats, IsLooping, BeatToInclusive);
|
||||
return new(LoopBeats, IsLooping, BeatToInclusive, IsExtrapolated);
|
||||
}
|
||||
|
||||
// The beat will not be wrapped.
|
||||
|
@ -928,7 +935,7 @@ namespace MuzikaGromche
|
|||
// 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 laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive, IsExtrapolated);
|
||||
var laterIndex = laterSpan.GetLastIndex(timeSeries);
|
||||
if (laterIndex != null)
|
||||
{
|
||||
|
@ -1014,94 +1021,144 @@ namespace MuzikaGromche
|
|||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
|
||||
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
|
||||
}
|
||||
}
|
||||
|
||||
class BeatTimeState
|
||||
class ExtrapolatedAudioSourceState
|
||||
{
|
||||
// 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;
|
||||
// AudioSource.isPlaying
|
||||
public bool IsPlaying { get; private set; }
|
||||
|
||||
// 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;
|
||||
// AudioSource.time, possibly extrapolated
|
||||
public float Time => ExtrapolatedTime;
|
||||
|
||||
// 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;
|
||||
// The object is newly created, the AudioSource began to play (possibly delayed) but its time hasn't advanced from 0.0f yet.
|
||||
// Time can not be extrapolated when HasStarted is false.
|
||||
public bool HasStarted { get; private set; } = false;
|
||||
|
||||
private bool windUpZeroBeatEventTriggered = false;
|
||||
public bool IsExtrapolated => LastKnownNonExtrapolatedTime != ExtrapolatedTime;
|
||||
|
||||
private readonly Track track;
|
||||
private float ExtrapolatedTime = 0f;
|
||||
|
||||
private float loopOffsetBeat = float.NegativeInfinity;
|
||||
private float LastKnownNonExtrapolatedTime = 0f;
|
||||
|
||||
private static System.Random lyricsRandom = null!;
|
||||
// Any wall clock based measurements of when this state was recorded
|
||||
private float LastKnownRealtime = 0f;
|
||||
|
||||
private int lyricsRandomPerLoop;
|
||||
private const float MaxExtrapolationInterval = 0.5f;
|
||||
|
||||
public BeatTimeState(Track track)
|
||||
public void Update(AudioSource audioSource, float realtime)
|
||||
{
|
||||
if (lyricsRandom == null)
|
||||
IsPlaying = audioSource.isPlaying;
|
||||
HasStarted |= audioSource.time != 0f;
|
||||
|
||||
if (LastKnownNonExtrapolatedTime != audioSource.time)
|
||||
{
|
||||
lyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337);
|
||||
lyricsRandomPerLoop = lyricsRandom.Next();
|
||||
LastKnownRealtime = realtime;
|
||||
LastKnownNonExtrapolatedTime = ExtrapolatedTime = audioSource.time;
|
||||
}
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
public List<BaseEvent> Update(AudioSource start, AudioSource loop)
|
||||
// Frames are rendering faster than AudioSource updates its playback time state
|
||||
else if (IsPlaying && HasStarted && Config.ExtrapolateTime)
|
||||
{
|
||||
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())
|
||||
{
|
||||
if (loopOffsetSpan.BeatFromExclusive > loopOffsetSpan.BeatToInclusive)
|
||||
{
|
||||
lyricsRandomPerLoop = lyricsRandom.Next();
|
||||
}
|
||||
|
||||
var windUpOffsetTimestamp = UpdateStateForWindUpOffset(start, loop);
|
||||
loopOffsetBeat = loopTimestamp.Beat;
|
||||
var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp);
|
||||
#if DEBUG
|
||||
Debug.Log($"{nameof(MuzikaGromche)} looping? {(loopOffsetIsLooping ? 'X' : '_')}{(windUpOffsetIsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} events={string.Join(",", events)}");
|
||||
Debug.Assert(LastKnownNonExtrapolatedTime == audioSource.time); // implied
|
||||
#endif
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Events other than colors start rotating at 0=WindUpTimer+LoopOffset.
|
||||
private BeatTimestamp UpdateStateForLoopOffset(AudioSource start, AudioSource loop)
|
||||
var deltaTime = realtime - LastKnownRealtime;
|
||||
if (0 < deltaTime && deltaTime < MaxExtrapolationInterval)
|
||||
{
|
||||
var offset = BaseOffset() + track.LoopOffsetInSeconds;
|
||||
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, loopOffsetIsLooping);
|
||||
loopOffsetIsLooping |= timestamp.IsLooping;
|
||||
return timestamp;
|
||||
ExtrapolatedTime = LastKnownNonExtrapolatedTime + deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Colors start rotating at 0=WindUpTimer
|
||||
private BeatTimestamp UpdateStateForWindUpOffset(AudioSource start, AudioSource loop)
|
||||
public void Finish()
|
||||
{
|
||||
var offset = BaseOffset();
|
||||
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, windUpOffsetIsLooping);
|
||||
windUpOffsetIsLooping |= timestamp.IsLooping;
|
||||
return timestamp;
|
||||
IsPlaying = false;
|
||||
}
|
||||
|
||||
private float BaseOffset()
|
||||
public override string ToString()
|
||||
{
|
||||
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds + track.WindUpTimer;
|
||||
return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} "
|
||||
+ (IsExtrapolated
|
||||
? $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4} => {ExtrapolatedTime:N4}"
|
||||
: $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4}"
|
||||
) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
BeatTimestamp GetTimestampRelativeToGivenOffset(AudioSource start, AudioSource loop, float offset, bool isLooping)
|
||||
class JesterAudioSourcesState
|
||||
{
|
||||
private readonly float StartClipLength;
|
||||
|
||||
// Neither start.isPlaying or loop.isPlaying are reliable indicators of which track is actually playing right now:
|
||||
// start.isPlaying would be true during the loop when Jester chases a player,
|
||||
// loop.isPlaying would be true when it is played delyaed but hasn't actually started playing yet.
|
||||
private readonly ExtrapolatedAudioSourceState Start = new();
|
||||
|
||||
private readonly ExtrapolatedAudioSourceState Loop = new();
|
||||
|
||||
// If true, use Start state as a reference, otherwise use Loop.
|
||||
private bool ReferenceIsStart = true;
|
||||
|
||||
public bool HasStarted => Start.HasStarted;
|
||||
|
||||
public bool IsExtrapolated => ReferenceIsStart ? Start.IsExtrapolated : Loop.IsExtrapolated;
|
||||
|
||||
// Time from the start of the start clip. It wraps when the loop AudioSource loops:
|
||||
// [...start...][...loop...]
|
||||
// ^ |
|
||||
// `----------'
|
||||
public float Time => ReferenceIsStart
|
||||
? Start.Time
|
||||
: StartClipLength + Loop.Time;
|
||||
|
||||
public JesterAudioSourcesState(float startClipLength)
|
||||
{
|
||||
StartClipLength = startClipLength;
|
||||
}
|
||||
|
||||
public void Update(AudioSource start, AudioSource loop, float realtime)
|
||||
{
|
||||
// It doesn't make sense to update start state after loop has started (because start.isPlaying occasionally becomes true).
|
||||
// But always makes sense to update loop, so we can check if it has actually started.
|
||||
Loop.Update(loop, realtime);
|
||||
|
||||
if (!Loop.HasStarted)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.Assert(ReferenceIsStart);
|
||||
#endif
|
||||
Start.Update(start, realtime);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReferenceIsStart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This class tracks looping state of the playback, so that the timestamps can be correctly wrapped only when needed.
|
||||
// [... ...time... ...]
|
||||
// ^ |
|
||||
// `---|---' loop
|
||||
// ^ IsLooping becomes true and stays true forever.
|
||||
class AudioLoopingState
|
||||
{
|
||||
public bool IsLooping { get; private set; } = false;
|
||||
|
||||
private readonly float StartOfLoop;
|
||||
private readonly float LoopLength;
|
||||
private readonly int Beats;
|
||||
|
||||
public AudioLoopingState(float startOfLoop, float loopLength, int beats)
|
||||
{
|
||||
StartOfLoop = startOfLoop;
|
||||
LoopLength = loopLength;
|
||||
Beats = beats;
|
||||
}
|
||||
|
||||
public BeatTimestamp Update(float time, bool isExtrapolated, float additionalOffset)
|
||||
{
|
||||
// If popped, calculate which beat the music is currently at.
|
||||
// In order to do that we should choose one of two strategies:
|
||||
|
@ -1114,42 +1171,114 @@ 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.
|
||||
|
||||
var timeFromTheVeryStart = start.isPlaying && start.time != 0f
|
||||
// [1] Start source is still playing
|
||||
? start.time
|
||||
// [2] Start source has finished
|
||||
: track.LoadedStart.length + loop.time;
|
||||
var offset = StartOfLoop + additionalOffset;
|
||||
|
||||
float adjustedTimeFromOffset = timeFromTheVeryStart - offset;
|
||||
float timeSinceStartOfLoop = time - offset;
|
||||
|
||||
var adjustedTimeNormalized = adjustedTimeFromOffset / track.LoadedLoop.length;
|
||||
var adjustedTimeNormalized = timeSinceStartOfLoop / LoopLength;
|
||||
|
||||
var beat = adjustedTimeNormalized * track.Beats;
|
||||
var beat = adjustedTimeNormalized * Beats;
|
||||
|
||||
// Let it infer the isLooping flag from the beat
|
||||
var timestamp = new BeatTimestamp(track.Beats, isLooping, beat);
|
||||
var timestamp = new BeatTimestamp(Beats, IsLooping, beat, isExtrapolated);
|
||||
|
||||
IsLooping |= timestamp.IsLooping;
|
||||
|
||||
#if DEBUG && false
|
||||
var color = ColorFromPaletteAtTimestamp(timestamp);
|
||||
Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} Start[{3}{4,8:N4} zero? {5}] Loop[{6}{7,8:N4}] norm={8,6:N4} beat={9,7:N4} color={10}",
|
||||
Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} {3} Time={4:N4} norm={5,6:N4} beat={6,7:N4}",
|
||||
nameof(MuzikaGromche),
|
||||
Time.realtimeSinceStartup, Time.deltaTime,
|
||||
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
||||
(loop.isPlaying ? '+' : ' '), loop.time,
|
||||
adjustedTimeNormalized, beat, color);
|
||||
isExtrapolated ? 'E' : '_', time,
|
||||
adjustedTimeNormalized, beat);
|
||||
#endif
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
class BeatTimeState
|
||||
{
|
||||
private readonly Track track;
|
||||
|
||||
private readonly JesterAudioSourcesState AudioState;
|
||||
|
||||
// Colors wrap from WindUpTimer
|
||||
private readonly AudioLoopingState WindUpLoopingState;
|
||||
|
||||
// Events other than colors wrap from WindUpTimer+LoopOffset.
|
||||
private readonly AudioLoopingState LoopLoopingState;
|
||||
|
||||
private float LastKnownLoopOffsetBeat = float.NegativeInfinity;
|
||||
|
||||
private static System.Random LyricsRandom = null!;
|
||||
|
||||
private int LyricsRandomPerLoop;
|
||||
|
||||
private bool WindUpZeroBeatEventTriggered = false;
|
||||
|
||||
public BeatTimeState(Track track)
|
||||
{
|
||||
if (LyricsRandom == null)
|
||||
{
|
||||
LyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337);
|
||||
LyricsRandomPerLoop = LyricsRandom.Next();
|
||||
}
|
||||
this.track = track;
|
||||
AudioState = new(track.LoadedStart.length);
|
||||
WindUpLoopingState = new(track.WindUpTimer, track.LoadedLoop.length, track.Beats);
|
||||
LoopLoopingState = new(track.WindUpTimer + track.LoopOffsetInSeconds, track.LoadedLoop.length, track.Beats);
|
||||
}
|
||||
|
||||
public List<BaseEvent> Update(AudioSource start, AudioSource loop)
|
||||
{
|
||||
var time = Time.realtimeSinceStartup;
|
||||
AudioState.Update(start, loop, time);
|
||||
|
||||
if (AudioState.HasStarted)
|
||||
{
|
||||
var loopTimestamp = Update(LoopLoopingState);
|
||||
var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopTimestamp);
|
||||
|
||||
// Do not go back in time
|
||||
if (!loopOffsetSpan.IsEmpty())
|
||||
{
|
||||
if (loopOffsetSpan.BeatFromExclusive > loopOffsetSpan.BeatToInclusive)
|
||||
{
|
||||
LyricsRandomPerLoop = LyricsRandom.Next();
|
||||
}
|
||||
|
||||
var windUpOffsetTimestamp = Update(WindUpLoopingState);
|
||||
LastKnownLoopOffsetBeat = loopTimestamp.Beat;
|
||||
var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp);
|
||||
#if DEBUG
|
||||
Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}");
|
||||
#endif
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private BeatTimestamp Update(AudioLoopingState loopingState)
|
||||
{
|
||||
return loopingState.Update(AudioState.Time, AudioState.IsExtrapolated, AdditionalOffset());
|
||||
}
|
||||
|
||||
// Timings that may be changes through config
|
||||
private float AdditionalOffset()
|
||||
{
|
||||
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds;
|
||||
}
|
||||
|
||||
private List<BaseEvent> GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp)
|
||||
{
|
||||
List<BaseEvent> events = [];
|
||||
|
||||
if (windUpOffsetTimestamp.Beat >= 0f && !windUpZeroBeatEventTriggered)
|
||||
if (windUpOffsetTimestamp.Beat >= 0f && !WindUpZeroBeatEventTriggered)
|
||||
{
|
||||
events.Add(new WindUpZeroBeatEvent());
|
||||
windUpZeroBeatEventTriggered = true;
|
||||
WindUpZeroBeatEventTriggered = true;
|
||||
}
|
||||
|
||||
if (GetColorEvent(loopOffsetSpan, windUpOffsetTimestamp) is { } colorEvent)
|
||||
|
@ -1171,7 +1300,7 @@ namespace MuzikaGromche
|
|||
{
|
||||
var line = track.LyricsLines[i];
|
||||
var alternatives = line.Split('\t');
|
||||
var randomIndex = lyricsRandomPerLoop % alternatives.Length;
|
||||
var randomIndex = LyricsRandomPerLoop % alternatives.Length;
|
||||
var alternative = alternatives[randomIndex];
|
||||
if (alternative != "")
|
||||
{
|
||||
|
@ -1466,6 +1595,7 @@ namespace MuzikaGromche
|
|||
|
||||
public static ConfigEntry<bool> OverrideSpawnRates { get; private set; } = null!;
|
||||
|
||||
public static bool ExtrapolateTime { get; private set; } = true;
|
||||
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
||||
|
||||
public static Palette? PaletteOverride { get; private set; } = null;
|
||||
|
@ -1502,6 +1632,7 @@ namespace MuzikaGromche
|
|||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions())));
|
||||
|
||||
#if DEBUG
|
||||
SetupEntriesForExtrapolation(configFile);
|
||||
SetupEntriesToSkipWinding(configFile);
|
||||
SetupEntriesForPaletteOverride(configFile);
|
||||
SetupEntriesForTimingsOverride(configFile);
|
||||
|
@ -1600,6 +1731,22 @@ namespace MuzikaGromche
|
|||
}
|
||||
|
||||
#if DEBUG
|
||||
private void SetupEntriesForExtrapolation(ConfigFile configFile)
|
||||
{
|
||||
var syncedEntry = configFile.BindSyncedEntry("General", "Extrapolate Audio Playback Time", true,
|
||||
new ConfigDescription("AudioSource only updates its playback position about 20 times per second.\n\nUse extrapolation technique to predict playback time between updates for smoother color animations."));
|
||||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(syncedEntry.Entry, Default(new BoolCheckBoxOptions())));
|
||||
CSyncHackAddSyncedEntry(syncedEntry);
|
||||
syncedEntry.Changed += (sender, args) => apply();
|
||||
syncedEntry.SyncHostToLocal();
|
||||
apply();
|
||||
|
||||
void apply()
|
||||
{
|
||||
ExtrapolateTime = syncedEntry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupEntriesToSkipWinding(ConfigFile configFile)
|
||||
{
|
||||
var syncedEntry = configFile.BindSyncedEntry("General", "Skip Winding Phase", false,
|
||||
|
@ -2015,9 +2162,9 @@ namespace MuzikaGromche
|
|||
}
|
||||
|
||||
// Manage the timeline: switch color of the lights according to the current playback/beat position.
|
||||
if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState != null)
|
||||
if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState is { } beatTimeState)
|
||||
{
|
||||
var events = Plugin.BeatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
|
||||
var events = beatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
|
||||
foreach (var ev in events)
|
||||
{
|
||||
switch (ev)
|
||||
|
|
Loading…
Reference in New Issue