add method to play wav files via ISoundOutput, use this instead of System.Media.SoundPlayer

This commit is contained in:
CasualPokePlayer 2023-12-30 04:49:26 -08:00
parent 94f6616e74
commit a38e854a6d
13 changed files with 396 additions and 48 deletions

View File

@ -15,6 +15,7 @@
<PackageReference Include="Vortice.MediaFoundation" Version="2.4.2" />
<PackageReference Include="Vortice.XAudio2" Version="2.4.2" />
<PackageReference Include="SharpDX.DirectSound" Version="4.2.0" />
<PackageReference Include="ppy.SDL2-CS" Version="1.0.630-alpha" ExcludeAssets="native;contentFiles" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Client.Common/BizHawk.Client.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -1,10 +1,12 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using BizHawk.Client.Common;
using BizHawk.Common;
using SharpDX;
using SharpDX.DirectSound;
@ -18,7 +20,7 @@ namespace BizHawk.Bizware.Audio
private readonly IntPtr _mainWindowHandle;
private bool _disposed;
private DirectSound _device;
private SecondarySoundBuffer _deviceBuffer;
private SecondarySoundBuffer _deviceBuffer, _wavDeviceBuffer;
private int _actualWriteOffsetBytes = -1;
private int _filledBufferSizeBytes;
private long _lastWriteTime;
@ -40,6 +42,7 @@ namespace BizHawk.Bizware.Audio
{
if (_disposed) return;
StopWav(throwOnInvalidDevice: false);
_device.Dispose();
_device = null;
@ -63,6 +66,8 @@ namespace BizHawk.Bizware.Audio
_deviceBuffer.Dispose();
_deviceBuffer = null;
StopWav(throwOnInvalidDevice: false);
_device.Dispose();
_device = new();
_device.SetCooperativeLevel(_mainWindowHandle, CooperativeLevel.Priority);
@ -273,5 +278,104 @@ namespace BizHawk.Bizware.Audio
StartPlaying();
}
}
private bool IsWavPlaying
{
get
{
if (_wavDeviceBuffer == null)
{
return false;
}
var status = (BufferStatus)_wavDeviceBuffer.Status;
return (status & BufferStatus.BufferLost) == 0 &&
(status & BufferStatus.Playing) == BufferStatus.Playing;
}
}
private void StopWav(bool throwOnInvalidDevice = true)
{
bool isPlaying;
try
{
isPlaying = IsWavPlaying;
}
catch (SharpDXException)
{
if (throwOnInvalidDevice)
{
throw;
}
isPlaying = false;
}
if (isPlaying)
{
try
{
_wavDeviceBuffer.Stop();
}
catch (SharpDXException)
{
}
}
_wavDeviceBuffer?.Dispose();
_wavDeviceBuffer = null;
}
public void PlayWavFile(string path, double volume)
{
using var wavStream = new SDL2WavStream(path);
var format = wavStream.Format == SDL2WavStream.AudioFormat.F32LSB
? WaveFormat.CreateIeeeFloatWaveFormat(wavStream.Frequency, wavStream.Channels)
: new(wavStream.Frequency, wavStream.BitsPerSample, wavStream.Channels);
var desc = new SoundBufferDescription
{
Format = format,
Flags =
BufferFlags.GlobalFocus |
BufferFlags.Software |
BufferFlags.GetCurrentPosition2 |
BufferFlags.ControlVolume,
BufferBytes = unchecked((int)wavStream.Length)
};
StopWav();
_wavDeviceBuffer = new(_device, desc);
const int TEMP_BUFFER_LENGTH = 65536;
var tempBuffer = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_LENGTH);
try
{
var bufferOffset = 0;
while (true)
{
var numRead = wavStream.Read(tempBuffer, 0, TEMP_BUFFER_LENGTH);
if (numRead == 0)
{
break;
}
if (wavStream.Format == SDL2WavStream.AudioFormat.S16MSB)
{
EndiannessUtils.MutatingByteSwap16(tempBuffer.AsSpan()[..numRead]);
}
_wavDeviceBuffer.Write(tempBuffer, 0, numRead, bufferOffset, LockFlags.None);
bufferOffset += numRead;
}
}
finally
{
ArrayPool<byte>.Shared.Return(tempBuffer);
}
const int range = Volume.Maximum - Volume.Minimum;
_wavDeviceBuffer.Volume = (int)(Math.Pow(volume, 0.1) * range) + Volume.Minimum;
_wavDeviceBuffer.Play(0, PlayFlags.None);
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using BizHawk.Client.Common;
using BizHawk.Common;
using Silk.NET.Core.Native;
using Silk.NET.OpenAL;
@ -26,12 +27,14 @@ namespace BizHawk.Bizware.Audio
private bool _disposed;
private readonly IHostAudioManager _sound;
private AudioContext _context;
private uint _sourceID;
private uint _sourceID, _wavSourceID;
private BufferPool _bufferPool;
private uint _wavBufferID;
private int _currentSamplesQueued;
private short[] _tempSampleBuffer;
private unsafe Device* _device;
private Disconnect _disconnectExt;
private FloatFormat _floatExt;
public OpenALSoundOutput(IHostAudioManager sound, string chosenDeviceName)
{
@ -44,14 +47,18 @@ namespace BizHawk.Bizware.Audio
unsafe
{
_device = _alc.GetContextsDevice(_alc.GetCurrentContext());
_disconnectExt = _alc.TryGetExtension<Disconnect>(_device, out var ext) ? ext : null;
_disconnectExt = _alc.TryGetExtension<Disconnect>(_device, out var disconnectExt) ? disconnectExt : null;
}
_floatExt = _al.TryGetExtension<FloatFormat>(out var floatFormatExt) ? floatFormatExt : null;
}
public void Dispose()
{
if (_disposed) return;
StopWav();
_context.Dispose();
_context = null;
@ -104,11 +111,13 @@ namespace BizHawk.Bizware.Audio
}
StopSound();
StopWav();
_context.Dispose();
_context = new(device: null, _sound.SampleRate);
_device = _alc.GetContextsDevice(_alc.GetCurrentContext());
_disconnectExt = _alc.TryGetExtension<Disconnect>(_device, out var ext) ? ext : null;
_floatExt = _al.TryGetExtension<FloatFormat>(out var floatFormatExt) ? floatFormatExt : null;
StartSound();
}
@ -161,6 +170,62 @@ namespace BizHawk.Bizware.Audio
}
}
private void StopWav()
{
if (_wavSourceID != 0)
{
_al.SourceStop(_wavSourceID);
_al.DeleteSource(_wavSourceID);
_wavSourceID = 0;
}
if (_wavBufferID != 0)
{
_al.DeleteBuffer(_wavBufferID);
_wavBufferID = 0;
}
}
public void PlayWavFile(string path, double volume)
{
using var wavStream = new SDL2WavStream(path);
if (wavStream.Channels > 2)
{
throw new NotSupportedException("OpenAL does not support more than 2 channels");
}
var format = wavStream.Format switch
{
SDL2WavStream.AudioFormat.U8 => wavStream.Channels == 1 ? BufferFormat.Mono8 : BufferFormat.Stereo8,
SDL2WavStream.AudioFormat.S16LSB or SDL2WavStream.AudioFormat.S16MSB => wavStream.Channels == 1 ? BufferFormat.Mono16 : BufferFormat.Stereo16,
SDL2WavStream.AudioFormat.S32LSB => throw new NotSupportedException("OpenAL does not support s32 samples"),
SDL2WavStream.AudioFormat.F32LSB when _floatExt == null => throw new NotSupportedException("This OpenAL implementation does not support f32 samples"),
SDL2WavStream.AudioFormat.F32LSB => (BufferFormat)(wavStream.Channels == 1 ? FloatBufferFormat.Mono : FloatBufferFormat.Stereo),
_ => throw new InvalidOperationException(),
};
StopWav();
_wavSourceID = _al.GenSource();
_wavBufferID = _al.GenBuffer();
var tempBuffer = new byte[wavStream.Length];
wavStream.Read(tempBuffer);
if (wavStream.Format == SDL2WavStream.AudioFormat.S16MSB)
{
EndiannessUtils.MutatingByteSwap16(tempBuffer);
}
_al.BufferData(_wavBufferID, format, tempBuffer, wavStream.Frequency);
_al.SetSourceProperty(_wavSourceID, SourceFloat.Gain, (float)volume);
unsafe
{
var bid = _wavBufferID;
_al.SourceQueueBuffers(_wavSourceID, 1, &bid);
}
_al.SourcePlay(_wavSourceID);
}
private unsafe void UnqueueProcessedBuffers()
{
var releaseCount = GetSource(GetSourceInteger.BuffersProcessed);
@ -223,13 +288,8 @@ namespace BizHawk.Bizware.Audio
public class BufferPoolItem
{
public uint BufferID { get; }
public uint BufferID { get; } = _al.GenBuffer();
public int Length { get; set; }
public BufferPoolItem()
{
BufferID = _al.GenBuffer();
}
}
}
}

View File

@ -0,0 +1,125 @@
using System;
using System.IO;
using BizHawk.Common;
using static SDL2.SDL;
namespace BizHawk.Bizware.Audio
{
internal sealed class SDL2WavStream : Stream, ISpanStream
{
private IntPtr _wav;
private readonly uint _len;
private uint _pos;
// 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,
}
public int Frequency { get; }
public AudioFormat Format { get; }
public byte Channels { get; }
public int BitsPerSample => Format switch
{
AudioFormat.U8 => 8,
AudioFormat.S16LSB or AudioFormat.S16MSB => 16,
AudioFormat.S32LSB or AudioFormat.F32LSB => 32,
_ => throw new InvalidOperationException(),
};
public SDL2WavStream(string path)
{
// 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)
{
throw new($"Could not load WAV file! SDL error: {SDL_GetError()}");
}
Frequency = spec.freq;
Format = (AudioFormat)spec.format;
Channels = spec.channels;
_wav = wav;
_len = len;
}
protected override void Dispose(bool disposing)
{
SDL_FreeWAV(_wav);
_wav = IntPtr.Zero;
base.Dispose(disposing);
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _len;
public override long Position
{
get => _pos;
set
{
if (value < 0 || value > _len)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), value, message: "index out of range");
}
_pos = (uint)value;
}
}
public override void Flush()
{
}
public unsafe int Read(Span<byte> buffer)
{
if (_wav == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(SDL2WavStream));
}
var count = (int)Math.Min(buffer.Length, _len - _pos);
new ReadOnlySpan<byte>((void*)((nint)_wav + _pos), count).CopyTo(buffer);
_pos += (uint)count;
return count;
}
public override int Read(byte[] buffer, int offset, int count)
=> Read(new(buffer, offset, count));
public override long Seek(long offset, SeekOrigin origin)
{
var newpos = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _pos + offset,
SeekOrigin.End => _len + offset,
_ => offset
};
Position = newpos;
return newpos;
}
public override void SetLength(long value)
=> throw new NotSupportedException();
public void Write(ReadOnlySpan<byte> buffer)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using BizHawk.Client.Common;
using BizHawk.Common;
using Vortice.MediaFoundation;
using Vortice.Multimedia;
@ -17,8 +18,9 @@ namespace BizHawk.Bizware.Audio
private volatile bool _deviceResetRequired;
private IXAudio2 _device;
private IXAudio2MasteringVoice _masteringVoice;
private IXAudio2SourceVoice _sourceVoice;
private IXAudio2SourceVoice _sourceVoice, _wavVoice;
private BufferPool _bufferPool;
private AudioBuffer _wavBuffer;
private long _runningSamplesQueued;
private static string GetDeviceId(string deviceName)
@ -49,7 +51,7 @@ namespace BizHawk.Bizware.Audio
// note that this won't be called on the main thread, so we'll defer the reset to the main thread
_device.CriticalError += (_, _) => _deviceResetRequired = true;
_masteringVoice = _device.CreateMasteringVoice(
inputChannels: _sound.ChannelCount,
inputChannels: _sound.ChannelCount,
inputSampleRate: _sound.SampleRate,
deviceId: GetDeviceId(chosenDeviceName));
}
@ -58,6 +60,7 @@ namespace BizHawk.Bizware.Audio
{
if (_disposed) return;
StopWav();
_masteringVoice.Dispose();
_device.Dispose();
@ -113,6 +116,7 @@ namespace BizHawk.Bizware.Audio
_deviceResetRequired = false;
StopSound();
StopWav();
_masteringVoice.Dispose();
_device.Dispose();
@ -138,6 +142,7 @@ namespace BizHawk.Bizware.Audio
{
_sound.HandleInitializationOrUnderrun(detectedUnderrun, ref samplesNeeded);
}
return samplesNeeded;
}
@ -154,6 +159,38 @@ namespace BizHawk.Bizware.Audio
_runningSamplesQueued += sampleCount;
}
private void StopWav()
{
_wavVoice?.Stop();
_wavVoice?.Dispose();
_wavVoice = null;
_wavBuffer?.Dispose();
_wavBuffer = null;
}
public void PlayWavFile(string path, double volume)
{
using var wavStream = new SDL2WavStream(path);
var format = wavStream.Format == SDL2WavStream.AudioFormat.F32LSB
? WaveFormat.CreateIeeeFloatWaveFormat(wavStream.Frequency, wavStream.Channels)
: new(wavStream.Frequency, wavStream.BitsPerSample, wavStream.Channels);
StopWav();
_wavVoice = _device.CreateSourceVoice(format);
_wavBuffer = new(unchecked((int)wavStream.Length));
wavStream.Read(_wavBuffer.AsSpan());
if (wavStream.Format == SDL2WavStream.AudioFormat.S16MSB)
{
EndiannessUtils.MutatingByteSwap16(_wavBuffer.AsSpan());
}
_wavVoice.SubmitSourceBuffer(_wavBuffer);
_wavVoice.Volume = (float)volume;
_wavVoice.Start();
}
private class BufferPool : IDisposable
{
private readonly List<BufferPoolItem> _availableItems = new();

View File

@ -10,5 +10,6 @@ namespace BizHawk.Client.Common
int MaxSamplesDeficit { get; }
int CalculateSamplesNeeded();
void WriteSamples(short[] samples, int sampleOffset, int sampleCount);
void PlayWavFile(string path, double volume);
}
}

View File

@ -76,5 +76,9 @@ namespace BizHawk.Client.Common
if (sampleCount == 0) return;
_remainingSamples += sampleCount;
}
public void PlayWavFile(string path, double volume)
{
}
}
}

View File

@ -4895,13 +4895,20 @@ namespace BizHawk.Client.EmuHawk
private void OpenRetroAchievements()
{
RA = RetroAchievements.CreateImpl(this, InputManager, Tools, () => Config, RetroAchievementsMenuItem.DropDownItems, () =>
{
RA.Dispose();
RA = null;
RetroAchievementsMenuItem.DropDownItems.Clear();
RetroAchievementsMenuItem.DropDownItems.Add(StartRetroAchievementsMenuItem);
});
RA = RetroAchievements.CreateImpl(
this,
InputManager,
Tools,
() => Config,
path => Sound.PlayWavFile(path, 1), // TODO: Make this configurable
RetroAchievementsMenuItem.DropDownItems,
() =>
{
RA.Dispose();
RA = null;
RetroAchievementsMenuItem.DropDownItems.Clear();
RetroAchievementsMenuItem.DropDownItems.Add(StartRetroAchievementsMenuItem);
});
RA?.Restart();
}

View File

@ -68,7 +68,7 @@ namespace BizHawk.Client.EmuHawk
{
config.RAUsername = Username;
config.RAToken = ApiToken;
if (EnableSoundEffects) _loginSound.PlayNoExceptions();
PlaySound(_loginSound);
return;
}
}
@ -79,9 +79,9 @@ namespace BizHawk.Client.EmuHawk
config.RAUsername = Username;
config.RAToken = ApiToken;
if (LoggedIn && EnableSoundEffects)
if (LoggedIn)
{
_loginSound.PlayNoExceptions();
PlaySound(_loginSound);
}
}

View File

@ -1,5 +1,5 @@
using System;
using System.IO;
using System.Media;
using BizHawk.Common.PathExtensions;
@ -7,29 +7,21 @@ namespace BizHawk.Client.EmuHawk
{
public partial class RCheevos
{
// NOTE: these are net framework only...
// this logic should probably be the main sound class
// this shouldn't be a blocker to moving to net core anyways
private static readonly SoundPlayer _loginSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/login.wav"));
private static readonly SoundPlayer _unlockSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/unlock.wav"));
private static readonly SoundPlayer _lboardStartSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/lb.wav"));
private static readonly SoundPlayer _lboardFailedSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/lbcancel.wav"));
private static readonly SoundPlayer _infoSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/info.wav"));
private readonly Action<string> _playWavFileCallback;
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 bool EnableSoundEffects { get; set; }
}
public static class SoundPlayerExtensions
{
public static void PlayNoExceptions(this SoundPlayer sound)
private void PlaySound(string path)
{
try
if (EnableSoundEffects)
{
sound.Play();
}
catch
{
// ignored
_playWavFileCallback(path);
}
}
}

View File

@ -214,10 +214,13 @@ namespace BizHawk.Client.EmuHawk
InputManager inputManager,
ToolManager tools,
Func<Config> getConfig,
Action<string> playWavFile,
ToolStripItemCollection raDropDownItems,
Action shutdownRACallback)
: base(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback)
{
_playWavFileCallback = playWavFile;
_isActive = true;
_httpThread = new(HttpRequestThreadProc) { IsBackground = true, Priority = ThreadPriority.BelowNormal, Name = "RCheevos HTTP Thread" };
_httpThread.Start();
@ -227,7 +230,6 @@ namespace BizHawk.Client.EmuHawk
{
throw new("rc_runtime_alloc returned NULL!");
}
Login();
_eventcb = EventHandlerCallback;
_peekcb = PeekCallback;
@ -241,6 +243,7 @@ namespace BizHawk.Client.EmuHawk
EnableSoundEffects = config.RASoundEffects;
AllowUnofficialCheevos = config.RAAllowUnofficialCheevos;
Login();
BuildMenu(raDropDownItems);
}
@ -488,7 +491,7 @@ namespace BizHawk.Client.EmuHawk
var prefix = HardcoreMode ? "[HARDCORE] " : "";
_dialogParent.AddOnScreenMessage($"{prefix}Achievement Unlocked!");
_dialogParent.AddOnScreenMessage(cheevo.Description);
if (EnableSoundEffects) _unlockSound.PlayNoExceptions();
PlaySound(_unlockSound);
if (cheevo.IsOfficial)
{
@ -509,7 +512,7 @@ namespace BizHawk.Client.EmuHawk
var prefix = HardcoreMode ? "[HARDCORE] " : "";
_dialogParent.AddOnScreenMessage($"{prefix}Achievement Primed!");
_dialogParent.AddOnScreenMessage(cheevo.Description);
if (EnableSoundEffects) _infoSound.PlayNoExceptions();
PlaySound(_infoSound);
}
break;
@ -528,7 +531,7 @@ namespace BizHawk.Client.EmuHawk
CurrentLboard = lboard;
_dialogParent.AddOnScreenMessage($"Leaderboard Attempt Started!");
_dialogParent.AddOnScreenMessage(lboard.Description);
if (EnableSoundEffects) _lboardStartSound.PlayNoExceptions();
PlaySound(_lboardStartSound);
}
}
@ -550,7 +553,7 @@ namespace BizHawk.Client.EmuHawk
_dialogParent.AddOnScreenMessage($"Leaderboard Attempt Failed! ({lboard.Score})");
_dialogParent.AddOnScreenMessage(lboard.Description);
if (EnableSoundEffects) _lboardFailedSound.PlayNoExceptions();
PlaySound(_lboardFailedSound);
}
lboard.SetScore(0);
@ -588,7 +591,7 @@ namespace BizHawk.Client.EmuHawk
_dialogParent.AddOnScreenMessage($"Leaderboard Attempt Complete! ({lboard.Score})");
_dialogParent.AddOnScreenMessage(lboard.Description);
if (EnableSoundEffects) _unlockSound.PlayNoExceptions();
PlaySound(_unlockSound);
}
}
@ -615,7 +618,7 @@ namespace BizHawk.Client.EmuHawk
var prefix = HardcoreMode ? "[HARDCORE] " : "";
_dialogParent.AddOnScreenMessage($"{prefix}Achievement Unprimed!");
_dialogParent.AddOnScreenMessage(cheevo.Description);
if (EnableSoundEffects) _infoSound.PlayNoExceptions();
PlaySound(_infoSound);
}
break;

View File

@ -47,6 +47,7 @@ namespace BizHawk.Client.EmuHawk
InputManager inputManager,
ToolManager tools,
Func<Config> getConfig,
Action<string> playWavFile,
ToolStripItemCollection raDropDownItems,
Action shutdownRACallback)
{
@ -72,7 +73,7 @@ namespace BizHawk.Client.EmuHawk
}
else
{
return new RCheevos(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback);
return new RCheevos(mainForm, inputManager, tools, getConfig, playWavFile, raDropDownItems, shutdownRACallback);
}
}

View File

@ -235,5 +235,18 @@ namespace BizHawk.Client.EmuHawk
_outputDevice.WriteSamples(samples, sampleOffset, sampleCount);
}
public void PlayWavFile(string path, float atten)
{
if (atten <= 0) return;
try
{
_outputDevice.PlayWavFile(path, Math.Min(atten, 1));
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}