Files
ClipTrimApp/stream_deck_plugin/ClipTrimDotNet/WavPlayer.cs
2026-02-21 09:02:16 -05:00

226 lines
6.9 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Security;
using System.Threading.Tasks;
using BarRaider.SdTools;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using Newtonsoft.Json;
class CachedSound
{
public byte[] AudioData { get; private set; }
public WaveFormat WaveFormat { get; private set; }
public CachedSound(string audioFileName)
{
using (var audioFileReader = new AudioFileReader(audioFileName))
{
// TODO: could add resampling in here if required
WaveFormat = audioFileReader.WaveFormat;
var wholeFile = new List<byte>((int)(audioFileReader.Length));
var readBuffer = new byte[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels*4];
int samplesRead;
while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0)
{
wholeFile.AddRange(readBuffer.Take(samplesRead));
}
AudioData = wholeFile.ToArray();
}
//Logger.Instance.LogMessage(TracingLevel.INFO, $"AudioData Length {AudioData.Length}");
}
}
class CachedSoundSampleProvider : IWaveProvider
{
private readonly CachedSound cachedSound;
private long position;
~CachedSoundSampleProvider() {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Cache destructor");
}
public CachedSoundSampleProvider(CachedSound cachedSound)
{
this.cachedSound = cachedSound;
position = 0;
}
public int Read(byte[] buffer, int offset, int count)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Read1 byte");
var availableSamples = cachedSound.AudioData.Length - position;
var samplesToCopy = Math.Min(availableSamples, count);
try
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"{cachedSound.AudioData.GetType()} {buffer.GetType()}");
Array.Copy(cachedSound.AudioData, position, buffer, offset, samplesToCopy);
}
catch (Exception ex)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"{ex.ToString()}");
}
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Read3");
position += samplesToCopy;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Sending {samplesToCopy} samples");
return (int)samplesToCopy;
}
public WaveFormat WaveFormat => cachedSound.WaveFormat;
}
public class WavPlayer
{
private static WavPlayer? instance;
public static WavPlayer Instance
{
get
{
instance ??= new WavPlayer();
return instance;
}
}
public enum PlayMode
{
PlayOverlap,
PlayStop
}
private readonly ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>> _activePlayers;
public WavPlayer()
{
_activePlayers = new ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>>();
}
public void Play(string filePath, string id, double volume, PlayMode mode)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
if (mode == PlayMode.PlayOverlap)
{
PlayWithOverlap(filePath, id, volume);
}
else if (mode == PlayMode.PlayStop)
{
PlayWithStop(filePath, id, volume);
}
else
{
throw new ArgumentOutOfRangeException(nameof(mode), "Invalid play mode specified.");
}
}
private void PlayWithOverlap(string filePath, string id, double volume)
{
try
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "Play overlap");
var player = CreatePlayer(filePath, id);
if (!_activePlayers.ContainsKey(filePath))
{
_activePlayers[filePath] = new List<Tuple<WaveOutEvent, IWaveProvider>>();
}
_activePlayers[filePath].Add(player);
player.Item1.Volume = (float)volume;
player.Item1.Play();
}
catch(Exception ex)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, ex.ToString());
}
//var playersToDispose = _activePlayers[filePath].Where(x => x.Item1.PlaybackState == PlaybackState.Stopped).ToList();
//foreach (var p in playersToDispose)
//{
// p.Item1.Stop();
// p.Item1.Dispose();
//}
//_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
}
private void PlayWithStop(string filePath, string id, double volume)
{
if (_activePlayers.TryGetValue(filePath, out var players))
{
// Stop and dispose all current players for this file
if (players.Any(x => x.Item1.PlaybackState == PlaybackState.Playing))
{
var playersToDispose = players.ToList();
foreach (var player in playersToDispose)
{
player.Item1.Stop();
player.Item1.Dispose();
}
}
else
{
PlayWithOverlap(filePath, id, volume);
}
}
else
{
// Start a new player
PlayWithOverlap(filePath, id, volume);
}
_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
}
private Tuple<WaveOutEvent, IWaveProvider> CreatePlayer(string filePath, string name)
{
var reader = new CachedSoundSampleProvider(new CachedSound(filePath));
//var reader = new AudioFileReader(filePath);
int number = -1;
for (int i = 0; i < WaveOut.DeviceCount; ++i)
{
if (WaveOut.GetCapabilities(i).ProductName == name)
{
number = i;
}
}
var player = new WaveOutEvent() { DeviceNumber = number };
player.Init(reader);
return new Tuple<WaveOutEvent, IWaveProvider>(player, reader);
}
private void CleanupPlayer(string filePath)
{
if (_activePlayers.TryGetValue(filePath, out var players))
{
var playersToDispose = players.ToList();
foreach (var p in playersToDispose)
{
p.Item1.Stop();
p.Item1.Dispose();
}
}
_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
}
public void StopAll()
{
foreach (var players in _activePlayers.Values)
{
var playersToDispose = players.ToList();
foreach (var player in playersToDispose)
{
player.Item1.Stop();
player.Item1.Dispose();
}
players.Clear();
}
_activePlayers.Clear();
}
}