From d2f5dbc11a671cb8a182c1dbfb1e656cdbd36df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sat, 28 Feb 2026 01:52:14 +0300 Subject: [PATCH 01/22] Automatic id / throwing errors changed to log error --- .../Exiled.API/Features/Audio/WavUtility.cs | 7 +- EXILED/Exiled.API/Features/Toys/Speaker.cs | 71 +++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/EXILED/Exiled.API/Features/Audio/WavUtility.cs b/EXILED/Exiled.API/Features/Audio/WavUtility.cs index c1b1bc3f7..f88749c39 100644 --- a/EXILED/Exiled.API/Features/Audio/WavUtility.cs +++ b/EXILED/Exiled.API/Features/Audio/WavUtility.cs @@ -92,7 +92,10 @@ public static void SkipHeader(Stream stream) short bits = BinaryPrimitives.ReadInt16LittleEndian(fmtData.Slice(14, 2)); if (format != 1 || channels != 1 || rate != VoiceChatSettings.SampleRate || bits != 16) - throw new InvalidDataException($"Invalid WAV format (format={format}, channels={channels}, rate={rate}, bits={bits}). Expected PCM16, mono and {VoiceChatSettings.SampleRate}Hz."); + { + Log.Error($"[Speaker] Invalid WAV format (format={format}, channels={channels}, rate={rate}, bits={bits}). Expected PCM16, mono and {VoiceChatSettings.SampleRate}Hz."); + throw new InvalidDataException("Unsupported WAV format."); + } if (chunkSize > 16) stream.Seek(chunkSize - 16, SeekOrigin.Current); @@ -109,7 +112,7 @@ public static void SkipHeader(Stream stream) } if (stream.Position >= stream.Length) - throw new InvalidDataException("WAV file does not contain a 'data' chunk."); + Log.Error("[Speaker] WAV file does not contain a 'data' chunk."); } } } diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 4478c1143..dd4b3e4a0 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -16,6 +16,7 @@ namespace Exiled.API.Features.Toys using Enums; using Exiled.API.Features.Audio; + using Exiled.API.Features.Pools; using Interfaces; @@ -29,6 +30,7 @@ namespace Exiled.API.Features.Toys using VoiceChat.Codec; using VoiceChat.Codec.Enums; using VoiceChat.Networking; + using VoiceChat.Playbacks; using Object = UnityEngine.Object; @@ -288,20 +290,8 @@ public byte ControllerId /// The scale of the . /// Whether the should be initially spawned. /// The new . - public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) - { - Speaker speaker = new(Object.Instantiate(Prefab)) - { - Position = position ?? Vector3.zero, - Rotation = Quaternion.Euler(rotation ?? Vector3.zero), - Scale = scale ?? Vector3.one, - }; - - if (spawn) - speaker.Spawn(); - - return speaker; - } + [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) methods, rotation useless.")] + public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) => Create(parent: null, position: position, scale: scale, controllerId: null, spawn: spawn, worldPositionStays: true); /// /// Creates a new . @@ -310,21 +300,60 @@ public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scal /// Whether the should be initially spawned. /// Whether the should keep the same world position. /// The new . - public static Speaker Create(Transform transform, bool spawn, bool worldPositionStays = true) + public static Speaker Create(Transform transform, bool spawn, bool worldPositionStays = true) => Create(parent: transform, position: Vector3.zero, scale: transform.localScale.normalized, controllerId: null, spawn: spawn, worldPositionStays: worldPositionStays); + + /// + /// Creates a new . + /// + /// The parent transform to attach the to. + /// The local position of the . + /// The scale of the . + /// The specific controller ID to assign. If null, the next available ID is used. + /// Whether the should be initially spawned. + /// Whether the should keep the same world position when parented. + /// The new . + public static Speaker Create(Transform parent = null, Vector3? position = null, Vector3? scale = null, byte? controllerId = null, bool spawn = true, bool worldPositionStays = true) { - Speaker speaker = new(Object.Instantiate(Prefab, transform, worldPositionStays)) + Speaker speaker = new(Object.Instantiate(Prefab, parent, worldPositionStays)) { - Position = transform.position, - Rotation = transform.rotation, - Scale = transform.localScale.normalized, + Scale = scale ?? Vector3.one, + ControllerId = controllerId ?? GetNextControllerId(), }; + speaker.Transform.localPosition = position ?? Vector3.zero; + if (spawn) speaker.Spawn(); return speaker; } + /// + /// Gets the next available controller ID for a . + /// + /// The next available byte ID, or 0 if all IDs are currently in use. + public static byte GetNextControllerId() + { + byte id = 0; + HashSet usedIds = HashSetPool.Pool.Get(); + + foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances) + usedIds.Add(playbackBase.ControllerId); + + while (usedIds.Contains(id)) + { + id++; + if (id == 255) + { + HashSetPool.Pool.Return(usedIds); + return 0; + } + } + + HashSetPool.Pool.Return(usedIds); + return id; + } + /// /// Plays audio through this speaker. /// @@ -354,10 +383,10 @@ public static void Play(AudioMessage message, IEnumerable targets = null public void Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false) { if (!File.Exists(path)) - throw new FileNotFoundException("The specified file does not exist.", path); + Log.Warn($"[Speaker] The specified file does not exist, path: `{path}`."); if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) - throw new NotSupportedException($"The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file."); + Log.Error($"[Speaker] The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file."); TryInitializePlayBack(); Stop(); From bae29947c893e6eb408741772ab4ddbde8e8b50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sat, 28 Feb 2026 01:55:18 +0300 Subject: [PATCH 02/22] f --- EXILED/Exiled.API/Features/Audio/WavUtility.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/EXILED/Exiled.API/Features/Audio/WavUtility.cs b/EXILED/Exiled.API/Features/Audio/WavUtility.cs index f88749c39..12ea4d32d 100644 --- a/EXILED/Exiled.API/Features/Audio/WavUtility.cs +++ b/EXILED/Exiled.API/Features/Audio/WavUtility.cs @@ -112,7 +112,10 @@ public static void SkipHeader(Stream stream) } if (stream.Position >= stream.Length) + { Log.Error("[Speaker] WAV file does not contain a 'data' chunk."); + throw new InvalidDataException("Missing 'data' chunk in WAV file."); + } } } } From be87e9fc818566fc89a4069ad7b04f7eb0597592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sat, 28 Feb 2026 01:57:43 +0300 Subject: [PATCH 03/22] . --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index dd4b3e4a0..2de141c0f 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -290,7 +290,7 @@ public byte ControllerId /// The scale of the . /// Whether the should be initially spawned. /// The new . - [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) methods, rotation useless.")] + [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) method, rotation is useless.")] public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) => Create(parent: null, position: position, scale: scale, controllerId: null, spawn: spawn, worldPositionStays: true); /// From ee82708f61d57eb994a8ca697bc2fb18f149737e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sat, 28 Feb 2026 02:10:45 +0300 Subject: [PATCH 04/22] s --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 2de141c0f..008b3f365 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -317,7 +317,7 @@ public static Speaker Create(Transform parent = null, Vector3? position = null, Speaker speaker = new(Object.Instantiate(Prefab, parent, worldPositionStays)) { Scale = scale ?? Vector3.one, - ControllerId = controllerId ?? GetNextControllerId(), + ControllerId = controllerId ?? GetNextFreeControllerId(), }; speaker.Transform.localPosition = position ?? Vector3.zero; @@ -331,8 +331,8 @@ public static Speaker Create(Transform parent = null, Vector3? position = null, /// /// Gets the next available controller ID for a . /// - /// The next available byte ID, or 0 if all IDs are currently in use. - public static byte GetNextControllerId() + /// The next available byte ID. If all IDs are currently in use, returns a default of 0. + public static byte GetNextFreeControllerId() { byte id = 0; HashSet usedIds = HashSetPool.Pool.Get(); @@ -340,14 +340,15 @@ public static byte GetNextControllerId() foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances) usedIds.Add(playbackBase.ControllerId); + if (usedIds.Count >= byte.MaxValue + 1) + { + HashSetPool.Pool.Return(usedIds); + return 0; + } + while (usedIds.Contains(id)) { id++; - if (id == 255) - { - HashSetPool.Pool.Return(usedIds); - return 0; - } } HashSetPool.Pool.Return(usedIds); From 36428d67630ceaac66fca93d96cee8df835529dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sat, 28 Feb 2026 02:14:44 +0300 Subject: [PATCH 05/22] gf --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 008b3f365..0bf29dbc2 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -384,10 +384,16 @@ public static void Play(AudioMessage message, IEnumerable targets = null public void Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false) { if (!File.Exists(path)) + { Log.Warn($"[Speaker] The specified file does not exist, path: `{path}`."); + return; + } if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) + { Log.Error($"[Speaker] The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file."); + return; + } TryInitializePlayBack(); Stop(); From 841d20d72cec341cdeb37d3facb76b0341f7b22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sat, 28 Feb 2026 02:15:53 +0300 Subject: [PATCH 06/22] =?UTF-8?q?e=C4=9FH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 0bf29dbc2..eb5106e72 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -385,7 +385,7 @@ public void Play(string path, bool stream = false, bool destroyAfter = false, bo { if (!File.Exists(path)) { - Log.Warn($"[Speaker] The specified file does not exist, path: `{path}`."); + Log.Error($"[Speaker] The specified file does not exist, path: `{path}`."); return; } From a08e3ffed5e7ec3f61e218349568bfd36ffbaa38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sun, 1 Mar 2026 22:14:10 +0300 Subject: [PATCH 07/22] =?UTF-8?q?added=20new=20=C3=B6zellik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index eb5106e72..72f8f1474 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -366,6 +366,32 @@ public static void Play(AudioMessage message, IEnumerable targets = null target.Connection.Send(message); } + /// + /// Plays a wav file one time through a newly spawned speaker and destroys it afterwards. (File must be 16 bit, mono and 48khz.) + /// + /// The path to the wav file. + /// The position of the speaker. + /// The parent transform, if any. + /// The play mode determining how audio is sent to players. + /// Whether to stream the audio or preload it. + /// The target player if PlayMode is Player. + /// The list of target players if PlayMode is PlayerList. + /// The condition if PlayMode is Predicate. + /// The created Speaker instance. + public static Speaker PlayOneShot(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) + { + Speaker speaker = Create(parent: parent, position: position, spawn: true); + + speaker.PlayMode = playMode; + speaker.TargetPlayer = targetPlayer; + speaker.TargetPlayers = targetPlayers; + speaker.Predicate = predicate; + + speaker.Play(path, stream: stream, destroyAfter: true, loop: false); + + return speaker; + } + /// /// Plays audio through this speaker. /// From c6fe071a3b434fed4bea0cc503616b61a43e51a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sun, 1 Mar 2026 22:31:54 +0300 Subject: [PATCH 08/22] update --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 36 ++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 72f8f1474..9da2ab49d 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -335,14 +335,16 @@ public static Speaker Create(Transform parent = null, Vector3? position = null, public static byte GetNextFreeControllerId() { byte id = 0; - HashSet usedIds = HashSetPool.Pool.Get(); + HashSet usedIds = NorthwoodLib.Pools.HashSetPool.Shared.Rent(256); foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances) + { usedIds.Add(playbackBase.ControllerId); + } if (usedIds.Count >= byte.MaxValue + 1) { - HashSetPool.Pool.Return(usedIds); + NorthwoodLib.Pools.HashSetPool.Shared.Return(usedIds); return 0; } @@ -351,7 +353,7 @@ public static byte GetNextFreeControllerId() id++; } - HashSetPool.Pool.Return(usedIds); + NorthwoodLib.Pools.HashSetPool.Shared.Return(usedIds); return id; } @@ -377,7 +379,7 @@ public static void Play(AudioMessage message, IEnumerable targets = null /// The target player if PlayMode is Player. /// The list of target players if PlayMode is PlayerList. /// The condition if PlayMode is Predicate. - /// The created Speaker instance. + /// The created instance if playback started successfully; otherwise, null. public static Speaker PlayOneShot(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) { Speaker speaker = Create(parent: parent, position: position, spawn: true); @@ -387,7 +389,11 @@ public static Speaker PlayOneShot(string path, Vector3 position, Transform paren speaker.TargetPlayers = targetPlayers; speaker.Predicate = predicate; - speaker.Play(path, stream: stream, destroyAfter: true, loop: false); + if (!speaker.Play(path, stream: stream, destroyAfter: true, loop: false)) + { + speaker.Destroy(); + return null; + } return speaker; } @@ -407,18 +413,19 @@ public static Speaker PlayOneShot(string path, Vector3 position, Transform paren /// Whether to stream the audio or preload it. /// Whether to destroy the speaker after playback. /// Whether to loop the audio. - public void Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false) + /// true if the audio file was successfully found, loaded, and playback started; otherwise, false. + public bool Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false) { if (!File.Exists(path)) { Log.Error($"[Speaker] The specified file does not exist, path: `{path}`."); - return; + return false; } if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) { Log.Error($"[Speaker] The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file."); - return; + return false; } TryInitializePlayBack(); @@ -427,8 +434,19 @@ public void Play(string path, bool stream = false, bool destroyAfter = false, bo Loop = loop; LastTrack = path; DestroyAfter = destroyAfter; - source = stream ? new WavStreamSource(path) : new PreloadedPcmSource(path); + + try + { + source = stream ? new WavStreamSource(path) : new PreloadedPcmSource(path); + } + catch (Exception ex) + { + Log.Error(ex); + return false; + } + playBackRoutine = Timing.RunCoroutine(PlayBackCoroutine().CancelWith(GameObject)); + return true; } /// From 2d9feea2014b63dbd5adde889cb80c1268d934d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sun, 1 Mar 2026 22:43:30 +0300 Subject: [PATCH 09/22] update log --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 9da2ab49d..9c5abc889 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -441,7 +441,8 @@ public bool Play(string path, bool stream = false, bool destroyAfter = false, bo } catch (Exception ex) { - Log.Error(ex); + string loadMode = stream ? "Stream" : "Preload"; + Log.Error($"[Speaker] Failed to initialize audio source ({loadMode}) for file at path: '{path}'.\nException Details: {ex}"); return false; } From 8ec3cde5cf5f2cfbaf6df0ebed7eb7730b7daadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Sun, 1 Mar 2026 23:21:42 +0300 Subject: [PATCH 10/22] color on wrong order --- EXILED/Exiled.API/Features/Toys/Light.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Light.cs b/EXILED/Exiled.API/Features/Toys/Light.cs index 9e167d1bb..4dad774de 100644 --- a/EXILED/Exiled.API/Features/Toys/Light.cs +++ b/EXILED/Exiled.API/Features/Toys/Light.cs @@ -151,13 +151,12 @@ public static Light Create(Vector3? position /*= null*/, Vector3? rotation /*= n Position = position ?? Vector3.zero, Rotation = Quaternion.Euler(rotation ?? Vector3.zero), Scale = scale ?? Vector3.one, + Color = color ?? Color.gray, }; if (spawn) light.Spawn(); - light.Color = color ?? Color.gray; - return light; } From 670dc1a64109260f3be92b46ae72214488dd85b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 01:12:17 +0300 Subject: [PATCH 11/22] wwait i will fix --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 87 +++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 9c5abc889..d39c3e7c3 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -24,6 +24,8 @@ namespace Exiled.API.Features.Toys using Mirror; + using NorthwoodLib.Pools; + using UnityEngine; using VoiceChat; @@ -42,6 +44,8 @@ public class Speaker : AdminToy, IWrapper private const int FrameSize = VoiceChatSettings.PacketSizePerChannel; private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate; + private static readonly Queue Pool = new(); + private float[] frame; private byte[] encoded; private float[] resampleBuffer; @@ -282,6 +286,11 @@ public byte ControllerId set => Base.NetworkControllerId = value; } + /// + /// Gets or sets a value indicating whether the speaker should return to the pool after playback finishes. + /// + public bool ReturnToPoolAfter { get; set; } + /// /// Creates a new . /// @@ -328,6 +337,45 @@ public static Speaker Create(Transform parent = null, Vector3? position = null, return speaker; } + /// + /// Rents an available speaker from the pool or creates a new one if the pool is empty. + /// + /// The local position of the . + /// The parent transform to attach the to. + /// A clean instance ready for use. + public static Speaker Rent(Vector3 position, Transform parent = null) + { + Speaker speaker = null; + + while (Pool.Count > 0) + { + speaker = Pool.Dequeue(); + + if (speaker != null && speaker.Base != null) + break; + + speaker = null; + } + + if (speaker == null) + { + speaker = Create(parent: parent, position: position, spawn: true); + } + else + { + speaker.Transform.SetParent(parent); + speaker.Transform.localPosition = position; + speaker.ControllerId = GetNextFreeControllerId(); + } + + speaker.Volume = 1f; + + speaker.ReturnToPoolAfter = true; + speaker.DestroyAfter = false; + + return speaker; + } + /// /// Gets the next available controller ID for a . /// @@ -465,6 +513,41 @@ public void Stop() source = null; } + /// + /// blabalbla. + /// + public void ReturnToPool() + { + Stop(); + + Transform.SetParent(null); + Transform.localPosition = Vector3.down * 999f; + + Loop = false; + PlayMode = default; + DestroyAfter = false; + ReturnToPoolAfter = false; + Channel = Channels.ReliableOrdered2; + + LastTrack = null; + Predicate = null; + TargetPlayer = null; + TargetPlayers = null; + + Pitch = 1f; + Volume = 0f; + IsSpatial = true; + + MinDistance = 1f; + MaxDistance = 15f; + + resampleTime = 0.0; + resampleBufferFilled = 0; + isPitchDefault = true; + + Pool.Enqueue(this); + } + private void TryInitializePlayBack() { if (isPlayBackInitialized) @@ -526,7 +609,9 @@ private IEnumerator PlayBackCoroutine() continue; } - if (DestroyAfter) + if (ReturnToPoolAfter) + ReturnToPool(); + else if (DestroyAfter) Destroy(); else Stop(); From 527613c3a62d36d70f67874f8284f20494eb0620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 01:35:36 +0300 Subject: [PATCH 12/22] finished --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index d39c3e7c3..38d16bc67 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -369,9 +369,8 @@ public static Speaker Rent(Vector3 position, Transform parent = null) } speaker.Volume = 1f; - - speaker.ReturnToPoolAfter = true; - speaker.DestroyAfter = false; + speaker.MinDistance = 1f; + speaker.MaxDistance = 15f; return speaker; } @@ -417,7 +416,7 @@ public static void Play(AudioMessage message, IEnumerable targets = null } /// - /// Plays a wav file one time through a newly spawned speaker and destroys it afterwards. (File must be 16 bit, mono and 48khz.) + /// Rents a speaker from the pool, plays a wav file one time, and automatically returns it to the pool afterwards. (File must be 16 bit, mono and 48khz.) /// /// The path to the wav file. /// The position of the speaker. @@ -427,19 +426,21 @@ public static void Play(AudioMessage message, IEnumerable targets = null /// The target player if PlayMode is Player. /// The list of target players if PlayMode is PlayerList. /// The condition if PlayMode is Predicate. - /// The created instance if playback started successfully; otherwise, null. - public static Speaker PlayOneShot(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) + /// The rented instance if playback started successfully; otherwise, null. + public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) { - Speaker speaker = Create(parent: parent, position: position, spawn: true); + Speaker speaker = Rent(position, parent); speaker.PlayMode = playMode; speaker.TargetPlayer = targetPlayer; speaker.TargetPlayers = targetPlayers; speaker.Predicate = predicate; - if (!speaker.Play(path, stream: stream, destroyAfter: true, loop: false)) + speaker.ReturnToPoolAfter = true; + + if (!speaker.Play(path, stream: stream)) { - speaker.Destroy(); + speaker.ReturnToPool(); return null; } @@ -521,7 +522,7 @@ public void ReturnToPool() Stop(); Transform.SetParent(null); - Transform.localPosition = Vector3.down * 999f; + Transform.localPosition = Vector3.down * 9999; Loop = false; PlayMode = default; @@ -538,8 +539,8 @@ public void ReturnToPool() Volume = 0f; IsSpatial = true; - MinDistance = 1f; - MaxDistance = 15f; + MinDistance = 0; + MaxDistance = 0; resampleTime = 0.0; resampleBufferFilled = 0; From 26909f645940e20b14a52b2573cbf6964164bfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 02:15:35 +0300 Subject: [PATCH 13/22] fix --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 34 +++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 38d16bc67..fff07ffeb 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -363,15 +363,14 @@ public static Speaker Rent(Vector3 position, Transform parent = null) } else { - speaker.Transform.SetParent(parent); + if (parent != null) + speaker.Transform.SetParent(parent); + + speaker.Volume = 1f; speaker.Transform.localPosition = position; speaker.ControllerId = GetNextFreeControllerId(); } - speaker.Volume = 1f; - speaker.MinDistance = 1f; - speaker.MaxDistance = 15f; - return speaker; } @@ -421,20 +420,32 @@ public static void Play(AudioMessage message, IEnumerable targets = null /// The path to the wav file. /// The position of the speaker. /// The parent transform, if any. + /// Whether the audio source is spatialized. + /// The minimum distance at which the audio reaches full volume. + /// The maximum distance at which the audio can be heard. /// The play mode determining how audio is sent to players. /// Whether to stream the audio or preload it. /// The target player if PlayMode is Player. /// The list of target players if PlayMode is PlayerList. /// The condition if PlayMode is Predicate. /// The rented instance if playback started successfully; otherwise, null. - public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) + public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? minDistance = null, float? maxDistance = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) { Speaker speaker = Rent(position, parent); + if (!isSpatial) + speaker.IsSpatial = isSpatial; + + if (minDistance != null) + speaker.MinDistance = (float)minDistance; + + if (maxDistance != null) + speaker.MaxDistance = (float)maxDistance; + speaker.PlayMode = playMode; + speaker.Predicate = predicate; speaker.TargetPlayer = targetPlayer; speaker.TargetPlayers = targetPlayers; - speaker.Predicate = predicate; speaker.ReturnToPoolAfter = true; @@ -444,6 +455,7 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare return null; } + Log.Warn("pool size " + Pool.Count); return speaker; } @@ -522,12 +534,12 @@ public void ReturnToPool() Stop(); Transform.SetParent(null); - Transform.localPosition = Vector3.down * 9999; + Transform.localPosition = Vector3.zero; Loop = false; - PlayMode = default; DestroyAfter = false; ReturnToPoolAfter = false; + PlayMode = SpeakerPlayMode.Global; Channel = Channels.ReliableOrdered2; LastTrack = null; @@ -539,8 +551,8 @@ public void ReturnToPool() Volume = 0f; IsSpatial = true; - MinDistance = 0; - MaxDistance = 0; + MinDistance = 1; + MaxDistance = 15; resampleTime = 0.0; resampleBufferFilled = 0; From 6da8cd226aa4e92076bf42c0cdfbbf0f65598992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 02:32:11 +0300 Subject: [PATCH 14/22] pool clear --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 4 ++-- EXILED/Exiled.Events/Handlers/Internal/Round.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index fff07ffeb..f82a01bcd 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -44,8 +44,6 @@ public class Speaker : AdminToy, IWrapper private const int FrameSize = VoiceChatSettings.PacketSizePerChannel; private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate; - private static readonly Queue Pool = new(); - private float[] frame; private byte[] encoded; private float[] resampleBuffer; @@ -60,6 +58,8 @@ public class Speaker : AdminToy, IWrapper private bool isPitchDefault = true; private bool isPlayBackInitialized = false; + internal static readonly Queue Pool = new(); + /// /// Initializes a new instance of the class. /// diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs index cebcffa74..886f8f05b 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs @@ -18,6 +18,7 @@ namespace Exiled.Events.Handlers.Internal using Exiled.API.Features.Items; using Exiled.API.Features.Pools; using Exiled.API.Features.Roles; + using Exiled.API.Features.Toys; using Exiled.API.Structs; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Scp049; @@ -65,6 +66,8 @@ public static void OnWaitingForPlayers() /// public static void OnRestartingRound() { + Speaker.Pool.Clear(); + Scp049Role.TurnedPlayers.Clear(); Scp173Role.TurnedPlayers.Clear(); Scp096Role.TurnedPlayers.Clear(); From 952200f0d18395e215d04ab0d91ed5631dbdb9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 02:32:47 +0300 Subject: [PATCH 15/22] fAHH! --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index f82a01bcd..984f83f2a 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -455,7 +455,6 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare return null; } - Log.Warn("pool size " + Pool.Count); return speaker; } From 06e8ffdfd0755bfa98ac3936bec77a171ced3e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 02:34:25 +0300 Subject: [PATCH 16/22] Fahh --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 984f83f2a..c9cc9957a 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -41,6 +41,12 @@ namespace Exiled.API.Features.Toys /// public class Speaker : AdminToy, IWrapper { + /// + /// A queue used for object pooling of instances. + /// Reusing idle speakers instead of constantly creating and destroying them significantly improves server performance, especially for frequent audio events. + /// + internal static readonly Queue Pool = new(); + private const int FrameSize = VoiceChatSettings.PacketSizePerChannel; private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate; @@ -58,8 +64,6 @@ public class Speaker : AdminToy, IWrapper private bool isPitchDefault = true; private bool isPlayBackInitialized = false; - internal static readonly Queue Pool = new(); - /// /// Initializes a new instance of the class. /// From 8cab48f2d1bd6f7b066735f3c5d82695878d2138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Mon, 2 Mar 2026 03:51:05 +0300 Subject: [PATCH 17/22] better and more functionally --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index c9cc9957a..1b6c0d047 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -425,27 +425,33 @@ public static void Play(AudioMessage message, IEnumerable targets = null /// The position of the speaker. /// The parent transform, if any. /// Whether the audio source is spatialized. + /// The volume level of the audio source. /// The minimum distance at which the audio reaches full volume. /// The maximum distance at which the audio can be heard. + /// The playback pitch level of the audio source. /// The play mode determining how audio is sent to players. /// Whether to stream the audio or preload it. /// The target player if PlayMode is Player. /// The list of target players if PlayMode is PlayerList. /// The condition if PlayMode is Predicate. /// The rented instance if playback started successfully; otherwise, null. - public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? minDistance = null, float? maxDistance = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) + public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? volume = null, float? minDistance = null, float? maxDistance = null, float pitch = 1f, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null) { Speaker speaker = Rent(position, parent); if (!isSpatial) speaker.IsSpatial = isSpatial; - if (minDistance != null) - speaker.MinDistance = (float)minDistance; + if (volume.HasValue) + speaker.Volume = volume.Value; - if (maxDistance != null) - speaker.MaxDistance = (float)maxDistance; + if (minDistance.HasValue) + speaker.MinDistance = minDistance.Value; + if (maxDistance.HasValue) + speaker.MaxDistance = maxDistance.Value; + + speaker.Pitch = pitch; speaker.PlayMode = playMode; speaker.Predicate = predicate; speaker.TargetPlayer = targetPlayer; @@ -554,8 +560,8 @@ public void ReturnToPool() Volume = 0f; IsSpatial = true; - MinDistance = 1; - MaxDistance = 15; + MinDistance = 1f; + MaxDistance = 15f; resampleTime = 0.0; resampleBufferFilled = 0; From 699eb564ac60ea734551cca898bdd449cdefdff7 Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:39:41 +0300 Subject: [PATCH 18/22] Return to pool doc --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 1b6c0d047..b6accaf78 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -536,8 +536,8 @@ public void Stop() } /// - /// blabalbla. - /// + /// Stops the current playback, resets all properties of the , and returns the instance to the object pool for future reuse. + /// public void ReturnToPool() { Stop(); From ec11415ee9b93ae2d727c4060f844fccb169592c Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:44:57 +0300 Subject: [PATCH 19/22] Cleanup & Breaking Changes for exiled 10 Fck this old useless things --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 45 +--------------------- 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index b6accaf78..d653240ac 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -295,39 +295,17 @@ public byte ControllerId /// public bool ReturnToPoolAfter { get; set; } - /// - /// Creates a new . - /// - /// The position of the . - /// The rotation of the . - /// The scale of the . - /// Whether the should be initially spawned. - /// The new . - [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) method, rotation is useless.")] - public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) => Create(parent: null, position: position, scale: scale, controllerId: null, spawn: spawn, worldPositionStays: true); - - /// - /// Creates a new . - /// - /// The transform to create this on. - /// Whether the should be initially spawned. - /// Whether the should keep the same world position. - /// The new . - public static Speaker Create(Transform transform, bool spawn, bool worldPositionStays = true) => Create(parent: transform, position: Vector3.zero, scale: transform.localScale.normalized, controllerId: null, spawn: spawn, worldPositionStays: worldPositionStays); - /// /// Creates a new . /// /// The parent transform to attach the to. /// The local position of the . - /// The scale of the . /// The specific controller ID to assign. If null, the next available ID is used. /// Whether the should be initially spawned. - /// Whether the should keep the same world position when parented. /// The new . - public static Speaker Create(Transform parent = null, Vector3? position = null, Vector3? scale = null, byte? controllerId = null, bool spawn = true, bool worldPositionStays = true) + public static Speaker Create(Transform parent = null, Vector3? position = null, byte? controllerId = null, bool spawn = true) { - Speaker speaker = new(Object.Instantiate(Prefab, parent, worldPositionStays)) + Speaker speaker = new(Object.Instantiate(Prefab, parent)) { Scale = scale ?? Vector3.one, ControllerId = controllerId ?? GetNextFreeControllerId(), @@ -407,17 +385,6 @@ public static byte GetNextFreeControllerId() return id; } - /// - /// Plays audio through this speaker. - /// - /// An instance. - /// Targets who will hear the audio. If null, audio will be sent to all players. - public static void Play(AudioMessage message, IEnumerable targets = null) - { - foreach (Player target in targets ?? Player.List) - target.Connection.Send(message); - } - /// /// Rents a speaker from the pool, plays a wav file one time, and automatically returns it to the pool afterwards. (File must be 16 bit, mono and 48khz.) /// @@ -468,14 +435,6 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare return speaker; } - /// - /// Plays audio through this speaker. - /// - /// Audio samples. - /// The length of the samples array. - /// Targets who will hear the audio. If null, audio will be sent to all players. - public void Play(byte[] samples, int? length = null, IEnumerable targets = null) => Play(new AudioMessage(ControllerId, samples, length ?? samples.Length), targets); - /// /// Plays a wav file through this speaker.(File must be 16 bit, mono and 48khz.) /// From 0c4371fe130881bea890431f6813c0bd87e29a29 Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:52:38 +0300 Subject: [PATCH 20/22] Update Speaker.cs --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index d653240ac..7ad9bd5c6 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -307,7 +307,6 @@ public static Speaker Create(Transform parent = null, Vector3? position = null, { Speaker speaker = new(Object.Instantiate(Prefab, parent)) { - Scale = scale ?? Vector3.one, ControllerId = controllerId ?? GetNextFreeControllerId(), }; @@ -496,7 +495,7 @@ public void Stop() /// /// Stops the current playback, resets all properties of the , and returns the instance to the object pool for future reuse. - /// + /// public void ReturnToPool() { Stop(); From b5edd7a4bdc465899153f415733cc4e150a7452c Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:00:00 +0300 Subject: [PATCH 21/22] Update Speaker.cs --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 7ad9bd5c6..639bd1ada 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -425,7 +425,7 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare speaker.ReturnToPoolAfter = true; - if (!speaker.Play(path, stream: stream)) + if (!speaker.Play(path, stream)) { speaker.ReturnToPool(); return null; From 03e9a4ed2e47b5cc38ea8908eb9ed5d3daeee8de Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:04:01 +0300 Subject: [PATCH 22/22] Update Speaker.cs --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 639bd1ada..4b254506f 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -469,8 +469,7 @@ public bool Play(string path, bool stream = false, bool destroyAfter = false, bo } catch (Exception ex) { - string loadMode = stream ? "Stream" : "Preload"; - Log.Error($"[Speaker] Failed to initialize audio source ({loadMode}) for file at path: '{path}'.\nException Details: {ex}"); + Log.Error($"[Speaker] Failed to initialize audio source for file at path: '{path}'.\nException Details: {ex}"); return false; }