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) | ||||||
|  |             { | ||||||
|  |                 case 1: | ||||||
|  |                     if (__state.previousState != 1) | ||||||
|                     { |                     { | ||||||
|                         // if just started winding up |                         // if just started winding up | ||||||
|                 // then stop the default music... |                         // then stop the default music... (already done above) | ||||||
|                 __instance.farAudio.Stop(); |                         // ...and set up both modded audio clips in advance | ||||||
|                 __instance.creatureVoice.Stop(); |                         introAudioSource.clip = Plugin.CurrentTrack.LoadedIntro; | ||||||
| 
 |                         loopAudioSource.clip = Plugin.CurrentTrack.LoadedLoop; | ||||||
|                 //  ...and start modded music |  | ||||||
|                         behaviour.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack); |                         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; |                         __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) |                         if (Config.ShouldSkipWindingPhase) | ||||||
|                         { |                         { | ||||||
|                             var rewind = 5f; |                             var rewind = 5f; | ||||||
|                             __instance.popUpTimer = rewind; |                             __instance.popUpTimer = rewind; | ||||||
|                     __instance.farAudio.time = Plugin.CurrentTrack.WindUpTimer - rewind; |                             introAudioSource.time = Plugin.CurrentTrack.WindUpTimer - rewind; | ||||||
|                         } |                         } | ||||||
|                         else |                         else | ||||||
|                         { |                         { | ||||||
|                             // reset if previously skipped winding by assigning different starting time. |                             // reset if previously skipped winding by assigning different starting time. | ||||||
|                     __instance.farAudio.time = 0; |                             introAudioSource.time = 0f; | ||||||
|                 } |  | ||||||
|                 __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}"); |  | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|             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(); |                 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