Hi, just wanted to share that I also had a lot of problems with the FMOD video playback sample https://www.fmod.com/docs/2.03/unity/examples-video-playback.html, and have attached a rewrite that fixes these issues:
- Works on macOS (AudioSampleProvider callback doesn’t get called on macOS, presumably a Unity bug, so uses polling in update instead), also tested on Windows.
- Writes to end of write cursor instead of to last read cursor, fixes stuttering playback
- Pauses playback until buffer is filled, fixes stuttering playback
- Buffers through one NativeArray instead of allocating/resizing/copying through multiple C# lists and arrays
- Removed dynamic resampling based on latency measurement, should not be necessary
using Unity.Collections;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Audio;
using UnityEngine.Experimental.Video;
using UnityEngine.Video;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System;
using UnityEngine.UIElements;
using Unity.Collections.LowLevel.Unsafe;
/// <summary>
/// Route video audio through FMOD; based on
/// https://www.fmod.com/docs/2.03/unity/examples-video-playback.html
///
/// - Poll in Update instead of using SampleFramesAvailable event, as the event is not invoked on macOS
/// - Avoid buffering through C# list, rely on AudioSampleProvider's internal buffer and lock/unlock FMOD sound directly
/// with one intermediate temp NativeArray buffer
/// - Pause channel until buffer is filled
/// - Write samples after write cursor instead of after last frame's read cursor to fix stuttering
/// </summary>
public class VideoPlayerAudioPlayer : MonoBehaviour
{
private const int LATENCY_MS = 50;
private VideoPlayer mVideoPlayer;
private AudioSampleProvider mProvider;
private FMOD.CREATESOUNDEXINFO mExinfo;
private FMOD.Channel mChannel;
private FMOD.Sound mSound;
private uint mWriteCursor;
void Start()
{
#if UNITY_EDITOR
EditorApplication.pauseStateChanged += EditorStateChange;
#endif
}
#if UNITY_EDITOR
private void EditorStateChange(PauseState state)
{
if (mChannel.hasHandle())
{
mChannel.setPaused(state == PauseState.Paused);
}
}
#endif
private void OnDestroy()
{
mChannel.stop();
mSound.release();
#if UNITY_EDITOR
EditorApplication.pauseStateChanged -= EditorStateChange;
#endif
}
public void Setup(VideoPlayer videoPlayer)
{
this.mVideoPlayer = videoPlayer;
}
public void OnLoopPointReached(VideoPlayer vp)
{
if (!vp.isLooping)
{
mChannel.setPaused(true);
}
}
public void OnPrepared(VideoPlayer vp)
{
mProvider = vp.GetAudioSampleProvider(0);
int sampleRate = (int)(mProvider.sampleRate * mVideoPlayer.playbackSpeed);
uint targetLatencySamples = (uint)(sampleRate * LATENCY_MS) / 1000;
mExinfo.cbsize = Marshal.SizeOf(typeof(FMOD.CREATESOUNDEXINFO));
mExinfo.numchannels = mProvider.channelCount;
mExinfo.defaultfrequency = sampleRate;
mExinfo.length = targetLatencySamples * (uint)mExinfo.numchannels * sizeof(float);
mExinfo.format = FMOD.SOUND_FORMAT.PCMFLOAT;
FMODUnity.RuntimeManager.CoreSystem.createSound("", FMOD.MODE.LOOP_NORMAL | FMOD.MODE.OPENUSER, ref mExinfo, out mSound);
FMOD.ChannelGroup channelGroup;
FMODUnity.RuntimeManager.CoreSystem.getMasterChannelGroup(out channelGroup);
FMODUnity.RuntimeManager.CoreSystem.playSound(mSound, channelGroup, true, out mChannel);
}
private void Update()
{
if (!mVideoPlayer || !mVideoPlayer.isPrepared)
return;
uint availableSamples = mProvider.availableSampleFrameCount;
if (availableSamples > 0)
{
mChannel.getPosition(out uint readCursorLoop, FMOD.TIMEUNIT.PCMBYTES);
uint writeCursorLoop = mWriteCursor % mExinfo.length;
uint maxWriteBytes = readCursorLoop - writeCursorLoop;
if (readCursorLoop < writeCursorLoop || mWriteCursor == 0)
maxWriteBytes += mExinfo.length;
if (maxWriteBytes > 0)
{
using (var buffer = new NativeArray<byte>((int)maxWriteBytes, Allocator.Temp))
{
uint samples = mProvider.ConsumeSampleFrames(buffer.Reinterpret<float>(1));
uint bytes = samples * (uint)mExinfo.numchannels * sizeof(float);
var res = mSound.@lock(writeCursorLoop, bytes, out IntPtr ptr1, out IntPtr ptr2, out uint lenBytes1, out uint lenBytes2);
if (res != FMOD.RESULT.OK) Debug.LogError(res);
unsafe
{
byte* src = (byte*)buffer.GetUnsafeReadOnlyPtr();
UnsafeUtility.MemCpy((byte*)ptr1, src, lenBytes1);
UnsafeUtility.MemCpy((byte*)ptr2, src + lenBytes1, lenBytes2);
}
res = mSound.unlock(ptr1, ptr2, lenBytes1, lenBytes2);
if (res != FMOD.RESULT.OK) Debug.LogError(res);
mWriteCursor += bytes;
}
}
}
if (mWriteCursor >= mExinfo.length)
mChannel.setPaused(false);
#if UNITY_EDITOR
if (mChannel.hasHandle() && mChannel.getChannelGroup(out var channelGroup) == FMOD.RESULT.OK)
channelGroup.setMute(EditorUtility.audioMasterMute);
#endif
}
}