Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OofPlugin/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions OofPlugin/OofPlugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="NAudio.Core" Version="2.2.1" />
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
<PackageReference Include="SoundFlow" Version="1.4.0" />
</ItemGroup>
<ItemGroup>
<None Update="OofPlugin.yaml">
Expand Down
165 changes: 105 additions & 60 deletions OofPlugin/SoundManager.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,82 +18,124 @@ 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<SoundPlayer> activePlayers = new();

private bool isInitialized = false;

internal CancellationTokenSource CancelToken;

public SoundManager(OofPlugin plugin) {
Configuration = plugin.Configuration;
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;
}

/// <summary>
/// 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.
/// </summary>
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();
}

}

/// <summary>
/// Stops all active sound players and cleans up their resources.
/// </summary>
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) {
Expand All @@ -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;

Expand All @@ -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);
}

Expand All @@ -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);
Expand All @@ -164,8 +209,8 @@ async Task PlayTest(float volume) {
public void Dispose() {
CancelToken.Cancel();
CancelToken.Dispose();

soundOut?.Dispose();
soundOut = null;
playbackDevice?.Dispose();
engine.Dispose();
}
}
}
17 changes: 15 additions & 2 deletions OofPlugin/Windows/ConfigWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -321,7 +336,5 @@ public static FileDialogManager SetupFileManager() {

return fileManager;
}


}

17 changes: 4 additions & 13 deletions OofPlugin/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=="
}
}
}
Expand Down