1
0
Fork 0

Split Track into Selectable and Audio interfaces, add support for groups

This commit is contained in:
ivan tkachenko 2025-08-14 18:47:33 +03:00
parent 47f984cd28
commit 5649a18633
2 changed files with 215 additions and 79 deletions

View File

@ -2,6 +2,7 @@
## MuzikaGromche 1337.420.9001
- Added support for tracks to rotate between multiple audio variants during a round.
## MuzikaGromche 1337.420.69 - It's All DiscoNnected Edition

View File

@ -48,8 +48,8 @@ namespace MuzikaGromche
.Select(a => $" Trying... {a}")
];
public static readonly Track[] Tracks = [
new Track
public static readonly ISelectableTrack[] Tracks = [
new SelectableAudioTrack
{
Name = "MuzikaGromche",
AudioType = AudioType.OGGVORBIS,
@ -89,7 +89,7 @@ namespace MuzikaGromche
(63, "Muzyka Gromche\nGlaza zakryty >_<"),
],
},
new Track
new SelectableAudioTrack
{
Name = "VseVZale",
AudioType = AudioType.OGGVORBIS,
@ -124,7 +124,7 @@ namespace MuzikaGromche
(60, "Everybody shake your body"),
],
},
new Track
new SelectableAudioTrack
{
Name = "DeployDestroy",
AudioType = AudioType.OGGVORBIS,
@ -167,7 +167,7 @@ namespace MuzikaGromche
(25, "Davaj-davaj!"),
],
},
new Track
new SelectableAudioTrack
{
Name = "MoyaZhittya",
AudioType = AudioType.OGGVORBIS,
@ -214,7 +214,7 @@ namespace MuzikaGromche
( 30, "IT'S MY"),
],
},
new Track
new SelectableAudioTrack
{
Name = "Gorgorod",
AudioType = AudioType.OGGVORBIS,
@ -232,7 +232,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [20],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "Durochka",
AudioType = AudioType.OGGVORBIS,
@ -250,7 +250,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-9],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "ZmeiGorynich",
AudioType = AudioType.OGGVORBIS,
@ -268,7 +268,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5, 31],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "GodMode",
AudioType = AudioType.OGGVORBIS,
@ -286,7 +286,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "RiseAndShine",
AudioType = AudioType.OGGVORBIS,
@ -304,7 +304,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5.5f, 31, 63.9f],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "Song2",
AudioType = AudioType.OGGVORBIS,
@ -322,7 +322,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [2.5f],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "Peretasovka",
AudioType = AudioType.OGGVORBIS,
@ -340,7 +340,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-8, 31],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "Yalgaar",
AudioType = AudioType.OGGVORBIS,
@ -358,7 +358,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "Chereshnya",
AudioType = AudioType.OGGVORBIS,
@ -379,7 +379,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5, 27, 29, 59, 61],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "PWNED",
AudioType = AudioType.OGGVORBIS,
@ -451,7 +451,7 @@ namespace MuzikaGromche
(98, $"\t\t\tresolving ur private IP\nP_WNED"),
],
},
new Track
new SelectableAudioTrack
{
Name = "Kach",
AudioType = AudioType.OGGVORBIS,
@ -475,7 +475,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45],
Lyrics = [],
},
new Track
new SelectableAudioTrack
{
Name = "BeefLiver",
AudioType = AudioType.OGGVORBIS,
@ -498,7 +498,7 @@ namespace MuzikaGromche
},
];
public static Track ChooseTrack()
public static ISelectableTrack ChooseTrack()
{
var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed;
var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
@ -510,12 +510,12 @@ namespace MuzikaGromche
return tracks[trackId];
}
public static Track? FindTrackNamed(string name)
public static IAudioTrack? FindTrackNamed(string name)
{
return Tracks.FirstOrDefault(track => track.Name == name);
return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name);
}
internal static Track? CurrentTrack;
internal static IAudioTrack? CurrentTrack;
internal static BeatTimeState? BeatTimeState;
public static void SetLightColor(Color color)
@ -562,7 +562,7 @@ namespace MuzikaGromche
Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> requests = [];
requests.EnsureCapacity(Tracks.Length * 2);
foreach (var track in Tracks)
foreach (var track in Tracks.SelectMany(track => track.GetTracks()))
{
foreach (var (fileName, setter) in new (string, Action<AudioClip>)[]
{
@ -599,7 +599,7 @@ namespace MuzikaGromche
#if DEBUG
foreach (var track in Tracks)
{
Debug.Log($"{nameof(MuzikaGromche)} Track {track.Name} {track.LoadedIntro.length:N4} {track.LoadedLoop.length:N4}");
track.Debug();
}
#endif
Config = new Config(base.Config);
@ -721,60 +721,80 @@ namespace MuzikaGromche
}
}
public class Track
public struct SelectableTrackData()
{
public required string Name;
// Name of the track, as shown in config entry UI; also used for default file names.
public required string Name { get; init; }
// Language of the track's lyrics.
public required Language Language;
public required Language Language { get; init; }
// Whether this track has NSFW/explicit lyrics.
public bool IsExplicit = false;
public bool IsExplicit { get; init; } = false;
// How often this track should be chosen, relative to the sum of weights of all tracks.
public ConfigEntry<int> Weight { get; internal set; } = null!;
}
// An instance of a track which appears as a configuration entry and
// can be selected using weighted random from a list of selectable tracks.
public interface ISelectableTrack
{
// Name of the track, as shown in config entry UI; also used for default file names.
public string Name { get; init; }
// Language of the track's lyrics.
public Language Language { get; init; }
// Whether this track has NSFW/explicit lyrics.
public bool IsExplicit { get; init; }
// How often this track should be chosen, relative to the sum of weights of all tracks.
internal ConfigEntry<int> Weight { get; set; }
internal IAudioTrack[] GetTracks();
// Index is a non-negative monotonically increasing number of times
// this ISelectableTrack has been played for this Jester on this day.
// A group of tracks can use this index to rotate tracks sequentially.
internal IAudioTrack SelectTrack(int index);
internal void Debug();
}
// An instance of a track which has file names, timings data, palette; can be loaded and played.
public interface IAudioTrack
{
// Name of the track used for default file names.
public string Name { get; }
// Wind-up time can and should be shorter than the Intro audio track,
// so that the "pop" effect can be baked into the Intro audio and kept away
// from the looped part. This also means that the light show starts before
// the looped track does, so we need to sync them up as soon as we enter the Loop.
public required float WindUpTimer;
public float WindUpTimer { get; }
// Estimated number of beats per minute. Not used for light show, but might come in handy.
public float Bpm => 60f / (LoadedLoop.length / Beats);
// How many beats the loop segment has. The default strategy is to switch color of lights on each beat.
public int Beats;
public int Beats { get; }
// Number of beats between WindUpTimer and where looped segment starts (not the loop audio).
public int LoopOffset = 0;
public int LoopOffset { get; }
public float LoopOffsetInSeconds => LoopOffset / Beats * LoadedLoop.length;
// Shorthand for four beats
public int Bars
{
set => Beats = value * 4;
}
// MPEG is basically mp3, and it can produce gaps at the start.
// WAV is OK, but takes a lot of space. Try OGGVORBIS instead.
public AudioType AudioType = AudioType.MPEG;
public AudioType AudioType { get; }
public AudioClip LoadedIntro = null!;
public AudioClip LoadedLoop = null!;
public AudioClip LoadedIntro { get; internal set; }
public AudioClip LoadedLoop { get; internal set; }
// How often this track should be chosen, relative to the sum of weights of all tracks.
public ConfigEntry<int> Weight = null!;
public string FileNameIntro { get; }
public string FileNameLoop { get; }
private string? FileNameIntroOverride = null;
public string FileNameIntro
{
get => FileNameIntroOverride ?? $"{Name}Intro.{Ext}";
set => FileNameIntroOverride = value;
}
private string? FileNameLoopOverride = null;
public string FileNameLoop
{
get => FileNameLoopOverride ?? $"{Name}Loop.{Ext}";
set => FileNameLoopOverride = value;
}
private string Ext => AudioType switch
public string Ext => AudioType switch
{
AudioType.MPEG => "mp3",
AudioType.WAV => "wav",
@ -783,28 +803,86 @@ namespace MuzikaGromche
};
// Offset of beats. Bigger offset => colors will change later.
public float BeatsOffset { get; }
// Offset of beats, in seconds. Bigger offset => colors will change later.
public float BeatsOffsetInSeconds => BeatsOffset / Beats * LoadedLoop.length;
public float FadeOutBeat { get; }
public float FadeOutDuration { get; }
// Duration of color transition, measured in beats.
public float ColorTransitionIn { get; }
public float ColorTransitionOut { get; }
// Easing function for color transitions.
public Easing ColorTransitionEasing { get; }
public float[] FlickerLightsTimeSeries { get; }
public float[] LyricsTimeSeries { get; }
// Lyrics line may contain multiple tab-separated alternatives.
// In such case, a random number chosen and updated once per loop
// is used to select an alternative.
// If the chosen alternative is an empty string, lyrics event shall be skipped.
public string[] LyricsLines { get; }
public Palette Palette { get; }
}
// Core audio track implementation with some defaults and config overrides.
// Suitable to declare elemnents of SelectableTracksGroup and as a base for standalone selectable tracks.
public class CoreAudioTrack : IAudioTrack
{
public required string Name { get; init; }
public required float WindUpTimer { get; init; }
public int Beats { get; init; }
// Shorthand for four beats
public int Bars
{
init => Beats = value * 4;
}
public int LoopOffset { get; init; } = 0;
public AudioType AudioType { get; init; } = AudioType.MPEG;
public AudioClip LoadedIntro { get; set; } = null!;
public AudioClip LoadedLoop { get; set; } = null!;
private string? FileNameIntroOverride = null;
public string FileNameIntro
{
get => FileNameIntroOverride ?? $"{Name}Intro.{((IAudioTrack)this).Ext}";
init => FileNameIntroOverride = value;
}
private string? FileNameLoopOverride = null;
public string FileNameLoop
{
get => FileNameLoopOverride ?? $"{Name}Loop.{((IAudioTrack)this).Ext}";
init => FileNameLoopOverride = value;
}
public float _BeatsOffset = 0f;
public float BeatsOffset
{
get => Config.BeatsOffsetOverride ?? _BeatsOffset;
set => _BeatsOffset = value;
init => _BeatsOffset = value;
}
// 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;
init => _FadeOutBeat = value;
}
public float _FadeOutDuration = 2f;
public float FadeOutDuration
{
get => Config.FadeOutDurationOverride ?? _FadeOutDuration;
set => _FadeOutDuration = value;
init => _FadeOutDuration = value;
}
// Duration of color transition, measured in beats.
@ -812,14 +890,14 @@ namespace MuzikaGromche
public float ColorTransitionIn
{
get => Config.ColorTransitionInOverride ?? _ColorTransitionIn;
set => _ColorTransitionIn = value;
init => _ColorTransitionIn = value;
}
public float _ColorTransitionOut = 0.25f;
public float ColorTransitionOut
{
get => Config.ColorTransitionOutOverride ?? _ColorTransitionOut;
set => _ColorTransitionOut = value;
init => _ColorTransitionOut = value;
}
// Easing function for color transitions.
@ -829,14 +907,14 @@ namespace MuzikaGromche
get => Config.ColorTransitionEasingOverride != null
? Easing.FindByName(Config.ColorTransitionEasingOverride)
: _ColorTransitionEasing;
set => _ColorTransitionEasing = value;
init => _ColorTransitionEasing = value;
}
public float[] _FlickerLightsTimeSeries = [];
public float[] FlickerLightsTimeSeries
{
get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries;
set
init
{
Array.Sort(value);
_FlickerLightsTimeSeries = value;
@ -877,6 +955,53 @@ namespace MuzikaGromche
}
}
// Standalone, top-level, selectable audio track
public class SelectableAudioTrack : CoreAudioTrack, ISelectableTrack
{
public required Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
IAudioTrack ISelectableTrack.SelectTrack(int index) => this;
void ISelectableTrack.Debug()
{
Debug.Log($"{nameof(MuzikaGromche)} Track \"{Name}\", Intro={LoadedIntro.length:N4}, Loop={LoadedLoop.length:N4}");
}
}
public class SelectableTracksGroup : ISelectableTrack
{
public required string Name { get; init; }
public required Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
public required IAudioTrack[] Tracks;
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
IAudioTrack ISelectableTrack.SelectTrack(int index)
{
if (Tracks.Length == 0)
{
throw new IndexOutOfRangeException("Tracks list is empty");
}
return Mod.Index(Tracks, index);
}
void ISelectableTrack.Debug()
{
Debug.Log($"{nameof(MuzikaGromche)} Track Group \"{Name}\", Count={Tracks.Length}");
foreach (var (track, index) in Tracks.Select((x, i) => (x, i)))
{
Debug.Log($"{nameof(MuzikaGromche)} Track {index} \"{track.Name}\", Intro={track.LoadedIntro.length:N4}, Loop={track.LoadedLoop.length:N4}");
}
}
}
readonly record struct BeatTimestamp
{
// Number of beats in the loop audio segment.
@ -1252,7 +1377,7 @@ namespace MuzikaGromche
class BeatTimeState
{
private readonly Track track;
private readonly IAudioTrack track;
private readonly JesterAudioSourcesState AudioState;
@ -1270,7 +1395,7 @@ namespace MuzikaGromche
private bool WindUpZeroBeatEventTriggered = false;
public BeatTimeState(Track track)
public BeatTimeState(IAudioTrack track)
{
if (LyricsRandom == null)
{
@ -1853,7 +1978,7 @@ namespace MuzikaGromche
void load()
{
var palette = Plugin.CurrentTrack?._Palette ?? Palette.DEFAULT;
var palette = (Plugin.CurrentTrack as CoreAudioTrack)?._Palette ?? Palette.DEFAULT;
var colors = palette.Colors;
var count = Math.Min(colors.Count(), maxCustomPaletteSize);
@ -1886,7 +2011,7 @@ namespace MuzikaGromche
const string section = "Timings";
var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f);
// Declare and initialize early to avoid "Use of unassigned local variable"
List<(Action<Track?> Load, Action Apply)> entries = [];
List<(Action<CoreAudioTrack?> Load, Action Apply)> entries = [];
SyncedEntry<bool> overrideTimingsSyncedEntry = null!;
SyncedEntry<float> fadeOutBeatSyncedEntry = null!;
SyncedEntry<float> fadeOutDurationSyncedEntry = null!;
@ -1945,12 +2070,12 @@ namespace MuzikaGromche
registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = x);
registerClass(colorTransitionEasingSyncedEntry, t => t._ColorTransitionEasing.Name, x => ColorTransitionEasingOverride = x);
void register<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action applier)
void register<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action applier)
{
CSyncHackAddSyncedEntry(syncedEntry);
syncedEntry.SyncHostToLocal();
syncedEntry.Changed += (sender, args) => applier();
void loader(Track? track)
void loader(CoreAudioTrack? track)
{
// if track is null, set everything to defaults
syncedEntry.LocalValue = track == null ? (T)syncedEntry.Entry.DefaultValue : getter(track);
@ -1958,11 +2083,11 @@ namespace MuzikaGromche
entries.Add((loader, applier));
}
void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action<T?> setter) where T : struct =>
void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : struct =>
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 =>
void registerClass<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, 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 =>
void registerArray<T>(SyncedEntry<string> syncedEntry, Func<CoreAudioTrack, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct =>
register(syncedEntry,
(track) => string.Join(", ", getter(track)),
() =>
@ -1996,7 +2121,7 @@ namespace MuzikaGromche
var track = Plugin.CurrentTrack;
foreach (var entry in entries)
{
entry.Load(track);
entry.Load(track as CoreAudioTrack);
}
}
@ -2043,6 +2168,11 @@ namespace MuzikaGromche
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
{
// Number of times a selected track has been played.
// Increases by 1 with each ChooseTrackServerRpc call.
// Resets on SettingChanged.
private int SelectedTrackIndex = 0;
public override void OnNetworkSpawn()
{
ChooseTrackDeferred();
@ -2069,6 +2199,7 @@ namespace MuzikaGromche
private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
{
SelectedTrackIndex = 0;
ChooseTrackDeferred();
}
@ -2099,9 +2230,11 @@ namespace MuzikaGromche
[ServerRpc]
public void ChooseTrackServerRpc()
{
var track = Plugin.ChooseTrack();
Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {track.Name}");
SetTrackClientRpc(track.Name);
var selectableTrack = Plugin.ChooseTrack();
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}");
SetTrackClientRpc(audioTrack.Name);
SelectedTrackIndex += 1;
}
}
@ -2195,6 +2328,8 @@ namespace MuzikaGromche
{
Plugin.ResetLightColor();
DiscoBallManager.Disable();
// Rotate track groups
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc();
}
if (__instance.previousState == 2 && __state.previousState != 2)