11using System ;
2+ using System . Collections . Concurrent ;
3+ using System . Collections . Generic ;
24using System . IO ;
35using System . Runtime . InteropServices ;
46using FMOD ;
@@ -29,6 +31,29 @@ public bool IsOneshot
2931 /// </summary>
3032 public abstract class FmodAudioPlayback : FmodAudioPlaybackBase , IAudioPlayback
3133 {
34+ // ------------------------------------------------------------------------------------------------------------
35+ // Hideous timeline callback functionality. I do not approve, but this is the way FMOD says it needs to be done:
36+ // https://www.fmod.com/docs/2.03/unity/examples-timeline-callbacks.html
37+ // This class exists so that it can be pinned in memory, which in turn is then passed along to a *static*
38+ // callback method for timeline events *that happens on a separate thread, not the Unity main thread*. Then
39+ // we grab this object, queue up the timeline markers that are reached, and then on the main thread,
40+ // on the next Update, the playback instance will fire all the timeline events that were buffered, to ensure
41+ // that the callbacks happen on the main Unity thread. This thing with pinning the class in memory is not
42+ // necessary to get it to work in the editor, but it IS necessary to get it to work in IL2CPP builds.
43+ // So while this may all look very "un-C#" and bloated and unnecessary, I assure you that every part of this
44+ // is necessary for FMOD's code to work.
45+
46+ // Variables that are modified in the callback need to be part of a seperate class.
47+ // This class needs to be 'blittable' otherwise it can't be pinned in memory.
48+ private class TimelineInfo
49+ {
50+ public readonly ConcurrentQueue < string > TimelineMarkersReached = new ( ) ;
51+ }
52+ private TimelineInfo timelineInfo ;
53+ private GCHandle timelineInfoHandle ;
54+ private EVENT_CALLBACK timelineEventCallback ;
55+ // ------------------------------------------------------------------------------------------------------------
56+
3257 public delegate void TimelineMarkerReachedHandler ( FmodAudioPlayback playback , string markerName ) ;
3358 private int timelineMarkerListenerCount ;
3459 private event TimelineMarkerReachedHandler timelineMarkerReachedEvent ;
@@ -39,19 +64,36 @@ public event TimelineMarkerReachedHandler TimelineMarkerReachedEvent
3964 timelineMarkerReachedEvent += value ;
4065 timelineMarkerListenerCount ++ ;
4166
42- if ( timelineMarkerListenerCount == 1 )
43- Instance . setCallback ( OnTimelineMarkerReached , EVENT_CALLBACK_TYPE . TIMELINE_MARKER ) ;
67+ UpdateTimelineMarkerCallbackState ( ) ;
4468 }
4569 remove
4670 {
4771 timelineMarkerReachedEvent -= value ;
4872 timelineMarkerListenerCount -- ;
4973
50- if ( timelineMarkerListenerCount == 0 )
51- Instance . setCallback ( null , EVENT_CALLBACK_TYPE . TIMELINE_MARKER ) ;
74+ UpdateTimelineMarkerCallbackState ( ) ;
5275 }
5376 }
5477
78+ [ NonSerialized ] private bool hasRegisteredTimelineMarkerReachedCallback ;
79+
80+ public void Update ( )
81+ {
82+ if ( hasRegisteredTimelineMarkerReachedCallback )
83+ FireBufferedTimelineMarkerEvents ( ) ;
84+ }
85+
86+ private void FireBufferedTimelineMarkerEvents ( )
87+ {
88+ // Timeline marker callbacks happen on a separate FMOD audio thread, so to safely pass it along to Unity,
89+ // we need to buffer the timeline marker events and fire them on the next update of the main thread.
90+ while ( timelineInfo . TimelineMarkersReached . TryDequeue ( out string id ) )
91+ {
92+ timelineMarkerReachedEvent ? . Invoke ( this , id ) ;
93+ }
94+ }
95+
96+
5597 public void Play ( EventDescription eventDescription , Transform source )
5698 {
5799 eventDescription . getPath ( out string path ) ;
@@ -101,6 +143,8 @@ public override void Cleanup()
101143 timelineMarkerReachedEvent = null ;
102144 timelineMarkerListenerCount = 0 ;
103145
146+ UpdateTimelineMarkerCallbackState ( true ) ;
147+
104148 if ( Instance . isValid ( ) )
105149 {
106150 Instance . setCallback ( null , EVENT_CALLBACK_TYPE . TIMELINE_MARKER ) ;
@@ -117,6 +161,43 @@ public override void Cleanup()
117161
118162 FmodSyntaxSystem . UnregisterActiveEventPlayback ( this ) ;
119163 }
164+
165+ private void UpdateTimelineMarkerCallbackState ( bool forceRemove = false )
166+ {
167+ bool shouldRegisterTimelineMarkerReachedCallback = timelineMarkerListenerCount > 0 ;
168+ if ( forceRemove )
169+ shouldRegisterTimelineMarkerReachedCallback = false ;
170+
171+ if ( ! hasRegisteredTimelineMarkerReachedCallback && shouldRegisterTimelineMarkerReachedCallback )
172+ {
173+ // -----------------------------------------------------------------------------------------------------
174+ // Timeline callback code (see the comment section at the top of this file)
175+ timelineInfo = new TimelineInfo ( ) ;
176+
177+ // Explicitly create the delegate object and assign it to a member so it doesn't get freed
178+ // by the garbage collected while it's being used
179+ // NOTE: The documentation of 2.00 does this but 2.03 does not, but I am finding a very nasty hard
180+ // editor crash that seemingly only occurs when this delegate object is not created / used,
181+ // so do not remove the delegate object that wraps the method...
182+ timelineEventCallback = new EVENT_CALLBACK ( OnTimelineMarkerReached ) ;
183+
184+ // Pin the class that will store the data modified during the callback
185+ timelineInfoHandle = GCHandle . Alloc ( timelineInfo ) ;
186+ // Pass the object through the userdata of the instance
187+ Instance . setUserData ( GCHandle . ToIntPtr ( timelineInfoHandle ) ) ;
188+
189+ Instance . setCallback ( timelineEventCallback , EVENT_CALLBACK_TYPE . TIMELINE_MARKER ) ;
190+ // -----------------------------------------------------------------------------------------------------
191+ }
192+ else if ( hasRegisteredTimelineMarkerReachedCallback && ! shouldRegisterTimelineMarkerReachedCallback )
193+ {
194+ Instance . setCallback ( null , EVENT_CALLBACK_TYPE . TIMELINE_MARKER ) ;
195+ Instance . setUserData ( IntPtr . Zero ) ;
196+ timelineInfoHandle . Free ( ) ;
197+ }
198+
199+ hasRegisteredTimelineMarkerReachedCallback = shouldRegisterTimelineMarkerReachedCallback ;
200+ }
120201
121202 /// <summary>
122203 /// Fluid method for subscribing to timeline events so you don't have to save the playback to a variable first
@@ -138,14 +219,65 @@ public FmodAudioPlayback UnsubscribeFromTimelineMarkerReachedEvent(TimelineMarke
138219 return this ;
139220 }
140221
141- private RESULT OnTimelineMarkerReached ( EVENT_CALLBACK_TYPE type , IntPtr @event , IntPtr parameterPtr )
222+ // -------------------------------------------------------------------------------------------------------------
223+ // Awful FMOD timeline callback code. Do not change it too much, it is written in this really bizarre way
224+ // for very complicated and undocumented reasons and changing small things is likely to break things.
225+ [ AOT . MonoPInvokeCallback ( typeof ( FMOD . Studio . EVENT_CALLBACK ) ) ]
226+ private static RESULT OnTimelineMarkerReached ( EVENT_CALLBACK_TYPE type , IntPtr instancePtr , IntPtr parameterPtr )
142227 {
143- TIMELINE_MARKER_PROPERTIES parameter = ( TIMELINE_MARKER_PROPERTIES ) Marshal . PtrToStructure (
144- parameterPtr , typeof ( TIMELINE_MARKER_PROPERTIES ) ) ;
228+ EventInstance instance = new ( instancePtr ) ;
229+
230+ // Retrieve the user data
231+ IntPtr timelineInfoPtr ;
232+ RESULT result = instance . getUserData ( out timelineInfoPtr ) ;
233+ if ( result != RESULT . OK )
234+ {
235+ Debug . LogError ( "Timeline Callback error: " + result ) ;
236+ return RESULT . OK ;
237+ }
238+
239+ if ( timelineInfoPtr == IntPtr . Zero )
240+ {
241+ Debug . LogError ( "Bogus Timeline Marker reached: timeline info pointer was NULL." ) ;
242+ return RESULT . OK ;
243+ }
244+
245+ // Get the object to store beat and marker details
246+ GCHandle timelineHandle = GCHandle . FromIntPtr ( timelineInfoPtr ) ;
247+ TimelineInfo timelineInfo = ( TimelineInfo ) timelineHandle . Target ;
248+
249+ if ( timelineInfo == null )
250+ {
251+ Debug . LogError ( "Bogus Timeline Marker reached: timeline handle could not be cast to timeline handle." ) ;
252+ return RESULT . OK ;
253+ }
254+
255+ string id = string . Empty ;
256+ switch ( type )
257+ {
258+ // Unused
259+ //case FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_BEAT:
260+
261+ case FMOD . Studio . EVENT_CALLBACK_TYPE . TIMELINE_MARKER :
262+ {
263+ TIMELINE_MARKER_PROPERTIES parameter = ( FMOD . Studio . TIMELINE_MARKER_PROPERTIES ) Marshal
264+ . PtrToStructure ( parameterPtr , typeof ( FMOD . Studio . TIMELINE_MARKER_PROPERTIES ) ) ;
265+
266+ id = parameter . name ;
267+ break ;
268+ }
269+
270+ case FMOD . Studio . EVENT_CALLBACK_TYPE . DESTROYED :
271+ // Now the event has been destroyed, unpin the timeline memory so it can be garbage collected
272+ timelineHandle . Free ( ) ;
273+ break ;
274+ }
145275
146- timelineMarkerReachedEvent ? . Invoke ( this , parameter . name ) ;
276+ // Buffer the timeline marker event so we can handle it on the main thread.
277+ timelineInfo . TimelineMarkersReached . Enqueue ( id ) ;
147278
148279 return RESULT . OK ;
149280 }
281+ // -------------------------------------------------------------------------------------------------------------
150282 }
151283}
0 commit comments