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.
|
||||
- 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
|
||||
|
||||
|
|
|
@ -2249,20 +2249,57 @@ namespace MuzikaGromche
|
|||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"{nameof(MuzikaGromche)} Patching {nameof(JesterAI)} with {nameof(MuzikaGromcheJesterNetworkBehaviour)} component");
|
||||
networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>();
|
||||
Debug.Log($"{nameof(MuzikaGromche)} Patched JesterEnemy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
|
||||
{
|
||||
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
|
||||
const string LoopAudioGameObjectName = "MuzikaGromcheAudio (Loop)";
|
||||
|
||||
// Number of times a selected track has been played.
|
||||
// Increases by 1 with each ChooseTrackServerRpc call.
|
||||
// Resets on SettingChanged.
|
||||
private int SelectedTrackIndex = 0;
|
||||
|
||||
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()
|
||||
{
|
||||
|
@ -2327,26 +2364,37 @@ namespace MuzikaGromche
|
|||
SetTrackClientRpc(audioTrack.Name);
|
||||
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))]
|
||||
static class JesterPatch
|
||||
{
|
||||
#if DEBUG
|
||||
[HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))]
|
||||
[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;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class State
|
||||
{
|
||||
public required AudioSource farAudio;
|
||||
public required int currentBehaviourStateIndex;
|
||||
public required int previousState;
|
||||
public required float stunNormalizedTimer;
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(JesterAI.Update))]
|
||||
|
@ -2355,20 +2403,10 @@ namespace MuzikaGromche
|
|||
{
|
||||
__state = new State
|
||||
{
|
||||
farAudio = __instance.farAudio,
|
||||
currentBehaviourStateIndex = __instance.currentBehaviourStateIndex,
|
||||
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))]
|
||||
|
@ -2378,76 +2416,84 @@ namespace MuzikaGromche
|
|||
if (Plugin.CurrentTrack == null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.Log($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
|
||||
Debug.LogError($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
case 1:
|
||||
if (__state.previousState != 1)
|
||||
{
|
||||
// if just started winding up
|
||||
// then stop the default music...
|
||||
__instance.farAudio.Stop();
|
||||
__instance.creatureVoice.Stop();
|
||||
|
||||
// ...and start modded music
|
||||
// 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);
|
||||
// Set up custom popup timer, which is shorter than Start audio
|
||||
|
||||
// Set up custom popup timer, which is shorter than Intro audio
|
||||
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
|
||||
|
||||
// Override popGoesTheWeaselTheme with Start audio
|
||||
__instance.farAudio.maxDistance = Plugin.AudioMaxDistance;
|
||||
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro;
|
||||
__instance.farAudio.loop = false;
|
||||
if (Config.ShouldSkipWindingPhase)
|
||||
{
|
||||
var rewind = 5f;
|
||||
__instance.popUpTimer = rewind;
|
||||
__instance.farAudio.time = Plugin.CurrentTrack.WindUpTimer - rewind;
|
||||
introAudioSource.time = Plugin.CurrentTrack.WindUpTimer - rewind;
|
||||
}
|
||||
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}");
|
||||
introAudioSource.time = 0f;
|
||||
}
|
||||
|
||||
if (__instance.previousState != 2 && __state.previousState == 2)
|
||||
__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;
|
||||
}
|
||||
|
||||
// transition away from state 2 ("poppedOut"), normally to state 0
|
||||
if (__state.previousState == 2 && __instance.previousState != 2)
|
||||
{
|
||||
behaviour.BeatTimeState = null;
|
||||
Plugin.ResetLightColor();
|
||||
DiscoBallManager.Disable();
|
||||
// Rotate track groups
|
||||
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc();
|
||||
}
|
||||
|
||||
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}");
|
||||
behaviour.ChooseTrackServerRpc();
|
||||
behaviour.BeatTimeState = null;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
switch (ev)
|
||||
|
|
Loading…
Reference in New Issue