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
86 changes: 86 additions & 0 deletions AudioFileLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Diagnostics;
using System.Threading.Tasks;

namespace AudioPlayerApi;

public static class AudioFileLoader
{
public static float[] LoadAudioFile(string filePath, int targetSampleRate = 48000, int targetChannels = 1)
{
if (!File.Exists(filePath))
{
ServerConsole.AddLog($"[AudioFileLoader] File not found: {filePath}");
return Array.Empty<float>();
}

// I think this line can kill the server, but i idk how to check ffmpeg. On server start mb
Ffmpeg.InitializeFfmpegAsync().GetAwaiter().GetResult();

try
{
return ConvertToRawPcm(filePath, targetSampleRate, targetChannels);
}
catch (Exception ex)
{
ServerConsole.AddLog($"[AudioFileLoader] Error loading audio file {filePath}: {ex}");
return Array.Empty<float>();
}
}


// this method I writed with gpt because I am dumb and can't understand simple things
private static float[] ConvertToRawPcm(string filePath, int sampleRate, int channels)
{
var psi = new ProcessStartInfo
{
FileName = Ffmpeg.FfmpegPath,
Arguments = $"-hide_banner -nostats -loglevel error -i \"{filePath}\" -vn -ac {channels} -ar {sampleRate} -f f32le pipe:1",
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = new Process { StartInfo = psi };
try
{
if (!process.Start())
{
ServerConsole.AddLog("[AudioFileLoader] Failed to start FFmpeg process");
return Array.Empty<float>();
}

byte[] pcmBytes;
using (var ms = new MemoryStream())
{
process.StandardOutput.BaseStream.CopyTo(ms);
if (!process.WaitForExit(10000))
{
try { process.Kill(); } catch { /* ignore */ }
ServerConsole.AddLog("[AudioFileLoader] FFmpeg process timeout");
return Array.Empty<float>();
}

pcmBytes = ms.ToArray();
}

if (process.ExitCode != 0)
{
ServerConsole.AddLog($"[AudioFileLoader] FFmpeg exited with code {process.ExitCode}");
return Array.Empty<float>();
}

int sampleCount = pcmBytes.Length / 4;
var samples = new float[sampleCount];
Buffer.BlockCopy(pcmBytes, 0, samples, 0, pcmBytes.Length);

ServerConsole.AddLog($"[AudioFileLoader] Loaded {filePath}: {sampleCount} samples, {sampleCount / (float)(sampleRate * channels):F2}s duration");
return samples;
}
catch
{
try { if (!process.HasExited) process.Kill(); } catch { }
throw;
}
}
}
45 changes: 45 additions & 0 deletions Models/AudioClipStorage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Reflection;
using AudioPlayerApi;

/// <summary>
/// Manages the storage and loading of audio clips for playback.
Expand Down Expand Up @@ -121,6 +122,50 @@ public static bool LoadClip(string path, string name = null)
AudioClips.Add(name, new AudioClipData(name, sampleRate, channels, samples));
return true;
}
/// <summary>
/// Loads an audio file, converts it to PCM, and adds it to the clip storage and player.
/// </summary>
/// <param name="player">The AudioPlayer instance</param>
/// <param name="filePath">Path to the audio file</param>
/// <param name="clipName">Name to assign to the clip (if null, uses filename)</param>
/// <param name="volume">Playback volume</param>
/// <param name="loop">Whether to loop the clip</param>
/// <param name="destroyOnEnd">Whether to destroy after playback</param>
/// <returns>AudioClipPlayback instance or null if loading failed</returns>
public static bool LoadClipAny(
string filePath,
string clipName)
{
if (string.IsNullOrEmpty(clipName))
clipName = Path.GetFileNameWithoutExtension(filePath);

EnsureFfmpegInitialized();

float[] samples = AudioFileLoader.LoadAudioFile(
filePath,
AudioClipPlayback.SamplingRate,
AudioClipPlayback.Channels);

if (samples.Length == 0)
{
ServerConsole.AddLog($"[AudioPlayer] Failed to load audio file: {filePath}");
return false;
}

if (!AudioClips.ContainsKey(clipName))
{
AudioClips[clipName] = new AudioClipData(clipName, AudioClipPlayback.SamplingRate,
AudioClipPlayback.Channels, samples);
ServerConsole.AddLog($"[AudioPlayer] Added clip '{clipName}' to storage");
}

return true;
}
private static void EnsureFfmpegInitialized()
{
if (!File.Exists(Ffmpeg.FfmpegPath))
throw new InvalidOperationException("FFmpeg not initialized. Call InitializeFfmpegAsync at startup.");
}

/// <summary>
/// Destroys loaded clips.
Expand Down
58 changes: 33 additions & 25 deletions Pooling/PooledAudioPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public class PooledAudioPlayer
{
private readonly AudioPlayerPool _pool;
private readonly string _internalName;
private Vector3 _hiddenPosition = new Vector3(999, 999, 999);
private static readonly Vector3 _hiddenPosition = new Vector3(999, 999, 999);
private bool _isReturning = false;

public AudioPlayer Player { get; private set; }
Expand Down Expand Up @@ -68,6 +68,7 @@ internal void Deactivate()
public void Return()
{
if (_isReturning) return;
_isReturning = true;
_pool.Return(this);
}

Expand Down Expand Up @@ -116,41 +117,48 @@ public void ReturnDelWhenAllClipsPlayed()

private System.Collections.IEnumerator MonitorClipsAndReturn(bool destroy)
{
while (Player.ClipsById.Count > 0)
try
{
bool allClipsWillEnd = true;
foreach (var clip in Player.ClipsById.Values)
while (Player.ClipsById.Count > 0)
{
if (clip.Loop && clip.DestroyOnEnd == false)
bool allClipsWillEnd = true;
foreach (var clip in Player.ClipsById.Values)
{
allClipsWillEnd = false;
break;
if (clip.Loop && clip.DestroyOnEnd == false)
{
allClipsWillEnd = false;
break;
}
}
}

if (!allClipsWillEnd)
{
foreach (var clip in Player.ClipsById.Values.ToList())

if (!allClipsWillEnd)
{
if (clip.Loop)
foreach (var clip in Player.ClipsById.Values)
{
clip.Loop = false;
if (clip.Loop)
{
clip.Loop = false;
}
}
}

yield return new WaitForSeconds(0.5f);
}

_isReturning = false;

if (destroy)
{
ReturnDel();
}
else
{
Return();
}

yield return new WaitForSeconds(0.1f);
}

_isReturning = false;

if (destroy)
{
ReturnDel();
}
else
finally
{
Return();
_isReturning = false;
}
}

Expand Down