Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5b8b8b8
cleaning core & seperate extensions & fix for unity main thread freez…
MS-crew May 18, 2026
2ebbeb3
^3+%&/!'+%&()=(/&%+^'
MS-crew May 19, 2026
d6f3333
iou
MS-crew May 20, 2026
3b96907
d
MS-crew May 20, 2026
0bc0602
stream dispose fix & speaker using array pool
MS-crew May 22, 2026
5f7450d
thread safe internalSource dispose
MS-crew May 22, 2026
9c6259e
cancel token fix & thread safe internalSource & add missing isdispose…
MS-crew May 22, 2026
f9ca645
final
MS-crew May 23, 2026
4051c36
f'x
MS-crew May 23, 2026
1b4cd33
Merge branch 'dev' into SpeakerApiFinal
MS-crew May 23, 2026
a7ada1e
f
MS-crew May 23, 2026
932cc92
Merge branch 'SpeakerApiFinal' of https://github.com/MS-crew/EXILEDPR…
MS-crew May 23, 2026
a300e7c
g
MS-crew May 23, 2026
db9711c
d
MS-crew May 23, 2026
ce43142
s
MS-crew May 23, 2026
9f9286a
f
MS-crew May 23, 2026
00a097e
hg
MS-crew May 24, 2026
f87798b
f'x
MS-crew May 24, 2026
296fa86
in class property to fields
MS-crew May 24, 2026
36329aa
fix: error
louis1706 May 24, 2026
bc29689
thread safety
MS-crew May 24, 2026
1baad44
Merge branch 'SpeakerApiFinal' of https://github.com/MS-crew/EXILEDPR…
MS-crew May 24, 2026
4dfa807
Merge branch 'dev' into SpeakerApiFinal
MS-crew May 26, 2026
724121d
fix ra log have error
MS-crew Jun 3, 2026
2a66173
Merge branch 'dev' into SpeakerApiFinal
MS-crew Jun 9, 2026
18ddfd6
unscalledtime fo more accature
MS-crew Jun 9, 2026
fd1ddb5
Merge branch 'SpeakerApiFinal' of https://github.com/MS-crew/EXILEDPR…
MS-crew Jun 9, 2026
e0ea1d7
really rare Ciritical server abort fix try & preload trackdata thread…
MS-crew Jun 9, 2026
ba05cfc
using fix
MS-crew Jun 9, 2026
f26331d
different way threat fix
MS-crew Jun 14, 2026
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
106 changes: 6 additions & 100 deletions EXILED/Exiled.API/Features/Audio/AudioDataStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,12 @@

namespace Exiled.API.Features.Audio
{
using System;
using System.Collections.Generic;
using System.IO;

using Exiled.API.Structs.Audio;

using MEC;

using RoundRestarting;

using UnityEngine.Networking;

/// <summary>
/// Manages a global in-memory storage of decoded PCM audio data. Once stored, audio can be played using <see cref="PcmSources.CachedPcmSource"/>.
/// </summary>
Expand All @@ -40,47 +34,6 @@ static AudioDataStorage()
/// </summary>
public static bool ClearOnRoundRestart { get; set; } = true;

/// <summary>
/// Loads and stores a local .wav file under the specified name.
/// </summary>
/// <param name="name">The unique storage key to assign to this audio.</param>
/// <param name="path">The absolute path to the local .wav file.</param>
/// <returns><c>true</c> if the file was successfully loaded and stored; otherwise, <c>false</c>.</returns>
public static bool AddWav(string name, string path)
{
if (!ValidateName(name))
return false;

if (AudioStorage.ContainsKey(name))
{
Log.Warn($"[AudioDataStorage] An entry with the key '{name}' already exists. Skipping add.");
return false;
}

if (path.StartsWith("http"))
{
Log.Error($"[AudioDataStorage] '{path}' is a URL. Use AudioDataStorage.AddUrl() for web sources.");
return false;
}

if (!File.Exists(path))
{
Log.Error($"[AudioDataStorage] Local file not found: '{path}'");
return false;
}

try
{
AudioData parsed = WavUtility.WavToPcm(path);
return AudioStorage.TryAdd(name, parsed);
}
catch (Exception ex)
{
Log.Error($"[AudioDataStorage] Failed to load '{path}' into storage:\n{ex}");
return false;
}
}

/// <summary>
/// Stores raw PCM audio samples under the specified name.
/// </summary>
Expand Down Expand Up @@ -130,58 +83,6 @@ public static bool Add(string name, AudioData audioData)
return AudioStorage.TryAdd(name, audioData);
}

/// <summary>
/// Starts an asynchronous download of a .wav file from the specified URL and adds it to the storage.
/// </summary>
/// <param name="name">The unique storage key to assign.</param>
/// <param name="url">The HTTP or HTTPS URL pointing to a valid .wav file.</param>
/// <returns>A <see cref="CoroutineHandle"/> for the running download coroutine.</returns>
public static CoroutineHandle AddWavUrl(string name, string url) => Timing.RunCoroutine(AddUrlCoroutine(name, url));

/// <summary>
/// Starts an asynchronous download of a .wav file from the specified URL and adds it to the storage.
/// </summary>
/// <param name="name">The unique storage key to assign.</param>
/// <param name="url">The HTTP or HTTPS URL pointing to a valid .wav file.</param>
/// <returns>A MEC-compatible <see cref="IEnumerator{T}"/> of <see cref="float"/>.</returns>
public static IEnumerator<float> AddUrlCoroutine(string name, string url)
{
if (!ValidateName(name))
yield break;

if (string.IsNullOrEmpty(url) || !url.StartsWith("http"))
{
Log.Error($"[AudioDataStorage] Invalid URL for key '{name}': '{url}'. Must start with http/https.");
yield break;
}

if (AudioStorage.ContainsKey(name))
{
Log.Warn($"[AudioDataStorage] An entry with the key '{name}' already exists. Skipping download.");
yield break;
}

using UnityWebRequest www = UnityWebRequest.Get(url);
yield return Timing.WaitUntilDone(www.SendWebRequest());

if (www.result != UnityWebRequest.Result.Success)
{
Log.Error($"[AudioDataStorage] Download failed for '{url}': {www.error}");
yield break;
}

try
{
AudioData parsed = WavUtility.WavToPcm(www.downloadHandler.data);
parsed.TrackInfo.Path = url;
AudioStorage.TryAdd(name, parsed);
}
catch (Exception ex)
{
Log.Error($"[AudioDataStorage] Failed to parse downloaded WAV from '{url}':\n{ex}");
}
}

/// <summary>
/// Removes a stored audio entry by name.
/// </summary>
Expand All @@ -194,7 +95,12 @@ public static IEnumerator<float> AddUrlCoroutine(string name, string url)
/// </summary>
public static void Clear() => AudioStorage.Clear();

private static bool ValidateName(string name)
/// <summary>
/// Validates that the storage name (key) is valid.
/// </summary>
/// <param name="name">The storage name (key) to validate.</param>
/// <returns>True when name is valid; otherwise false.</returns>
internal static bool ValidateName(string name)
{
if (!string.IsNullOrEmpty(name))
return true;
Expand Down
154 changes: 154 additions & 0 deletions EXILED/Exiled.API/Features/Audio/Extensions/SourceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// -----------------------------------------------------------------------
// <copyright file="SourceExtensions.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.API.Features.Audio.Extensions
{
using System;
using System.Collections.Generic;
using System.Linq;

using Exiled.API.Features.Audio.PcmSources;
using Exiled.API.Features.Toys;
using Exiled.API.Interfaces.Audio;

/// <summary>
/// Provides extension methods for playing audio for pre-made sources on Speaker instances.
/// </summary>
public static class SourceExtensions
{
/// <summary>
/// Plays the live voice of a specific player through this speaker.
/// </summary>
/// <param name="speaker">The speaker through which to play the audio.</param>
/// <param name="player">The player whose voice will be broadcasted.</param>
/// <param name="blockOriginalVoice">If <c>true</c>, prevents the player's original voice message's from being heard while broadcasting.</param>
/// <param name="clearQueue">If <c>true</c>, clears the upcoming tracks in the playlist before starting playback.</param>
/// <returns><c>true</c> if the playback started successfully; otherwise, <c>false</c>.</returns>
public static bool PlayFromPlayer(this Speaker speaker, Player player, bool blockOriginalVoice = false, bool clearQueue = true)
{
if (player == null)
{
Log.Error("[Speaker] Source player cannot be null when streaming live microphone!");
return false;
}

PlayerVoiceSource source;
try
{
source = new PlayerVoiceSource(player, blockOriginalVoice);
}
catch (Exception ex)
{
Log.Error($"[Speaker] Failed to initialize live voice stream for player '{player.Nickname}' ({player.Id}).\nException Details: {ex}");
return false;
}

return speaker.Play(source, clearQueue);
}

/// <summary>
/// Plays the specified text as speech through this speaker using the VoiceRss TTS service.
/// </summary>
/// <param name="speaker">The speaker that will play the generated speech.</param>
/// <param name="text"> The text to convert to speech.(Length limited by 100KB).</param>
/// <param name="apiKeys"> Your VoiceRSS API keys. Get a free key at <see href="https://www.voicerss.org/registration.aspx"/>.</param>
/// <param name="language"> The language and locale code for the TTS voice. See <see href="https://www.voicerss.org/api/"/> for all supported language codes.</param>
/// <param name="voice"> Optional specific voice name for the selected language.(See <see href="https://www.voicerss.org/api/"/> for available voices per language.)</param>
/// <param name="rate"> Speech rate from -10 (slowest) to 10 (fastest). Defaults to 0 (normal speed).</param>
/// <param name="clearQueue">If <c>true</c>, clears the upcoming tracks in the playlist before starting playback.</param>
/// <returns><c>true</c> if the TTS playback started successfully; otherwise, <c>false</c>.</returns>
public static bool PlayTts(this Speaker speaker, string text, IEnumerable<string> apiKeys, string language = "en-us", string voice = null, int rate = 0, bool clearQueue = true)
{
VoiceRssTtsSource ttsSource;
try
{
ttsSource = new(text, apiKeys, language, voice, rate);
}
catch (Exception ex)
{
Log.Error($"Failed to create TTS source: {ex.Message}");
return false;
}

return speaker.Play(ttsSource, clearQueue);
}

/// <summary>
/// Plays multiple <see cref="IPcmSource"/> instances mixed together.
/// </summary>
/// <param name="speaker">The speaker through which to play the audio.</param>
/// <param name="sources">The collection of PCM sources to mix and play.</param>
/// <param name="clearQueue">If <c>true</c>, clears the upcoming tracks in the playlist before starting playback.</param>
/// <returns><c>true</c> if at least one source was successfully mixed; otherwise, <c>false</c>.</returns>
public static bool PlayMixed(this Speaker speaker, IEnumerable<IPcmSource> sources, bool clearQueue = true)
{
if (sources == null || !sources.Any())
{
Log.Error("[Speaker] No sources provided for PlayMixed!");
return false;
}

if (clearQueue)
speaker.TrackQueue.Clear();

bool anyAdded = false;

foreach (IPcmSource source in sources)
{
if (source == null)
continue;

if (speaker.AddMixed(source))
anyAdded = true;
}

return anyAdded;
}

/// <summary>
/// Dynamically mixes a new audio source into the currently playing audio without interrupting it.
/// </summary>
/// <param name="speaker">The speaker through which to play the audio.</param>
/// <param name="extraSource">The additional <see cref="IPcmSource"/> to mix with the current playback.</param>
/// <returns><c>true</c> if the source was successfully mixed or started; otherwise, <c>false</c>.</returns>
public static bool AddMixed(this Speaker speaker, IPcmSource extraSource)
{
if (extraSource == null)
{
Log.Error("[Speaker] Provided extra IPcmSource for mixing is null!");
return false;
}

if (extraSource is ILiveSource)
speaker.Pitch = 1.0f;

IPcmSource currentSource = speaker.CurrentSource;

if ((!speaker.IsPlaying && !speaker.IsPaused) || currentSource == null || currentSource.Ended)
return speaker.Play(extraSource, false);

if (currentSource is MixerSource currentMixer)
{
currentMixer.AddSource(extraSource);
return true;
}

try
{
IPcmSource oldSource = currentSource;
MixerSource newMixer = new([oldSource, extraSource]);
speaker.CurrentSource = newMixer;
return true;
}
catch (Exception ex)
{
Log.Error($"[Speaker] Failed to transition to MixerSource on the fly!\nException Details: {ex}");
return false;
}
}
}
}
Loading
Loading