Through browsing through the forums, I have seen that GetTimelinePosition() is not accurate and a possible more accurate solution is using DSPClock which I can get through:
eventInstance.getChannelGroup(out FMOD.ChannelGroup channelGroup);
channelGroup.getDSPClock(out ulong DSPClock, out ulong parent);
However I can’t wrap my head on how I’ll be able to get the current time through that as the number is large.
I read from here: Get Playback Time . That you will need to compare it to a value retrieved earlier in time (i.e. when the event instance was started. So I tried retrieving the DPS clock when my event started then retrieved it in update minus the initial but the number still doesn’t resemble time.
Can anyone help me with an example code on how to do it right? For my use case I’ll need to retrieve the current time of the event in the update loop. Haven’t seen actual code snippet on how to properly use it. Thanks in advance!
getDSPClock returns the time in samples, so to get seconds you need to divide it by your sample rate. It should just be something like:
eventInstance.getChannelGroup(out FMOD.ChannelGroup channelGroup);
channelGroup.getDSPClock(out ulong DSPClock, out ulong parent);
FMODUnity.RuntimeManager.CoreSystem.getSoftwareFormat(out int samplerate, out SPEAKERMODE speakermode, out int numrawspeakers);
float timeSeconds = DSPClock / (float)samplerate;
Here’s my solution for an event instance that is going to be paused in runtime:
Using DSP Clock needs cached values so i minimalized caching as much as possible.
A more accurate solution would be using the eventInstance.setCallback method. That way the channelgroup is valid and you can get the DSP Clock IN the pinpoint accuracy of event.
private void UpdateSongEmitterDSPClockStart()
{
RuntimeManager.CoreSystem.getSoftwareFormat(out int sampleRate, out _, out _);
EventInstance instance = m_SongEmitterRef.EventInstance;
if (m_SongEmitterDSPClockStart == 0) // ChannelGroup invalid in STARTING state. So in the next frame, without caching the playback state, you will gather the DSP clock when the song started again.
{
instance.getChannelGroup(out ChannelGroup channelGroup);
instance.getTimelinePosition(out int timelinePositionInMilliseconds);
channelGroup.getDSPClock(out ulong DSPClock, out _);
ulong timelinePositionInSeconds = ((ulong)timelinePositionInMilliseconds / 1000);
ulong timelineDSPClock = timelinePositionInSeconds * (ulong)sampleRate;
m_SongEmitterDSPClockStart = (DSPClock - timelineDSPClock);
}
instance.getPlaybackState(out PLAYBACK_STATE playbackState);
switch (playbackState)
{
case PLAYBACK_STATE.STARTING:
case PLAYBACK_STATE.STOPPED:
{
m_SongEmitterDSPClockStart = 0;
}
break;
}
}
private float GetSongEmitterTimelinePositionInSeconds()
{
RuntimeManager.CoreSystem.getSoftwareFormat(out int sampleRate, out _, out _);
EventInstance instance = m_SongEmitterRef.EventInstance;
instance.getChannelGroup(out ChannelGroup channelGroup);
channelGroup.getDSPClock(out ulong DSPClock, out _);
UpdateSongEmitterDSPClockStart(); // This method should be removed and be called before this method.
return (DSPClock - m_SongEmitterDSPClockStart) / (float)sampleRate;
}
I was here again. In order to not confuse myself anymore, this is how you could do this:
I would use a static helper for getting accurate timeline position:
using FMOD;
using FMOD.Studio;
public static class EventInstanceTimeline
{
/// <param name="DSPClockStart">
/// Best to cache at event <see cref="EVENT_CALLBACK_TYPE.STARTED"/>
/// </param>
public static void GetDSPClockStart(this EventInstance instance, out ulong DSPClockStart)
{
RuntimeManager.CoreSystem.getSoftwareFormat(out int sampleRate, out _, out _);
instance.getChannelGroup(out ChannelGroup channelGroup);
instance.getTimelinePosition(out int timelineMilliseconds);
channelGroup.getDSPClock(out ulong DSPClock, out _);
float timelineSeconds = (timelineMilliseconds / 1000f);
ulong timelineDSPClock = (ulong)(timelineSeconds * sampleRate);
DSPClockStart = (DSPClock - timelineDSPClock);
}
/// <param name="DSPClockStart">
/// Best to use cached from event <see cref="EVENT_CALLBACK_TYPE.STARTED"/>
/// </param>
public static void GetAccurateTimelinePosition(this EventInstance instance, ulong DSPClockStart, out int timelineMilliseconds)
{
RuntimeManager.CoreSystem.getSoftwareFormat(out int sampleRate, out _, out _);
instance.getChannelGroup(out ChannelGroup channelGroup);
channelGroup.getDSPClock(out ulong DSPClock, out _);
ulong DSPClockDifference = (DSPClock - DSPClockStart);
float timelineSeconds = (DSPClockDifference / (float)sampleRate);
timelineMilliseconds = (int)(timelineSeconds * 1000f);
}
}
I would create an user data to pass around event instances:
public sealed class SongInstanceUserData
{
public ulong DSPClockStart;
}
Lastly, this is how you could call the code:
using FMOD.Studio;
using FMODUnity;
using System.Runtime.InteropServices;
private static readonly EVENT_CALLBACK m_OnCallback = new(OnCallback); // GC wont allow it to disappear
[AOT.MonoPInvokeCallback(typeof(EVENT_CALLBACK))]
private static FMOD.RESULT OnCallback(EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
{
EventInstance instance = new(instancePtr);
instance.getUserData(out IntPtr userDataPtr);
GCHandle userDataHandle = GCHandle.FromIntPtr(userDataPtr);
SongInstanceUserData userData = userDataHandle.Target as SongInstanceUserData;
switch (type)
{
// ...
case EVENT_CALLBACK_TYPE.STARTED:
{
instance.GetDSPClockStart(out userData.DSPClockStart);
break;
}
// ...
}
return FMOD.RESULT.OK;
}
private StudioEventEmitter m_Emitter;
private void PlaySong()
{
SongInstanceUserData userData = new();
StudioEventEmitter emitter = m_Emitter;
emitter.Stop(); // Invalidate instance
emitter.Play(); // Queue refresh instance
emitter.EventInstance.setCallback(m_OnCallback);
emitter.EventInstance.setUserData(GCHandle.ToIntPtr(GCHandle.Alloc(userData)));
}
Of course, you cannot get exact time because the drivers are the providers. But it will be near as possible.