diff --git a/.gitmodules b/.gitmodules index 481755f0..e323a49f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/WellspringCS"] path = lib/WellspringCS url = https://gitea.moonside.games/MoonsideGames/WellspringCS.git +[submodule "lib/Theorafile"] + path = lib/Theorafile + url = https://github.com/FNA-XNA/Theorafile.git diff --git a/MoonWorks.csproj b/MoonWorks.csproj index 88105d4b..31c1fce8 100644 --- a/MoonWorks.csproj +++ b/MoonWorks.csproj @@ -15,6 +15,7 @@ + @@ -22,4 +23,13 @@ PreserveNewest + + + + MoonWorks.Shaders.FullscreenVert.spv + + + MoonWorks.Shaders.YUV2RGBAFrag.spv + + diff --git a/MoonWorks.dll.config b/MoonWorks.dll.config index d384162a..c0764852 100644 --- a/MoonWorks.dll.config +++ b/MoonWorks.dll.config @@ -15,4 +15,8 @@ + + + + diff --git a/lib/Theorafile b/lib/Theorafile new file mode 160000 index 00000000..dd8c7fa6 --- /dev/null +++ b/lib/Theorafile @@ -0,0 +1 @@ +Subproject commit dd8c7fa69e678b6182cdaa71458ad08dd31c65da diff --git a/src/Audio/SoundInstance.cs b/src/Audio/SoundInstance.cs index 03a09ec4..caba229b 100644 --- a/src/Audio/SoundInstance.cs +++ b/src/Audio/SoundInstance.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using MoonWorks.Math.Float; namespace MoonWorks.Audio { @@ -8,13 +7,12 @@ namespace MoonWorks.Audio { internal IntPtr Handle; internal FAudio.FAudioWaveFormatEx Format; - public bool Loop { get; protected set; } = false; protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; public bool Is3D { get; protected set; } - public abstract SoundState State { get; protected set; } + public virtual SoundState State { get; protected set; } private float _pan = 0; public float Pan @@ -238,11 +236,10 @@ namespace MoonWorks.Audio ); } - public abstract void Play(bool loop); + public abstract void Play(); public abstract void Pause(); - public abstract void Stop(bool immediate); - public abstract void Seek(float seconds); - public abstract void Seek(uint sampleFrame); + public abstract void Stop(); + public abstract void StopImmediate(); private void InitDSPSettings(uint srcChannels) { @@ -345,8 +342,7 @@ namespace MoonWorks.Audio protected override void Destroy() { - Stop(true); - + StopImmediate(); FAudio.FAudioVoice_DestroyVoice(Handle); Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients); } diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs index 1afbb0d2..7f52a978 100644 --- a/src/Audio/StaticSoundInstance.cs +++ b/src/Audio/StaticSoundInstance.cs @@ -6,6 +6,8 @@ namespace MoonWorks.Audio { public StaticSound Parent { get; } + public bool Loop { get; set; } + private SoundState _state = SoundState.Stopped; public override SoundState State { @@ -18,7 +20,7 @@ namespace MoonWorks.Audio ); if (state.BuffersQueued == 0) { - Stop(true); + StopImmediate(); } return _state; @@ -38,15 +40,13 @@ namespace MoonWorks.Audio Parent = parent; } - public override void Play(bool loop = false) + public override void Play() { if (State == SoundState.Playing) { return; } - Loop = loop; - if (Loop) { Parent.Handle.LoopCount = 255; @@ -79,21 +79,20 @@ namespace MoonWorks.Audio } } - public override void Stop(bool immediate = true) + public override void Stop() { - if (immediate) - { - FAudio.FAudioSourceVoice_Stop(Handle, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); - State = SoundState.Stopped; - } - else - { - FAudio.FAudioSourceVoice_ExitLoop(Handle, 0); - } + FAudio.FAudioSourceVoice_ExitLoop(Handle, 0); + State = SoundState.Stopped; } - private void PerformSeek(uint sampleFrame) + public override void StopImmediate() + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, 0); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + State = SoundState.Stopped; + } + + public void Seek(uint sampleFrame) { if (State == SoundState.Playing) { @@ -102,20 +101,6 @@ namespace MoonWorks.Audio } Parent.Handle.PlayBegin = sampleFrame; - Play(); - } - - public override void Seek(float seconds) - { - uint sampleFrame = - (uint) (Parent.SamplesPerSecond * seconds); - - PerformSeek(sampleFrame); - } - - public override void Seek(uint sampleFrame) - { - PerformSeek(sampleFrame); } public void Free() diff --git a/src/Audio/StreamingSound.cs b/src/Audio/StreamingSound.cs index 18a5a33e..49825446 100644 --- a/src/Audio/StreamingSound.cs +++ b/src/Audio/StreamingSound.cs @@ -1,38 +1,46 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; namespace MoonWorks.Audio { /// /// For streaming long playback. - /// Can be extended to support custom decoders. + /// Must be extended with a decoder routine called by FillBuffer. + /// See StreamingSoundOgg for an example. /// public abstract class StreamingSound : SoundInstance { - private readonly List queuedBuffers = new List(); - private readonly List queuedSizes = new List(); - private const int MINIMUM_BUFFER_CHECK = 3; + private const int BUFFER_COUNT = 3; + private readonly IntPtr[] buffers; + private int nextBufferIndex = 0; + private uint queuedBufferCount = 0; + protected abstract int BUFFER_SIZE { get; } - public int PendingBufferCount => queuedBuffers.Count; - - public StreamingSound( + public unsafe StreamingSound( AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond - ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) { } + ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) + { + device.AddDynamicSoundInstance(this); - public override void Play(bool loop = false) + buffers = new IntPtr[BUFFER_COUNT]; + for (int i = 0; i < BUFFER_COUNT; i += 1) + { + buffers[i] = (IntPtr) NativeMemory.Alloc((nuint) BUFFER_SIZE); + } + } + + public override void Play() { if (State == SoundState.Playing) { return; } - Loop = loop; State = SoundState.Playing; Update(); @@ -48,19 +56,21 @@ namespace MoonWorks.Audio } } - public override void Stop(bool immediate = true) + public override void Stop() { - if (immediate) - { - FAudio.FAudioSourceVoice_Stop(Handle, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); - ClearBuffers(); - } + State = SoundState.Stopped; + } + + public override void StopImmediate() + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, 0); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + ClearBuffers(); State = SoundState.Stopped; } - internal void Update() + internal unsafe void Update() { if (State != SoundState.Playing) { @@ -73,109 +83,83 @@ namespace MoonWorks.Audio FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED ); - while (PendingBufferCount > state.BuffersQueued) - lock (queuedBuffers) - { - Marshal.FreeHGlobal(queuedBuffers[0]); - queuedBuffers.RemoveAt(0); - } + queuedBufferCount = state.BuffersQueued; QueueBuffers(); } protected void QueueBuffers() { - for ( - int i = MINIMUM_BUFFER_CHECK - PendingBufferCount; - i > 0; - i -= 1 - ) + for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1) { AddBuffer(); } } - protected void ClearBuffers() + protected unsafe void ClearBuffers() { - lock (queuedBuffers) - { - foreach (IntPtr buf in queuedBuffers) - { - Marshal.FreeHGlobal(buf); - } - queuedBuffers.Clear(); - queuedSizes.Clear(); - } + nextBufferIndex = 0; + queuedBufferCount = 0; } - protected void AddBuffer() + protected unsafe void AddBuffer() { - AddBuffer( - out var buffer, - out var bufferOffset, - out var bufferLength, - out var reachedEnd + var buffer = buffers[nextBufferIndex]; + nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; + + FillBuffer( + (void*) buffer, + BUFFER_SIZE, + out int filledLengthInBytes, + out bool reachedEnd ); - var lengthInBytes = bufferLength * sizeof(float); - - IntPtr next = Marshal.AllocHGlobal((int) lengthInBytes); - Marshal.Copy(buffer, (int) bufferOffset, next, (int) bufferLength); - - lock (queuedBuffers) + FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer { - queuedBuffers.Add(next); - if (State != SoundState.Stopped) - { - FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer - { - AudioBytes = lengthInBytes, - pAudioData = next, - PlayLength = ( - lengthInBytes / - Format.nChannels / - (uint) (Format.wBitsPerSample / 8) - ) - }; + AudioBytes = (uint) filledLengthInBytes, + pAudioData = (IntPtr) buffer, + PlayLength = ( + (uint) (filledLengthInBytes / + Format.nChannels / + (uint) (Format.wBitsPerSample / 8)) + ) + }; - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Handle, - ref buf, - IntPtr.Zero - ); - } - else - { - queuedSizes.Add(lengthInBytes); - } - } + FAudio.FAudioSourceVoice_SubmitSourceBuffer( + Handle, + ref buf, + IntPtr.Zero + ); + + queuedBufferCount += 1; /* We have reached the end of the file, what do we do? */ if (reachedEnd) { - if (Loop) - { - SeekStart(); - } - else - { - Stop(false); - } + OnReachedEnd(); } } - protected abstract void AddBuffer( - out float[] buffer, - out uint bufferOffset, /* in floats */ - out uint bufferLength, /* in floats */ + protected virtual void OnReachedEnd() + { + Stop(); + } + + protected unsafe abstract void FillBuffer( + void* buffer, + int bufferLengthInBytes, /* in bytes */ + out int filledLengthInBytes, /* in bytes */ out bool reachedEnd ); - protected abstract void SeekStart(); - - protected override void Destroy() + protected unsafe override void Destroy() { - Stop(true); + StopImmediate(); + + for (int i = 0; i < BUFFER_COUNT; i += 1) + { + NativeMemory.Free((void*) buffers[i]); + } } } } diff --git a/src/Audio/StreamingSoundOgg.cs b/src/Audio/StreamingSoundOgg.cs index 8aa4721d..1b44e55b 100644 --- a/src/Audio/StreamingSoundOgg.cs +++ b/src/Audio/StreamingSoundOgg.cs @@ -4,28 +4,23 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { - public class StreamingSoundOgg : StreamingSound + public class StreamingSoundOgg : StreamingSoundSeekable { - // FIXME: what should this value be? - public const int BUFFER_SIZE = 1024 * 128; - private IntPtr VorbisHandle; private IntPtr FileDataPtr; private FAudio.stb_vorbis_info Info; - private readonly float[] buffer; // currently decoded bytes + protected override int BUFFER_SIZE => 32768; - public override SoundState State { get; protected set; } - - public static StreamingSoundOgg Load(AudioDevice device, string filePath) + public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath) { var fileData = File.ReadAllBytes(filePath); - var fileDataPtr = Marshal.AllocHGlobal(fileData.Length); - Marshal.Copy(fileData, 0, fileDataPtr, fileData.Length); - var vorbisHandle = FAudio.stb_vorbis_open_memory(fileDataPtr, fileData.Length, out int error, IntPtr.Zero); + var fileDataPtr = NativeMemory.Alloc((nuint) fileData.Length); + Marshal.Copy(fileData, 0, (IntPtr) fileDataPtr, fileData.Length); + var vorbisHandle = FAudio.stb_vorbis_open_memory((IntPtr) fileDataPtr, fileData.Length, out int error, IntPtr.Zero); if (error != 0) { - ((GCHandle) fileDataPtr).Free(); + NativeMemory.Free(fileDataPtr); Logger.LogError("Error opening OGG file!"); Logger.LogError("Error: " + error); throw new AudioLoadException("Error opening OGG file!"); @@ -34,7 +29,7 @@ namespace MoonWorks.Audio return new StreamingSoundOgg( device, - fileDataPtr, + (IntPtr) fileDataPtr, vorbisHandle, info ); @@ -42,7 +37,7 @@ namespace MoonWorks.Audio internal StreamingSoundOgg( AudioDevice device, - IntPtr fileDataPtr, // MUST BE AN ALLOCHGLOBAL HANDLE!! + IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!! IntPtr vorbisHandle, FAudio.stb_vorbis_info info ) : base( @@ -57,12 +52,9 @@ namespace MoonWorks.Audio FileDataPtr = fileDataPtr; VorbisHandle = vorbisHandle; Info = info; - buffer = new float[BUFFER_SIZE]; - - device.AddDynamicSoundInstance(this); } - private void PerformSeek(uint sampleFrame) + public override void Seek(uint sampleFrame) { if (State == SoundState.Playing) { @@ -80,49 +72,32 @@ namespace MoonWorks.Audio } } - public override void Seek(float seconds) - { - uint sampleFrame = (uint) (Info.sample_rate * seconds); - PerformSeek(sampleFrame); - } - - public override void Seek(uint sampleFrame) - { - PerformSeek(sampleFrame); - } - - protected override void AddBuffer( - out float[] buffer, - out uint bufferOffset, - out uint bufferLength, + protected unsafe override void FillBuffer( + void* buffer, + int bufferLengthInBytes, + out int filledLengthInBytes, out bool reachedEnd ) { - buffer = this.buffer; + var lengthInFloats = bufferLengthInBytes / sizeof(float); /* NOTE: this function returns samples per channel, not total samples */ var samples = FAudio.stb_vorbis_get_samples_float_interleaved( VorbisHandle, Info.channels, - buffer, - buffer.Length + (IntPtr) buffer, + lengthInFloats ); var sampleCount = samples * Info.channels; - bufferOffset = 0; - bufferLength = (uint) sampleCount; - reachedEnd = sampleCount < buffer.Length; + reachedEnd = sampleCount < lengthInFloats; + filledLengthInBytes = sampleCount * sizeof(float); } - protected override void SeekStart() - { - FAudio.stb_vorbis_seek_start(VorbisHandle); - } - - protected override void Destroy() + protected unsafe override void Destroy() { FAudio.stb_vorbis_close(VorbisHandle); - Marshal.FreeHGlobal(FileDataPtr); + NativeMemory.Free((void*) FileDataPtr); } } } diff --git a/src/Audio/StreamingSoundSeekable.cs b/src/Audio/StreamingSoundSeekable.cs new file mode 100644 index 00000000..7ca563b6 --- /dev/null +++ b/src/Audio/StreamingSoundSeekable.cs @@ -0,0 +1,25 @@ +namespace MoonWorks.Audio +{ + public abstract class StreamingSoundSeekable : StreamingSound + { + public bool Loop { get; set; } + + protected StreamingSoundSeekable(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) + { + } + + public abstract void Seek(uint sampleFrame); + + protected override void OnReachedEnd() + { + if (Loop) + { + Seek(0); + } + else + { + Stop(); + } + } + } +} diff --git a/src/Graphics/CommandBuffer.cs b/src/Graphics/CommandBuffer.cs index bcd698ad..598d50b2 100644 --- a/src/Graphics/CommandBuffer.cs +++ b/src/Graphics/CommandBuffer.cs @@ -1,6 +1,4 @@ using System; -using System.Runtime.InteropServices; -using MoonWorks.Math; using RefreshCS; namespace MoonWorks.Graphics @@ -835,6 +833,26 @@ namespace MoonWorks.Graphics SetTextureData(new TextureSlice(texture), dataPtr, dataLengthInBytes); } + /// + /// Asynchronously copies YUV data into three textures. Use with compressed video. + /// + public void SetTextureDataYUV(Texture yTexture, Texture uTexture, Texture vTexture, IntPtr dataPtr, uint dataLengthInBytes) + { + Refresh.Refresh_SetTextureDataYUV( + Device.Handle, + Handle, + yTexture.Handle, + uTexture.Handle, + vTexture.Handle, + yTexture.Width, + yTexture.Height, + uTexture.Width, + uTexture.Height, + dataPtr, + dataLengthInBytes + ); + } + /// /// Performs an asynchronous texture-to-texture copy on the GPU. /// diff --git a/src/Graphics/GraphicsDevice.cs b/src/Graphics/GraphicsDevice.cs index 99915b47..c111c9e2 100644 --- a/src/Graphics/GraphicsDevice.cs +++ b/src/Graphics/GraphicsDevice.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using RefreshCS; namespace MoonWorks.Graphics @@ -8,6 +9,11 @@ namespace MoonWorks.Graphics { public IntPtr Handle { get; } + // Built-in video pipeline + private ShaderModule VideoVertexShader { get; } + private ShaderModule VideoFragmentShader { get; } + internal GraphicsPipeline VideoPipeline { get; } + public bool IsDisposed { get; private set; } private readonly List> resources = new List>(); @@ -28,6 +34,26 @@ namespace MoonWorks.Graphics presentationParameters, Conversions.BoolToByte(debugMode) ); + + VideoVertexShader = new ShaderModule(this, GetEmbeddedResource("MoonWorks.Shaders.FullscreenVert.spv")); + VideoFragmentShader = new ShaderModule(this, GetEmbeddedResource("MoonWorks.Shaders.YUV2RGBAFrag.spv")); + + VideoPipeline = new GraphicsPipeline( + this, + new GraphicsPipelineCreateInfo + { + AttachmentInfo = new GraphicsPipelineAttachmentInfo( + new ColorAttachmentDescription(TextureFormat.R8G8B8A8, ColorAttachmentBlendState.None) + ), + DepthStencilState = DepthStencilState.Disable, + VertexShaderInfo = GraphicsShaderInfo.Create(VideoVertexShader, "main", 0), + FragmentShaderInfo = GraphicsShaderInfo.Create(VideoFragmentShader, "main", 3), + VertexInputState = VertexInputState.Empty, + RasterizerState = RasterizerState.CCW_CullNone, + PrimitiveType = PrimitiveType.TriangleList, + MultisampleState = MultisampleState.None + } + ); } public CommandBuffer AcquireCommandBuffer() @@ -77,6 +103,11 @@ namespace MoonWorks.Graphics } } + private static Stream GetEmbeddedResource(string name) + { + return typeof(GraphicsDevice).Assembly.GetManifestResourceStream(name); + } + protected virtual void Dispose(bool disposing) { if (!IsDisposed) diff --git a/src/Input/Gamepad.cs b/src/Input/Gamepad.cs index e6d15d9a..7b53b3f9 100644 --- a/src/Input/Gamepad.cs +++ b/src/Input/Gamepad.cs @@ -159,6 +159,12 @@ namespace MoonWorks.Input { AxisButtonCode.RightY_Down, RightYDown } }; + TriggerCodeToTriggerButton = new Dictionary + { + { TriggerCode.Left, TriggerLeftButton }, + { TriggerCode.Right, TriggerRightButton } + }; + VirtualButtons = new VirtualButton[] { A, diff --git a/src/MoonWorksDllMap.cs b/src/MoonWorksDllMap.cs index 5164838e..fd9b0f39 100644 --- a/src/MoonWorksDllMap.cs +++ b/src/MoonWorksDllMap.cs @@ -196,6 +196,7 @@ namespace MoonWorks NativeLibrary.SetDllImportResolver(typeof(RefreshCS.Refresh).Assembly, MapAndLoad); NativeLibrary.SetDllImportResolver(typeof(FAudio).Assembly, MapAndLoad); NativeLibrary.SetDllImportResolver(typeof(WellspringCS.Wellspring).Assembly, MapAndLoad); + NativeLibrary.SetDllImportResolver(typeof(Theorafile).Assembly, MapAndLoad); } #endregion diff --git a/src/Video/Shaders/Compiled/FullscreenVert.spv b/src/Video/Shaders/Compiled/FullscreenVert.spv new file mode 100644 index 00000000..ffc57de4 Binary files /dev/null and b/src/Video/Shaders/Compiled/FullscreenVert.spv differ diff --git a/src/Video/Shaders/Compiled/YUV2RGBAFrag.spv b/src/Video/Shaders/Compiled/YUV2RGBAFrag.spv new file mode 100644 index 00000000..c9fbf324 Binary files /dev/null and b/src/Video/Shaders/Compiled/YUV2RGBAFrag.spv differ diff --git a/src/Video/Shaders/Source/Fullscreen.vert b/src/Video/Shaders/Source/Fullscreen.vert new file mode 100644 index 00000000..2f3c3154 --- /dev/null +++ b/src/Video/Shaders/Source/Fullscreen.vert @@ -0,0 +1,9 @@ +#version 450 + +layout(location = 0) out vec2 outTexCoord; + +void main() +{ + outTexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outTexCoord * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/src/Video/Shaders/Source/YUV2RGBA.frag b/src/Video/Shaders/Source/YUV2RGBA.frag new file mode 100644 index 00000000..fe2b5a16 --- /dev/null +++ b/src/Video/Shaders/Source/YUV2RGBA.frag @@ -0,0 +1,38 @@ +/* + * This effect is based on the YUV-to-RGBA GLSL shader found in SDL. + * Thus, it also released under the zlib license: + * http://libsdl.org/license.php + */ +#version 450 + +layout(location = 0) in vec2 TexCoord; + +layout(location = 0) out vec4 FragColor; + +layout(binding = 0, set = 1) uniform sampler2D YSampler; +layout(binding = 1, set = 1) uniform sampler2D USampler; +layout(binding = 2, set = 1) uniform sampler2D VSampler; + +/* More info about colorspace conversion: + * http://www.equasys.de/colorconversion.html + * http://www.equasys.de/colorformat.html + */ + +const vec3 offset = vec3(-0.0625, -0.5, -0.5); +const vec3 Rcoeff = vec3(1.164, 0.000, 1.793); +const vec3 Gcoeff = vec3(1.164, -0.213, -0.533); +const vec3 Bcoeff = vec3(1.164, 2.112, 0.000); + +void main() +{ + vec3 yuv; + yuv.x = texture(YSampler, TexCoord).r; + yuv.y = texture(USampler, TexCoord).r; + yuv.z = texture(VSampler, TexCoord).r; + yuv += offset; + + FragColor.r = dot(yuv, Rcoeff); + FragColor.g = dot(yuv, Gcoeff); + FragColor.b = dot(yuv, Bcoeff); + FragColor.a = 1.0; +} diff --git a/src/Video/StreamingSoundTheora.cs b/src/Video/StreamingSoundTheora.cs new file mode 100644 index 00000000..a49c939c --- /dev/null +++ b/src/Video/StreamingSoundTheora.cs @@ -0,0 +1,45 @@ +using System; +using MoonWorks.Audio; + +namespace MoonWorks.Video +{ + public unsafe class StreamingSoundTheora : StreamingSound + { + private IntPtr VideoHandle; + protected override int BUFFER_SIZE => 8192; + + internal StreamingSoundTheora( + AudioDevice device, + IntPtr videoHandle, + int channels, + uint sampleRate + ) : base( + device, + 3, /* float type */ + 32, /* size of float */ + (ushort) (4 * channels), + (ushort) channels, + sampleRate + ) { + VideoHandle = videoHandle; + } + + protected override unsafe void FillBuffer( + void* buffer, + int bufferLengthInBytes, + out int filledLengthInBytes, + out bool reachedEnd + ) { + var lengthInFloats = bufferLengthInBytes / sizeof(float); + + int samples = Theorafile.tf_readaudio( + VideoHandle, + (IntPtr) buffer, + lengthInFloats + ); + + filledLengthInBytes = samples * sizeof(float); + reachedEnd = Theorafile.tf_eos(VideoHandle) == 1; + } + } +} diff --git a/src/Video/Video.cs b/src/Video/Video.cs new file mode 100644 index 00000000..01dba5b2 --- /dev/null +++ b/src/Video/Video.cs @@ -0,0 +1,357 @@ +/* Heavily based on https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/VideoPlayer.cs */ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using MoonWorks.Audio; +using MoonWorks.Graphics; + +namespace MoonWorks.Video +{ + public enum VideoState + { + Playing, + Paused, + Stopped + } + + public unsafe class Video : IDisposable + { + internal IntPtr Handle; + + public bool Loop { get; private set; } + public float Volume { + get => volume; + set + { + volume = value; + if (audioStream != null) + { + audioStream.Volume = value; + } + } + } + public float PlaybackSpeed { get; set; } + public double FramesPerSecond => fps; + private VideoState State = VideoState.Stopped; + + private double fps; + private int yWidth; + private int yHeight; + private int uvWidth; + private int uvHeight; + + private void* yuvData = null; + private int yuvDataLength; + private int currentFrame; + + private GraphicsDevice GraphicsDevice; + private Texture RenderTexture = null; + private Texture yTexture = null; + private Texture uTexture = null; + private Texture vTexture = null; + private Sampler LinearSampler; + + private AudioDevice AudioDevice = null; + private StreamingSoundTheora audioStream = null; + private float volume = 1.0f; + + private Stopwatch timer; + private double lastTimestamp; + private double timeElapsed; + + private bool disposed; + + /* TODO: is there some way for us to load the data into memory? */ + public Video(GraphicsDevice graphicsDevice, AudioDevice audioDevice, string filename) + { + GraphicsDevice = graphicsDevice; + AudioDevice = audioDevice; + + if (!System.IO.File.Exists(filename)) + { + throw new ArgumentException("Video file not found!"); + } + + if (Theorafile.tf_fopen(filename, out Handle) < 0) + { + throw new ArgumentException("Invalid video file!"); + } + + Theorafile.th_pixel_fmt format; + Theorafile.tf_videoinfo( + Handle, + out yWidth, + out yHeight, + out fps, + out format + ); + + if (format == Theorafile.th_pixel_fmt.TH_PF_420) + { + uvWidth = yWidth / 2; + uvHeight = yHeight / 2; + } + else if (format == Theorafile.th_pixel_fmt.TH_PF_422) + { + uvWidth = yWidth / 2; + uvHeight = yHeight; + } + else if (format == Theorafile.th_pixel_fmt.TH_PF_444) + { + uvWidth = yWidth; + uvHeight = yHeight; + } + else + { + throw new NotSupportedException("Unrecognized YUV format!"); + } + + yuvDataLength = ( + (yWidth * yHeight) + + (uvWidth * uvHeight * 2) + ); + + yuvData = NativeMemory.Alloc((nuint) yuvDataLength); + + InitializeTheoraStream(); + + if (Theorafile.tf_hasvideo(Handle) == 1) + { + RenderTexture = Texture.CreateTexture2D( + GraphicsDevice, + (uint) yWidth, + (uint) yHeight, + TextureFormat.R8G8B8A8, + TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler + ); + + yTexture = Texture.CreateTexture2D( + GraphicsDevice, + (uint) yWidth, + (uint) yHeight, + TextureFormat.R8, + TextureUsageFlags.Sampler + ); + + uTexture = Texture.CreateTexture2D( + GraphicsDevice, + (uint) uvWidth, + (uint) uvHeight, + TextureFormat.R8, + TextureUsageFlags.Sampler + ); + + vTexture = Texture.CreateTexture2D( + GraphicsDevice, + (uint) uvWidth, + (uint) uvHeight, + TextureFormat.R8, + TextureUsageFlags.Sampler + ); + + LinearSampler = new Sampler(GraphicsDevice, SamplerCreateInfo.LinearClamp); + } + + timer = new Stopwatch(); + } + + public void Play(bool loop = false) + { + if (State == VideoState.Playing) + { + return; + } + + Loop = loop; + timer.Start(); + + if (audioStream != null) + { + audioStream.Play(); + } + + State = VideoState.Playing; + } + + public void Pause() + { + if (State != VideoState.Playing) + { + return; + } + + timer.Stop(); + + if (audioStream != null) + { + audioStream.Pause(); + } + + State = VideoState.Paused; + } + + public void Stop() + { + if (State == VideoState.Stopped) + { + return; + } + + timer.Stop(); + timer.Reset(); + + Theorafile.tf_reset(Handle); + lastTimestamp = 0; + timeElapsed = 0; + + if (audioStream != null) + { + audioStream.StopImmediate(); + audioStream.Dispose(); + audioStream = null; + } + + State = VideoState.Stopped; + } + + public Texture GetTexture() + { + if (RenderTexture == null) + { + throw new InvalidOperationException(); + } + + if (State == VideoState.Stopped) + { + return RenderTexture; + } + + timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed; + lastTimestamp = timer.Elapsed.TotalMilliseconds; + + int thisFrame = ((int) (timeElapsed / (1000.0 / FramesPerSecond))); + if (thisFrame > currentFrame) + { + if (Theorafile.tf_readvideo( + Handle, + (IntPtr) yuvData, + thisFrame - currentFrame + ) == 1 || currentFrame == -1) { + UpdateTexture(); + } + + currentFrame = thisFrame; + } + + bool ended = Theorafile.tf_eos(Handle) == 1; + if (ended) + { + timer.Stop(); + timer.Reset(); + + if (audioStream != null) + { + audioStream.Stop(); + audioStream.Dispose(); + audioStream = null; + } + + Theorafile.tf_reset(Handle); + + if (Loop) + { + // Start over! + InitializeTheoraStream(); + + timer.Start(); + } + else + { + State = VideoState.Stopped; + } + } + + return RenderTexture; + } + + private void UpdateTexture() + { + var commandBuffer = GraphicsDevice.AcquireCommandBuffer(); + + commandBuffer.SetTextureDataYUV( + yTexture, + uTexture, + vTexture, + (IntPtr) yuvData, + (uint) yuvDataLength + ); + + commandBuffer.BeginRenderPass( + new ColorAttachmentInfo(RenderTexture, Color.Black) + ); + + commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline); + commandBuffer.BindFragmentSamplers( + new TextureSamplerBinding(yTexture, LinearSampler), + new TextureSamplerBinding(uTexture, LinearSampler), + new TextureSamplerBinding(vTexture, LinearSampler) + ); + + commandBuffer.DrawPrimitives(0, 1, 0, 0); + + commandBuffer.EndRenderPass(); + + GraphicsDevice.Submit(commandBuffer); + } + + private void InitializeTheoraStream() + { + // Grab the first video frame ASAP. + while (Theorafile.tf_readvideo(Handle, (IntPtr) yuvData, 1) == 0); + + // Grab the first bit of audio. We're trying to start the decoding ASAP. + if (AudioDevice != null && Theorafile.tf_hasaudio(Handle) == 1) + { + int channels, sampleRate; + Theorafile.tf_audioinfo(Handle, out channels, out sampleRate); + audioStream = new StreamingSoundTheora(AudioDevice, Handle, channels, (uint) sampleRate); + } + + currentFrame = -1; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + // dispose managed state (managed objects) + RenderTexture.Dispose(); + yTexture.Dispose(); + uTexture.Dispose(); + vTexture.Dispose(); + } + + // free unmanaged resources (unmanaged objects) + Theorafile.tf_close(ref Handle); + NativeMemory.Free(yuvData); + + disposed = true; + } + } + + ~Video() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +}