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=="
}
}
}