diff --git a/OofPlugin/Configuration.cs b/OofPlugin/Configuration.cs index 25fd5bf..2fb4e61 100644 --- a/OofPlugin/Configuration.cs +++ b/OofPlugin/Configuration.cs @@ -26,6 +26,7 @@ public class Configuration : IPluginConfiguration { //audio settings public float Volume { get; set; } = 0.5f; public string DefaultSoundImportPath { get; set; } = string.Empty; + public bool AudioOverlap { get; set; } = false; // the below exist just to make saving less cumbersome diff --git a/OofPlugin/OofPlugin.csproj b/OofPlugin/OofPlugin.csproj index 7dd73a6..4868a88 100644 --- a/OofPlugin/OofPlugin.csproj +++ b/OofPlugin/OofPlugin.csproj @@ -23,8 +23,7 @@ - - + diff --git a/OofPlugin/SoundManager.cs b/OofPlugin/SoundManager.cs index 80bbe1b..8a1633c 100644 --- a/OofPlugin/SoundManager.cs +++ b/OofPlugin/SoundManager.cs @@ -1,9 +1,15 @@ -using NAudio.Wave; +using SoundFlow.Abstracts.Devices; +using SoundFlow.Backends.MiniAudio; +using SoundFlow.Components; +using SoundFlow.Providers; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; +using AudioFormat = SoundFlow.Structs.AudioFormat; namespace OofPlugin; @@ -12,10 +18,15 @@ internal class SoundManager : IDisposable { private readonly DeadPlayersList DeadPlayersList; // sound - public bool isSoundPlaying { get; private set; } = false; - private DirectSoundOut? soundOut; private string? soundFile; + private readonly MiniAudioEngine engine; + private AudioPlaybackDevice? playbackDevice; + private AudioFormat audioFormat; + private readonly List activePlayers = new(); + + private bool isInitialized = false; + internal CancellationTokenSource CancelToken; public SoundManager(OofPlugin plugin) { @@ -23,71 +34,108 @@ public SoundManager(OofPlugin plugin) { DeadPlayersList = plugin.DeadPlayersList; LoadFile(); - + + engine = new MiniAudioEngine(); + InitializeAudioDevice(); + CancelToken = new CancellationTokenSource(); Task.Run(() => OofAudioPolling(CancelToken.Token)); } public void LoadFile() { if (string.IsNullOrEmpty(Configuration.DefaultSoundImportPath)) { - soundFile = Path.Combine( - Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName, - "oof.wav"); + soundFile = Path.Combine(Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName, "oof.wav"); return; } soundFile = Configuration.DefaultSoundImportPath; } + /// + /// Create the playback device using the current system default output and starts it, if it's already + /// created it will be destroyed and re-created, safely + /// Falls back to logging an error if device discovery or initialization fails. + /// + public void InitializeAudioDevice() { + DestroyAudioDevice(); + + try { + engine.UpdateAudioDevicesInfo(); + + // Get the system default playback device + var defaultDevice = engine.PlaybackDevices.FirstOrDefault(x => x.IsDefault); + + audioFormat = AudioFormat.DvdHq; + playbackDevice = engine.InitializePlaybackDevice(defaultDevice, audioFormat); + playbackDevice.Start(); + + isInitialized = true; + } + catch (Exception ex) { + isInitialized = false; + Dalamud.Log.Error(ex, "OOF: OofAudioRecreateAudioDevice failed"); + } + } + + + private void DestroyAudioDevice() { + Stop(); + + if(playbackDevice != null) { + playbackDevice.Stop(); + playbackDevice.Dispose(); + } + + } + + /// + /// Stops all active sound players and cleans up their resources. + /// public void Stop() { - soundOut?.Pause(); - soundOut?.Dispose(); - soundOut = null; + if (!isInitialized) return; + + lock (activePlayers) { + activePlayers.ForEach(x => { + x.Stop(); + playbackDevice?.MasterMixer.RemoveComponent(x); + x.Dispose(); + }); + + activePlayers.Clear(); + } } public void Play(CancellationToken token, float volume = 1f) { + if (!isInitialized) return; + _ = Task.Run(() => { - isSoundPlaying = true; - - WaveStream reader; - try { - reader = new MediaFoundationReader(soundFile); - } - catch (Exception ex) { - isSoundPlaying = false; - Dalamud.Log.Error("Failed to read sound file", ex); - return; + if (!Configuration.AudioOverlap) { + Stop(); } - var audioStream = - new WaveChannel32(reader) { - Volume = Configuration.Volume * volume, - PadWithZeroes = false // you need this or else playbackstopped event will not fire - }; - - using (reader) { - soundOut?.Pause(); - soundOut?.Dispose(); - soundOut = new DirectSoundOut(); - - try { - soundOut.Init(audioStream); - soundOut.Play(); - - soundOut.PlaybackStopped += (_, _) => { isSoundPlaying = false; }; - } - catch (Exception ex) { - isSoundPlaying = false; - Dalamud.Log.Error("Failed to play sound", ex); - } + if (soundFile == null) { + Dalamud.Log.Error("OOF: No sound file found to play."); + return; } - }, token).ContinueWith((t) => { - t.Exception?.Handle((e) => { - Dalamud.Log.Error($"Failed to dispose DirectSoundOut {e} "); - - return true; - }); - }); + + var dataProvider = new StreamDataProvider(engine, audioFormat, File.OpenRead(soundFile)); + var player = new SoundPlayer(engine, audioFormat, dataProvider); + + lock(activePlayers) { activePlayers.Add(player); } + + playbackDevice?.MasterMixer.AddComponent(player); + + // this cleans up after the playback ends + player.PlaybackEnded += (_, _) => { + lock(activePlayers) { activePlayers.Remove(player); } + player.Stop(); + playbackDevice?.MasterMixer.RemoveComponent(player); + player.Dispose(); + }; + + player.Volume = volume; + player.Play(); + }, token); } private async Task OofAudioPolling(CancellationToken token) { @@ -100,8 +148,7 @@ private async Task OofAudioPolling(CancellationToken token) { // Run on framework thread AND await it so exceptions are observed await Dalamud.Framework.RunOnFrameworkThread(() => { - var localPlayer = - Dalamud.ObjectTable.LocalPlayer; + var localPlayer = Dalamud.ObjectTable.LocalPlayer; if (localPlayer is null) return; @@ -111,10 +158,8 @@ await Dalamud.Framework.RunOnFrameworkThread(() => { float volume = 1f; - if (Configuration.DistanceBasedOof && - player.Distance != Vector3.Zero) { - var dist = - Vector3.Distance(localPlayer.Position, player.Distance); + if (Configuration.DistanceBasedOof && player.Distance != Vector3.Zero) { + var dist = Vector3.Distance(localPlayer.Position, player.Distance); volume = VolumeFromDist(dist); } @@ -139,8 +184,8 @@ public float VolumeFromDist(float dist, float distMax = 30f) { dist = Math.Min(dist, distMax); var falloff = Configuration.DistanceFalloff > 0 - ? 3f - Configuration.DistanceFalloff * 3f - : 2.999f; + ? 3f - Configuration.DistanceFalloff * 3f + : 2.999f; var vol = 1f - ((dist / distMax) * (1f / falloff)); return Math.Max(Configuration.DistanceMinVolume, vol); @@ -164,8 +209,8 @@ async Task PlayTest(float volume) { public void Dispose() { CancelToken.Cancel(); CancelToken.Dispose(); - - soundOut?.Dispose(); - soundOut = null; + + playbackDevice?.Dispose(); + engine.Dispose(); } -} +} \ No newline at end of file diff --git a/OofPlugin/Windows/ConfigWindow.cs b/OofPlugin/Windows/ConfigWindow.cs index 768234c..48f2bc3 100644 --- a/OofPlugin/Windows/ConfigWindow.cs +++ b/OofPlugin/Windows/ConfigWindow.cs @@ -59,6 +59,21 @@ public override void Draw() { ImGui.Spacing(); + ImGui.TextColoredWrapped(headingColor, "Settings"); + var audioOverlap = Configuration.AudioOverlap; + if (ImGui.Checkbox("Allow audio overlap###config:overlap", ref audioOverlap)) { + Configuration.AudioOverlap = audioOverlap; + Configuration.Save(); + }; + ImGuiComponents.HelpMarker("Overlap audio instead of stopping an already playing audio"); + + ImGui.Spacing(); + if (ImGui.Button("Reload Audio Devices")) { + Plugin.SoundManager.InitializeAudioDevice(); + } + + + ImGui.Spacing(); ImGui.TextColoredWrapped(headingColor, "Play sound on"); // when self falls options @@ -321,7 +336,5 @@ public static FileDialogManager SetupFileManager() { return fileManager; } - - } diff --git a/OofPlugin/packages.lock.json b/OofPlugin/packages.lock.json index 725b17e..5797274 100644 --- a/OofPlugin/packages.lock.json +++ b/OofPlugin/packages.lock.json @@ -14,20 +14,11 @@ "resolved": "1.2.39", "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" }, - "NAudio.Core": { + "SoundFlow": { "type": "Direct", - "requested": "[2.2.1, )", - "resolved": "2.2.1", - "contentHash": "GgkdP6K/7FqXFo7uHvoqGZTJvW4z8g2IffhOO4JHaLzKCdDOUEzVKtveoZkCuUX8eV2HAINqi7VFqlFndrnz/g==" - }, - "NAudio.Wasapi": { - "type": "Direct", - "requested": "[2.2.1, )", - "resolved": "2.2.1", - "contentHash": "lFfXoqacZZe0WqNChJgGYI+XV/n/61LzPHT3C1CJp4khoxeo2sziyX5wzNYWeCMNbsWxFvT3b3iXeY1UYjBhZw==", - "dependencies": { - "NAudio.Core": "2.2.1" - } + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "PVc8NEpwmx16qJdOGRvc7Kpvs/3hNZBuldErmnMQSh9IClAA2UWrafcUIQUxki2GrWB2g71hPSDIUKEo3nRgeA==" } } }