diff --git a/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs b/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs index c67b55f2b8..6dec20ea26 100644 --- a/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs +++ b/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; @@ -326,9 +327,9 @@ namespace BizHawk.Bizware.Audio _wavDeviceBuffer = null; } - public void PlayWavFile(string path, double volume) + public void PlayWavFile(Stream wavFile, double volume) { - using var wavStream = new SDL2WavStream(path); + using var wavStream = new SDL2WavStream(wavFile); var format = wavStream.Format == SDL2WavStream.AudioFormat.F32LSB ? WaveFormat.CreateIeeeFloatWaveFormat(wavStream.Frequency, wavStream.Channels) : new(wavStream.Frequency, wavStream.BitsPerSample, wavStream.Channels); diff --git a/src/BizHawk.Bizware.Audio/OpenALSoundOutput.cs b/src/BizHawk.Bizware.Audio/OpenALSoundOutput.cs index 0961a08fa4..a33e5b5fdf 100644 --- a/src/BizHawk.Bizware.Audio/OpenALSoundOutput.cs +++ b/src/BizHawk.Bizware.Audio/OpenALSoundOutput.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using BizHawk.Client.Common; @@ -186,9 +187,9 @@ namespace BizHawk.Bizware.Audio } } - public void PlayWavFile(string path, double volume) + public void PlayWavFile(Stream wavFile, double volume) { - using var wavStream = new SDL2WavStream(path); + using var wavStream = new SDL2WavStream(wavFile); if (wavStream.Channels > 2) { throw new NotSupportedException("OpenAL does not support more than 2 channels"); diff --git a/src/BizHawk.Bizware.Audio/SDL2WavStream.cs b/src/BizHawk.Bizware.Audio/SDL2WavStream.cs index ea169e4af6..030b6ece0b 100644 --- a/src/BizHawk.Bizware.Audio/SDL2WavStream.cs +++ b/src/BizHawk.Bizware.Audio/SDL2WavStream.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.IO; +using System.Runtime.InteropServices; using BizHawk.Common; @@ -16,11 +18,11 @@ namespace BizHawk.Bizware.Audio // These are the only formats SDL2's wav loadder will output public enum AudioFormat : ushort { - U8 = 0x0008, - S16LSB = 0x8010, - S32LSB = 0x8020, - F32LSB = 0x8120, - S16MSB = 0x9010, + U8 = AUDIO_U8, + S16LSB = AUDIO_S16LSB, + S32LSB = AUDIO_S32LSB, + F32LSB = AUDIO_F32LSB, + S16MSB = AUDIO_S16MSB, } public int Frequency { get; } @@ -34,12 +36,19 @@ namespace BizHawk.Bizware.Audio AudioFormat.S32LSB or AudioFormat.F32LSB => 32, _ => throw new InvalidOperationException(), }; + + [DllImport("SDL2", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr SDL_LoadWAV_RW( + IntPtr src, + int freesrc, + out SDL_AudioSpec spec, + out IntPtr audio_buf, + out uint audio_len); - public SDL2WavStream(string path) + public SDL2WavStream(Stream wavFile) { - // TODO: Perhaps this should just take a Stream? - // need to update SDL2-CS since the version we're on doesn't expose SDL_LoadWAV_RW :( - if (SDL_LoadWAV(path, out var spec, out var wav, out var len) == IntPtr.Zero) + using var rwOpWrapper = new SDLRwOpsStreamWrapper(wavFile); + if (SDL_LoadWAV_RW(rwOpWrapper.Rw, 0, out var spec, out var wav, out var len) == IntPtr.Zero) { throw new($"Could not load WAV file! SDL error: {SDL_GetError()}"); } @@ -121,5 +130,94 @@ namespace BizHawk.Bizware.Audio public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + private unsafe class SDLRwOpsStreamWrapper : IDisposable + { + public IntPtr Rw { get; private set; } + private readonly Stream _s; + + private readonly SDLRWopsSizeCallback _sizeCallback; + private readonly SDLRWopsSeekCallback _seekCallback; + private readonly SDLRWopsReadCallback _readCallback; + private readonly SDLRWopsWriteCallback _writeCallback; + private readonly SDLRWopsCloseCallback _closeCallback; + + public SDLRwOpsStreamWrapper(Stream s) + { + Rw = SDL_AllocRW(); + if (Rw == IntPtr.Zero) + { + throw new($"Could not allocate SDL_RWops! SDL error: {SDL_GetError()}"); + } + + _s = s; + _sizeCallback = SizeCallback; + _seekCallback = SeekCallback; + _readCallback = ReadCallback; + _writeCallback = WriteCallback; + _closeCallback = CloseCallback; + + var rw = (SDL_RWops*)Rw; + rw->size = Marshal.GetFunctionPointerForDelegate(_sizeCallback); + rw->seek = Marshal.GetFunctionPointerForDelegate(_seekCallback); + rw->read = Marshal.GetFunctionPointerForDelegate(_readCallback); + rw->write = Marshal.GetFunctionPointerForDelegate(_writeCallback); + rw->close = Marshal.GetFunctionPointerForDelegate(_closeCallback); + rw->type = SDL_RWOPS_UNKNOWN; + } + + private long SizeCallback(IntPtr ctx) + => _s.Length; + + private long SeekCallback(IntPtr ctx, long offset, int whence) + => _s.Seek(offset, (SeekOrigin)whence); + + private nint ReadCallback(IntPtr ctx, IntPtr ptr, nint size, nint num) + { + const int TEMP_BUFFER_LENGTH = 65536; + var tempBuffer = ArrayPool.Shared.Rent(TEMP_BUFFER_LENGTH); + try + { + var numBytes = (nuint)size * (nuint)num; + var remainingBytes = numBytes; + while (remainingBytes != 0) + { + var numRead = _s.Read(tempBuffer, 0, (int)Math.Min(remainingBytes, TEMP_BUFFER_LENGTH)); + if (numRead == 0) + { + break; + } + + Marshal.Copy(tempBuffer, 0, ptr, numRead); + ptr += numRead; + remainingBytes -= (uint)numRead; + } + + return (nint)((numBytes - remainingBytes) / (nuint)size); + } + finally + { + ArrayPool.Shared.Return(tempBuffer); + } + } + + private static nint WriteCallback(IntPtr ctx, IntPtr ptr, nint size, nint num) + => 0; + + private int CloseCallback(IntPtr ctx) + { + Dispose(); + return 0; + } + + public void Dispose() + { + if (Rw != IntPtr.Zero) + { + SDL_FreeRW(Rw); + Rw = IntPtr.Zero; + } + } + } } } diff --git a/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs b/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs index cf6888e56c..59412b62a3 100644 --- a/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs +++ b/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using BizHawk.Client.Common; @@ -169,9 +170,9 @@ namespace BizHawk.Bizware.Audio _wavBuffer = null; } - public void PlayWavFile(string path, double volume) + public void PlayWavFile(Stream wavFile, double volume) { - using var wavStream = new SDL2WavStream(path); + using var wavStream = new SDL2WavStream(wavFile); var format = wavStream.Format == SDL2WavStream.AudioFormat.F32LSB ? WaveFormat.CreateIeeeFloatWaveFormat(wavStream.Frequency, wavStream.Channels) : new(wavStream.Frequency, wavStream.BitsPerSample, wavStream.Channels); diff --git a/src/BizHawk.Client.Common/Sound/Interfaces/ISoundOutput.cs b/src/BizHawk.Client.Common/Sound/Interfaces/ISoundOutput.cs index 8e2b2ecbdb..4add3d63e9 100644 --- a/src/BizHawk.Client.Common/Sound/Interfaces/ISoundOutput.cs +++ b/src/BizHawk.Client.Common/Sound/Interfaces/ISoundOutput.cs @@ -1,4 +1,5 @@ using System; +using System.IO; namespace BizHawk.Client.Common { @@ -10,6 +11,6 @@ namespace BizHawk.Client.Common int MaxSamplesDeficit { get; } int CalculateSamplesNeeded(); void WriteSamples(short[] samples, int sampleOffset, int sampleCount); - void PlayWavFile(string path, double volume); + void PlayWavFile(Stream wavFile, double volume); } } diff --git a/src/BizHawk.Client.Common/Sound/Output/DummySoundOutput.cs b/src/BizHawk.Client.Common/Sound/Output/DummySoundOutput.cs index 9de7e51059..2ddf2386c4 100644 --- a/src/BizHawk.Client.Common/Sound/Output/DummySoundOutput.cs +++ b/src/BizHawk.Client.Common/Sound/Output/DummySoundOutput.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.IO; namespace BizHawk.Client.Common { @@ -77,7 +78,7 @@ namespace BizHawk.Client.Common _remainingSamples += sampleCount; } - public void PlayWavFile(string path, double volume) + public void PlayWavFile(Stream wavFile, double volume) { } } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 872a4556cb..8a6d3c9aa8 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -4900,7 +4900,7 @@ namespace BizHawk.Client.EmuHawk InputManager, Tools, () => Config, - path => Sound.PlayWavFile(path, 1), // TODO: Make this configurable + wavFile => Sound.PlayWavFile(wavFile, 1), // TODO: Make this configurable RetroAchievementsMenuItem.DropDownItems, () => { diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs index 0ff6655cbe..360c33927b 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs @@ -7,21 +7,34 @@ namespace BizHawk.Client.EmuHawk { public partial class RCheevos { - private readonly Action _playWavFileCallback; + private static MemoryStream ReadWavFile(string path) + { + try + { + return new(File.ReadAllBytes(Path.Combine(PathUtils.ExeDirectoryPath, path)), false); + } + catch + { + return null; + } + } - private static readonly string _loginSound = Path.Combine(PathUtils.ExeDirectoryPath, "overlay/login.wav"); - private static readonly string _unlockSound = Path.Combine(PathUtils.ExeDirectoryPath, "overlay/unlock.wav"); - private static readonly string _lboardStartSound = Path.Combine(PathUtils.ExeDirectoryPath, "overlay/lb.wav"); - private static readonly string _lboardFailedSound = Path.Combine(PathUtils.ExeDirectoryPath, "overlay/lbcancel.wav"); - private static readonly string _infoSound = Path.Combine(PathUtils.ExeDirectoryPath, "overlay/info.wav"); + private static readonly MemoryStream _loginSound = ReadWavFile("overlay/login.wav"); + private static readonly MemoryStream _unlockSound = ReadWavFile( "overlay/unlock.wav"); + private static readonly MemoryStream _lboardStartSound = ReadWavFile("overlay/lb.wav"); + private static readonly MemoryStream _lboardFailedSound = ReadWavFile("overlay/lbcancel.wav"); + private static readonly MemoryStream _infoSound = ReadWavFile("overlay/info.wav"); + + private readonly Action _playWavFileCallback; private bool EnableSoundEffects { get; set; } - private void PlaySound(string path) + private void PlaySound(Stream wavFile) { if (EnableSoundEffects) { - _playWavFileCallback(path); + wavFile.Position = 0; + _playWavFileCallback(wavFile); } } } diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs index bc23153026..712da532c2 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Windows.Forms; using BizHawk.Client.Common; @@ -47,7 +48,7 @@ namespace BizHawk.Client.EmuHawk InputManager inputManager, ToolManager tools, Func getConfig, - Action playWavFile, + Action playWavFile, ToolStripItemCollection raDropDownItems, Action shutdownRACallback) { diff --git a/src/BizHawk.Client.EmuHawk/Sound/Sound.cs b/src/BizHawk.Client.EmuHawk/Sound/Sound.cs index 702190da21..fe4886b531 100644 --- a/src/BizHawk.Client.EmuHawk/Sound/Sound.cs +++ b/src/BizHawk.Client.EmuHawk/Sound/Sound.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading; using BizHawk.Bizware.Audio; @@ -236,12 +237,12 @@ namespace BizHawk.Client.EmuHawk _outputDevice.WriteSamples(samples, sampleOffset, sampleCount); } - public void PlayWavFile(string path, float atten) + public void PlayWavFile(Stream wavFile, float atten) { if (atten <= 0) return; try { - _outputDevice.PlayWavFile(path, Math.Min(atten, 1)); + _outputDevice.PlayWavFile(wavFile, Math.Min(atten, 1)); } catch (Exception e) {