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) | ||||
|             { | ||||
|                 // if just started winding up | ||||
|                 // then stop the default music... | ||||
|                 __instance.farAudio.Stop(); | ||||
|                 __instance.creatureVoice.Stop(); | ||||
|                 case 1: | ||||
|                     if (__state.previousState != 1) | ||||
|                     { | ||||
|                         // 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 | ||||
|                 behaviour.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack); | ||||
|                 // Set up custom popup timer, which is shorter than Start audio | ||||
|                 __instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer; | ||||
|                         // 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; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // reset if previously skipped winding by assigning different starting time. | ||||
|                     __instance.farAudio.time = 0; | ||||
|                 } | ||||
|                 __instance.farAudio.Play(); | ||||
|                         if (Config.ShouldSkipWindingPhase) | ||||
|                         { | ||||
|                             var rewind = 5f; | ||||
|                             __instance.popUpTimer = rewind; | ||||
|                             introAudioSource.time = Plugin.CurrentTrack.WindUpTimer - rewind; | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             // reset if previously skipped winding by assigning different starting time. | ||||
|                             introAudioSource.time = 0f; | ||||
|                         } | ||||
| 
 | ||||
|                 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(); | ||||
|                 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