forked from nikita/muzika-gromche
Rewrite AudioSource handling from scratch
This commit is contained in:
parent
e67de4556c
commit
73ad702684
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
- Added a new track OnePartiyaUdar in Japanese language.
|
- Added a new track OnePartiyaUdar in Japanese language.
|
||||||
- Remastered recently added tracks at conventional 44100 Hz for better stitching.
|
- Remastered recently added tracks at conventional 44100 Hz for better stitching.
|
||||||
|
- Improved playback experience: use precise DSP time and up-front scheduing for seamless audio stitching, add custom Audio Sources to improve reliability.
|
||||||
|
|
||||||
## MuzikaGromche 1337.420.9001 - Multiverse Edition
|
## MuzikaGromche 1337.420.9001 - Multiverse Edition
|
||||||
|
|
||||||
|
|
|
@ -2249,20 +2249,57 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug.Log($"{nameof(MuzikaGromche)} Patching {nameof(JesterAI)} with {nameof(MuzikaGromcheJesterNetworkBehaviour)} component");
|
|
||||||
networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>();
|
networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>();
|
||||||
|
Debug.Log($"{nameof(MuzikaGromche)} Patched JesterEnemy");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
|
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
|
||||||
{
|
{
|
||||||
|
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
|
||||||
|
const string LoopAudioGameObjectName = "MuzikaGromcheAudio (Loop)";
|
||||||
|
|
||||||
// Number of times a selected track has been played.
|
// Number of times a selected track has been played.
|
||||||
// Increases by 1 with each ChooseTrackServerRpc call.
|
// Increases by 1 with each ChooseTrackServerRpc call.
|
||||||
// Resets on SettingChanged.
|
// Resets on SettingChanged.
|
||||||
private int SelectedTrackIndex = 0;
|
private int SelectedTrackIndex = 0;
|
||||||
|
|
||||||
internal BeatTimeState? BeatTimeState = null;
|
internal BeatTimeState? BeatTimeState = null;
|
||||||
|
internal AudioSource IntroAudioSource = null!;
|
||||||
|
internal AudioSource LoopAudioSource = null!;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
var farAudioTransform = gameObject.transform.Find("FarAudio");
|
||||||
|
if (farAudioTransform == null)
|
||||||
|
{
|
||||||
|
Debug.LogError($"{nameof(MuzikaGromche)} JesterEnemy->FarAudio prefab not found!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Instead of hijacking farAudio and creatureVoice sources,
|
||||||
|
// create our own copies to ensure uniform playback experience.
|
||||||
|
// For reasons unknown adding them directly to the prefab didn't work.
|
||||||
|
var introAudioGameObject = Instantiate(farAudioTransform.gameObject, gameObject.transform);
|
||||||
|
introAudioGameObject.name = IntroAudioGameObjectName;
|
||||||
|
|
||||||
|
var loopAudioGameObject = Instantiate(farAudioTransform.gameObject, gameObject.transform);
|
||||||
|
loopAudioGameObject.name = LoopAudioGameObjectName;
|
||||||
|
|
||||||
|
IntroAudioSource = introAudioGameObject.GetComponent<AudioSource>();
|
||||||
|
IntroAudioSource.maxDistance = Plugin.AudioMaxDistance;
|
||||||
|
IntroAudioSource.dopplerLevel = 0;
|
||||||
|
IntroAudioSource.loop = false;
|
||||||
|
|
||||||
|
LoopAudioSource = loopAudioGameObject.GetComponent<AudioSource>();
|
||||||
|
LoopAudioSource.maxDistance = Plugin.AudioMaxDistance;
|
||||||
|
LoopAudioSource.dopplerLevel = 0;
|
||||||
|
LoopAudioSource.loop = true;
|
||||||
|
|
||||||
|
Debug.Log($"{nameof(MuzikaGromche)} {nameof(MuzikaGromcheJesterNetworkBehaviour)} Patched JesterEnemy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
|
@ -2327,26 +2364,37 @@ namespace MuzikaGromche
|
||||||
SetTrackClientRpc(audioTrack.Name);
|
SetTrackClientRpc(audioTrack.Name);
|
||||||
SelectedTrackIndex += 1;
|
SelectedTrackIndex += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void PlayScheduledLoop()
|
||||||
|
{
|
||||||
|
double loopStartDspTime = AudioSettings.dspTime + IntroAudioSource.clip.length - IntroAudioSource.time;
|
||||||
|
LoopAudioSource.PlayScheduled(loopStartDspTime);
|
||||||
|
Debug.Log($"{nameof(MuzikaGromche)} Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// farAudio is during windup, Intro overrides popGoesTheWeaselTheme
|
|
||||||
// creatureVoice is when popped, Loop overrides screamingSFX
|
|
||||||
[HarmonyPatch(typeof(JesterAI))]
|
[HarmonyPatch(typeof(JesterAI))]
|
||||||
static class JesterPatch
|
static class JesterPatch
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
[HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))]
|
[HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))]
|
||||||
[HarmonyPostfix]
|
[HarmonyPostfix]
|
||||||
static void AlmostInstantFollowTimerPostfix(JesterAI __instance)
|
static void SetJesterInitialValuesPostfix(JesterAI __instance)
|
||||||
{
|
{
|
||||||
|
var behaviour = __instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>();
|
||||||
|
behaviour.IntroAudioSource.Stop();
|
||||||
|
behaviour.LoopAudioSource.Stop();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
// Almost instant follow timer
|
||||||
__instance.beginCrankingTimer = 1f;
|
__instance.beginCrankingTimer = 1f;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
class State
|
class State
|
||||||
{
|
{
|
||||||
public required AudioSource farAudio;
|
public required int currentBehaviourStateIndex;
|
||||||
public required int previousState;
|
public required int previousState;
|
||||||
|
public required float stunNormalizedTimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HarmonyPatch(nameof(JesterAI.Update))]
|
[HarmonyPatch(nameof(JesterAI.Update))]
|
||||||
|
@ -2355,20 +2403,10 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
__state = new State
|
__state = new State
|
||||||
{
|
{
|
||||||
farAudio = __instance.farAudio,
|
currentBehaviourStateIndex = __instance.currentBehaviourStateIndex,
|
||||||
previousState = __instance.previousState,
|
previousState = __instance.previousState,
|
||||||
|
stunNormalizedTimer = __instance.stunNormalizedTimer,
|
||||||
};
|
};
|
||||||
if (__instance.currentBehaviourStateIndex == 2 && __instance.previousState != 2)
|
|
||||||
{
|
|
||||||
// If just popped out, then override farAudio so that vanilla logic does not stop the modded Intro music.
|
|
||||||
// The game will stop farAudio it during its Update, so we temporarily set it to any other AudioSource
|
|
||||||
// which we don't care about stopping for now.
|
|
||||||
//
|
|
||||||
// Why creatureVoice though? We gonna need creatureVoice later in Postfix to schedule the Loop,
|
|
||||||
// but right now we still don't care if it's stopped, so it shouldn't matter.
|
|
||||||
// And it's cheaper and simpler than figuring out how to instantiate an AudioSource behaviour.
|
|
||||||
__instance.farAudio = __instance.creatureVoice;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HarmonyPatch(nameof(JesterAI.Update))]
|
[HarmonyPatch(nameof(JesterAI.Update))]
|
||||||
|
@ -2378,76 +2416,84 @@ namespace MuzikaGromche
|
||||||
if (Plugin.CurrentTrack == null)
|
if (Plugin.CurrentTrack == null)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Debug.Log($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
|
Debug.LogError($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
|
||||||
#endif
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var behaviour = __instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>();
|
var behaviour = __instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>();
|
||||||
|
var introAudioSource = behaviour.IntroAudioSource;
|
||||||
|
var loopAudioSource = behaviour.LoopAudioSource;
|
||||||
|
|
||||||
if (__instance.previousState == 1 && __state.previousState != 1)
|
// This switch statement resembles the one from JesterAI.Update
|
||||||
|
switch (__state.currentBehaviourStateIndex)
|
||||||
{
|
{
|
||||||
// if just started winding up
|
case 1:
|
||||||
// then stop the default music...
|
if (__state.previousState != 1)
|
||||||
__instance.farAudio.Stop();
|
{
|
||||||
__instance.creatureVoice.Stop();
|
// if just started winding up
|
||||||
|
// then stop the default music... (already done above)
|
||||||
|
// ...and set up both modded audio clips in advance
|
||||||
|
introAudioSource.clip = Plugin.CurrentTrack.LoadedIntro;
|
||||||
|
loopAudioSource.clip = Plugin.CurrentTrack.LoadedLoop;
|
||||||
|
behaviour.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack);
|
||||||
|
|
||||||
// ...and start modded music
|
// Set up custom popup timer, which is shorter than Intro audio
|
||||||
behaviour.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack);
|
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
|
||||||
// Set up custom popup timer, which is shorter than Start audio
|
|
||||||
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
|
|
||||||
|
|
||||||
// Override popGoesTheWeaselTheme with Start audio
|
if (Config.ShouldSkipWindingPhase)
|
||||||
__instance.farAudio.maxDistance = Plugin.AudioMaxDistance;
|
{
|
||||||
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro;
|
var rewind = 5f;
|
||||||
__instance.farAudio.loop = false;
|
__instance.popUpTimer = rewind;
|
||||||
if (Config.ShouldSkipWindingPhase)
|
introAudioSource.time = Plugin.CurrentTrack.WindUpTimer - rewind;
|
||||||
{
|
}
|
||||||
var rewind = 5f;
|
else
|
||||||
__instance.popUpTimer = rewind;
|
{
|
||||||
__instance.farAudio.time = Plugin.CurrentTrack.WindUpTimer - rewind;
|
// reset if previously skipped winding by assigning different starting time.
|
||||||
}
|
introAudioSource.time = 0f;
|
||||||
else
|
}
|
||||||
{
|
|
||||||
// reset if previously skipped winding by assigning different starting time.
|
|
||||||
__instance.farAudio.time = 0;
|
|
||||||
}
|
|
||||||
__instance.farAudio.Play();
|
|
||||||
|
|
||||||
Debug.Log($"{nameof(MuzikaGromche)} Playing Intro music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}");
|
__instance.farAudio.Stop();
|
||||||
|
introAudioSource.Play();
|
||||||
|
behaviour.PlayScheduledLoop();
|
||||||
|
}
|
||||||
|
if (__instance.stunNormalizedTimer > 0f)
|
||||||
|
{
|
||||||
|
introAudioSource.Pause();
|
||||||
|
loopAudioSource.Stop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!introAudioSource.isPlaying)
|
||||||
|
{
|
||||||
|
__instance.farAudio.Stop();
|
||||||
|
introAudioSource.UnPause();
|
||||||
|
behaviour.PlayScheduledLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if (__state.previousState != 2)
|
||||||
|
{
|
||||||
|
__instance.creatureVoice.Stop();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__instance.previousState != 2 && __state.previousState == 2)
|
// transition away from state 2 ("poppedOut"), normally to state 0
|
||||||
|
if (__state.previousState == 2 && __instance.previousState != 2)
|
||||||
{
|
{
|
||||||
behaviour.BeatTimeState = null;
|
|
||||||
Plugin.ResetLightColor();
|
Plugin.ResetLightColor();
|
||||||
DiscoBallManager.Disable();
|
DiscoBallManager.Disable();
|
||||||
// Rotate track groups
|
// Rotate track groups
|
||||||
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc();
|
behaviour.ChooseTrackServerRpc();
|
||||||
}
|
behaviour.BeatTimeState = null;
|
||||||
|
|
||||||
if (__instance.previousState == 2 && __state.previousState != 2)
|
|
||||||
{
|
|
||||||
// Restore stashed AudioSource. See the comment in Prefix
|
|
||||||
__instance.farAudio = __state.farAudio;
|
|
||||||
|
|
||||||
var time = __instance.farAudio.time;
|
|
||||||
var delay = Plugin.CurrentTrack.LoadedIntro.length - time;
|
|
||||||
|
|
||||||
// Override screamingSFX with Loop, delayed by the remaining time of the Intro audio
|
|
||||||
__instance.creatureVoice.Stop();
|
|
||||||
__instance.creatureVoice.maxDistance = Plugin.AudioMaxDistance;
|
|
||||||
__instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop;
|
|
||||||
__instance.creatureVoice.PlayDelayed(delay);
|
|
||||||
|
|
||||||
Debug.Log($"{nameof(MuzikaGromche)} Intro length: {Plugin.CurrentTrack.LoadedIntro.length}; played time: {time}");
|
|
||||||
Debug.Log($"{nameof(MuzikaGromche)} Playing loop music: maxDistance: {__instance.creatureVoice.maxDistance}, minDistance: {__instance.creatureVoice.minDistance}, volume: {__instance.creatureVoice.volume}, spread: {__instance.creatureVoice.spread}, in seconds: {delay}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manage the timeline: switch color of the lights according to the current playback/beat position.
|
// Manage the timeline: switch color of the lights according to the current playback/beat position.
|
||||||
if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState)
|
else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState)
|
||||||
{
|
{
|
||||||
var events = beatTimeState.Update(intro: __instance.farAudio, loop: __instance.creatureVoice);
|
var events = beatTimeState.Update(introAudioSource, loopAudioSource);
|
||||||
foreach (var ev in events)
|
foreach (var ev in events)
|
||||||
{
|
{
|
||||||
switch (ev)
|
switch (ev)
|
||||||
|
|
Loading…
Reference in New Issue