From 269c2be619ed5a1e7cb26298772a00b67d7b6726 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Thu, 28 Nov 2024 16:15:44 +0100 Subject: [PATCH 01/21] FIrst --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 40 +++++++++++++++++++ EXILED/Exiled.API/Features/Toys/ToysHelper.cs | 24 +++++++++++ 2 files changed, 64 insertions(+) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index dc03c3ff9..e3fa7bf67 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -5,6 +5,12 @@ // // ----------------------------------------------------------------------- +using System.IO; +using UnityEngine; +using VoiceChat; +using VoiceChat.Networking; +using VoiceChat.Playbacks; + namespace Exiled.API.Features.Toys { using AdminToys; @@ -80,5 +86,39 @@ public float MinDistance get => Base.NetworkMinDistance; set => Base.NetworkMinDistance = value; } + + /// + /// Creates a new . + /// + /// The position of the . + /// The rotation of the . + /// The scale of the . + /// Whether the should be initially spawned. + /// The new . + public static Speaker Create(Vector3? position /*= null*/, Vector3? rotation /*= null*/, Vector3? scale /*= null*/, bool spawn /*= true*/) + { + Speaker speaker = new(UnityEngine.Object.Instantiate(ToysHelper.Speaker)) + { + Position = position ?? Vector3.zero, + Rotation = Quaternion.Euler(rotation ?? Vector3.zero), + Scale = scale ?? Vector3.one, + }; + + if (spawn) + speaker.Spawn(); + + speaker.Base.Playback = new SpeakerToyPlaybackBase(); + speaker.Base.Playback.Buffer = new PlaybackBuffer(); + + return speaker; + } + + /// + /// Todo. + /// + public void Play() + { + // TODO + } } } diff --git a/EXILED/Exiled.API/Features/Toys/ToysHelper.cs b/EXILED/Exiled.API/Features/Toys/ToysHelper.cs index 5ebcfc42f..4be1c42ba 100644 --- a/EXILED/Exiled.API/Features/Toys/ToysHelper.cs +++ b/EXILED/Exiled.API/Features/Toys/ToysHelper.cs @@ -23,6 +23,7 @@ public static class ToysHelper private static ShootingTarget sportShootingTargetObject; private static ShootingTarget dboyShootingTargetObject; private static ShootingTarget binaryShootingTargetObject; + private static SpeakerToy speakerObject; /// /// Gets the base to instantiate when creating a new primitive. @@ -138,5 +139,28 @@ public static ShootingTarget BinaryShootingTargetObject return binaryShootingTargetObject; } } + + /// + /// Gets the base to instantiate when creating a new speaker. + /// + public static SpeakerToy Speaker + { + get + { + if (speakerObject is null) + { + foreach (GameObject gameObject in NetworkClient.prefabs.Values) + { + if ((gameObject.name == "SpeakerToy") && gameObject.TryGetComponent(out SpeakerToy shootingTarget)) + { + speakerObject = shootingTarget; + break; + } + } + } + + return speakerObject; + } + } } } \ No newline at end of file From 34982485122e12f2558169f5dd4b40e830a71ce1 Mon Sep 17 00:00:00 2001 From: notzerotwo_ <63092138+NotZer0Two@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:13:34 -0800 Subject: [PATCH 02/21] Completed the Speaker API Wrapper w/ NotZer0Two --- EXILED/Exiled.API/Exiled.API.csproj | 1 + EXILED/Exiled.API/Features/Toys/Speaker.cs | 205 ++++++++++++++++-- EXILED/Exiled.API/Features/Toys/ToysHelper.cs | 49 +++-- 3 files changed, 207 insertions(+), 48 deletions(-) diff --git a/EXILED/Exiled.API/Exiled.API.csproj b/EXILED/Exiled.API/Exiled.API.csproj index e0949b7ff..beb37f9bc 100644 --- a/EXILED/Exiled.API/Exiled.API.csproj +++ b/EXILED/Exiled.API/Exiled.API.csproj @@ -17,6 +17,7 @@ + diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index e3fa7bf67..eff6b158e 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -5,24 +5,37 @@ // // ----------------------------------------------------------------------- -using System.IO; -using UnityEngine; -using VoiceChat; -using VoiceChat.Networking; -using VoiceChat.Playbacks; +using Mirror; namespace Exiled.API.Features.Toys { - using AdminToys; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using AdminToys; using Enums; - using Exiled.API.Interfaces; + using Interfaces; + using MEC; + using NVorbis; + using UnityEngine; + using Utils.Networking; + using VoiceChat.Codec; + using VoiceChat.Networking; + + using Object = UnityEngine.Object; /// /// A wrapper class for . /// public class Speaker : AdminToy, IWrapper { + // private float allowedSamples; + // private int samplesPerSecond; + private bool stopPlayback; + private bool isPlaying; + /// /// Initializes a new instance of the class. /// @@ -35,6 +48,15 @@ internal Speaker(SpeakerToy speakerToy) /// public SpeakerToy Base { get; } + /// + /// Gets or sets the controller ID of the SpeakerToy. + /// + public byte ControllerID + { + get => Base.NetworkControllerId; + set => Base.NetworkControllerId = value; + } + /// /// Gets or sets the volume of the audio source. /// @@ -88,37 +110,172 @@ public float MinDistance } /// - /// Creates a new . + /// Creates a new Speaker instance. /// - /// The position of the . - /// The rotation of the . - /// The scale of the . - /// Whether the should be initially spawned. - /// The new . - public static Speaker Create(Vector3? position /*= null*/, Vector3? rotation /*= null*/, Vector3? scale /*= null*/, bool spawn /*= true*/) + /// The controller ID for the SpeakerToy. + /// The position to place the SpeakerToy. + /// Indicates whether the audio is spatialized. + /// Determines if the speaker should be spawned immediately. + /// A new instance. + public static Speaker Create(byte controllerId, Vector3 position, bool isSpatial = false, bool spawn = true) { - Speaker speaker = new(UnityEngine.Object.Instantiate(ToysHelper.Speaker)) + Speaker speaker = new(Object.Instantiate(ToysHelper.SpeakerBaseObject)) { - Position = position ?? Vector3.zero, - Rotation = Quaternion.Euler(rotation ?? Vector3.zero), - Scale = scale ?? Vector3.one, + Position = position, + IsSpatial = isSpatial, + Base = { ControllerId = controllerId }, }; if (spawn) speaker.Spawn(); - speaker.Base.Playback = new SpeakerToyPlaybackBase(); - speaker.Base.Playback.Buffer = new PlaybackBuffer(); - return speaker; } /// - /// Todo. + /// Gets the associated with a given . + /// + /// The SpeakerToy instance. + /// The corresponding Speaker instance. + public static Speaker Get(SpeakerToy speakerToy) + { + AdminToy adminToy = Map.Toys.FirstOrDefault(x => x.AdminToyBase == speakerToy); + return adminToy is not null ? adminToy as Speaker : new Speaker(speakerToy); + } + + /// + /// Plays a single audio file through the speaker system. (No Arguments given (assuming you already preset those)). + /// + /// Path to the audio file to play. + /// Whether or not the Speaker gets destroyed after its done playing. + /// A boolean indicating if playback was successful. + public bool Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, destroyAfter); + + /// + /// Plays an audio file using FFmpeg to decode it into raw audio data. /// - public void Play() + /// The file path of the audio file. + /// The desired playback volume. (0 to ) max limit. + /// The minimum distance at which the audio is audible. + /// The maximum distance at which the audio is audible. + /// Whether or not the Speaker gets destroyed after its done playing. + /// A boolean indicating if playback was successful. + public bool Play(string path, float volume, float minDistance, float maxDistance, bool destroyAfter) { - // TODO + if (isPlaying) + Stop(); + + if (!File.Exists(path)) + { + Log.Warn($"Tried playing audio at {path} but no file was found."); + return false; + } + + Volume = volume; + MinDistance = minDistance; + MaxDistance = maxDistance; + + isPlaying = true; + Timing.RunCoroutine(PlaybackRoutine(path, destroyAfter)); + return true; } + + /// + /// Stops the current playback. + /// + public void Stop() + { + stopPlayback = true; + isPlaying = false; + } + + private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) + { + stopPlayback = false; + + string fileExtension = Path.GetExtension(filePath).ToLower(); + Log.Info($"Detected file: {filePath}, Extension: {fileExtension}"); + + const int sampleRate = 48000; // Enforce 48kHz + const int channels = 1; // Enforce mono audio + const int frameSize = 480; + + PlaybackBuffer playbackBuffer = new(); + Queue streamBuffer = new(); + float[] readBuffer = new float[(48000 / 5) + 1920]; + float[] sendBuffer = new float[(48000 / 5) + 1920]; + byte[] encodedBuffer = new byte[512]; + OpusEncoder encoder = new(VoiceChat.Codec.Enums.OpusApplicationType.Voip); + + // Decoding and playback loop + if (fileExtension == ".ogg") + { + using VorbisReader vorbisReader = new(filePath); + + // Validate format + if (vorbisReader.SampleRate != sampleRate || vorbisReader.Channels != channels) + { + Log.Error($"Invalid OGG file. Expected 48kHz mono, got {vorbisReader.SampleRate}Hz {vorbisReader.Channels} channel(s)."); + yield break; + } + + Log.Info($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); + + while (!stopPlayback) + { + int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); + if (samplesRead <= 0) + break; // End of file + + foreach (float sample in readBuffer.Take(samplesRead)) + streamBuffer.Enqueue(sample); + + while (streamBuffer.Count >= frameSize) + { + for (int i = 0; i < frameSize; i++) + playbackBuffer.Write(streamBuffer.Dequeue()); + + if (playbackBuffer.Length >= frameSize) + { + playbackBuffer.ReadTo(sendBuffer, frameSize, 0); + int dataLen = encoder.Encode(sendBuffer, encodedBuffer, frameSize); + + AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); + audioMessage.SendToAuthenticated(); + + Log.Info($"Sent {dataLen} bytes of encoded audio."); + } + + yield return Timing.WaitForOneFrame; + } + } + } + else + { + Log.Error($"Unsupported file format: {fileExtension}"); + yield break; + } + + Log.Info("Playback completed."); + isPlaying = false; + if(destroyAfter) + Destroy(); + } + + // Naudio Support :| + // private int ConvertBytesToFloats(byte[] byteBuffer, float[] floatBuffer, int bytesRead, WaveFormat waveFormat) + // { + // int bytesPerSample = waveFormat.BitsPerSample / 8; + // int sampleCount = bytesRead / bytesPerSample; + // + // for (int i = 0; i < sampleCount; i++) + // { + // // Assuming 16-bit PCM data + // short sample = BitConverter.ToInt16(byteBuffer, i * bytesPerSample); + // floatBuffer[i] = sample / 32768f; // Normalize to -1.0 to 1.0 range + // } + // + // return sampleCount; + // } } } diff --git a/EXILED/Exiled.API/Features/Toys/ToysHelper.cs b/EXILED/Exiled.API/Features/Toys/ToysHelper.cs index 4be1c42ba..a9c1d1e09 100644 --- a/EXILED/Exiled.API/Features/Toys/ToysHelper.cs +++ b/EXILED/Exiled.API/Features/Toys/ToysHelper.cs @@ -20,10 +20,11 @@ public static class ToysHelper { private static PrimitiveObjectToy primitiveBaseObject; private static LightSourceToy lightBaseObject; + private static SpeakerToy speakerObject; + private static ShootingTarget sportShootingTargetObject; private static ShootingTarget dboyShootingTargetObject; private static ShootingTarget binaryShootingTargetObject; - private static SpeakerToy speakerObject; /// /// Gets the base to instantiate when creating a new primitive. @@ -71,6 +72,29 @@ public static LightSourceToy LightBaseObject } } + /// + /// Gets the base to instantiate when creating a new speaker. + /// + public static SpeakerToy SpeakerBaseObject + { + get + { + if (speakerObject is null) + { + foreach (GameObject gameObject in NetworkClient.prefabs.Values) + { + if (gameObject.TryGetComponent(out SpeakerToy component)) + { + speakerObject = component; + break; + } + } + } + + return speakerObject; + } + } + /// /// Gets the base to instantiate when creating a new sport shooting target. /// @@ -139,28 +163,5 @@ public static ShootingTarget BinaryShootingTargetObject return binaryShootingTargetObject; } } - - /// - /// Gets the base to instantiate when creating a new speaker. - /// - public static SpeakerToy Speaker - { - get - { - if (speakerObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if ((gameObject.name == "SpeakerToy") && gameObject.TryGetComponent(out SpeakerToy shootingTarget)) - { - speakerObject = shootingTarget; - break; - } - } - } - - return speakerObject; - } - } } } \ No newline at end of file From 953276055db8e93933fb93f9133a4e7211fb68d8 Mon Sep 17 00:00:00 2001 From: SticksDev Date: Fri, 29 Nov 2024 23:09:55 -0800 Subject: [PATCH 03/21] dwadadad --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index eff6b158e..440f7a446 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -5,11 +5,8 @@ // // ----------------------------------------------------------------------- -using Mirror; - namespace Exiled.API.Features.Toys { - using System; using System.Collections.Generic; using System.IO; using System.Linq; From 7b71da034fd4283b9615d0c7a6c674c01b317837 Mon Sep 17 00:00:00 2001 From: SticksDev Date: Sat, 30 Nov 2024 00:39:28 -0800 Subject: [PATCH 04/21] IT WORKS --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 59 +++++++++++++--------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 440f7a446..4dd1c7852 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -186,6 +186,11 @@ public void Stop() isPlaying = false; } + // Stick please go to bed -Stick + // Credits: + // - ChatGPT + // - Zer0Tw0 + // - Stick private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) { stopPlayback = false; @@ -195,55 +200,63 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) const int sampleRate = 48000; // Enforce 48kHz const int channels = 1; // Enforce mono audio - const int frameSize = 480; + const int frameSize = 480; // Frame size for 10ms of audio at 48kHz - PlaybackBuffer playbackBuffer = new(); Queue streamBuffer = new(); - float[] readBuffer = new float[(48000 / 5) + 1920]; - float[] sendBuffer = new float[(48000 / 5) + 1920]; + float[] readBuffer = new float[frameSize * 4]; + float[] sendBuffer = new float[frameSize]; byte[] encodedBuffer = new byte[512]; OpusEncoder encoder = new(VoiceChat.Codec.Enums.OpusApplicationType.Voip); - // Decoding and playback loop + float playbackInterval = frameSize / (float)sampleRate; + float nextPlaybackTime = Timing.LocalTime; + if (fileExtension == ".ogg") { using VorbisReader vorbisReader = new(filePath); - // Validate format if (vorbisReader.SampleRate != sampleRate || vorbisReader.Channels != channels) { - Log.Error($"Invalid OGG file. Expected 48kHz mono, got {vorbisReader.SampleRate}Hz {vorbisReader.Channels} channel(s)."); + Log.Error($"Invalid OGG file. Expected {sampleRate / 1000}kHz mono, got {vorbisReader.SampleRate / 1000}kHz {vorbisReader.Channels} channel(s)."); yield break; } Log.Info($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); - while (!stopPlayback) + while (streamBuffer.Count < frameSize * 2 && !stopPlayback) { int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); if (samplesRead <= 0) - break; // End of file + break; foreach (float sample in readBuffer.Take(samplesRead)) streamBuffer.Enqueue(sample); + } - while (streamBuffer.Count >= frameSize) + while (!stopPlayback && streamBuffer.Count > 0) + { + if (Timing.LocalTime < nextPlaybackTime) { - for (int i = 0; i < frameSize; i++) - playbackBuffer.Write(streamBuffer.Dequeue()); + yield return Timing.WaitForOneFrame; + continue; + } - if (playbackBuffer.Length >= frameSize) - { - playbackBuffer.ReadTo(sendBuffer, frameSize, 0); - int dataLen = encoder.Encode(sendBuffer, encodedBuffer, frameSize); + for (int i = 0; i < frameSize && streamBuffer.Count > 0; i++) + sendBuffer[i] = streamBuffer.Dequeue(); - AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); - audioMessage.SendToAuthenticated(); + int dataLen = encoder.Encode(sendBuffer, encodedBuffer); + AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); + audioMessage.SendToAuthenticated(); - Log.Info($"Sent {dataLen} bytes of encoded audio."); - } + Log.Debug($"Sent {dataLen} bytes of encoded audio."); - yield return Timing.WaitForOneFrame; + nextPlaybackTime += playbackInterval; + + if (streamBuffer.Count < frameSize && !vorbisReader.IsEndOfStream) + { + int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); + foreach (float sample in readBuffer.Take(samplesRead)) + streamBuffer.Enqueue(sample); } } } @@ -255,7 +268,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) Log.Info("Playback completed."); isPlaying = false; - if(destroyAfter) + if (destroyAfter) Destroy(); } @@ -275,4 +288,4 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) // return sampleCount; // } } -} +} \ No newline at end of file From 222778b7f7d3804a5adb9b794ad36d8692ad306f Mon Sep 17 00:00:00 2001 From: SticksDev Date: Sat, 30 Nov 2024 23:38:42 -0800 Subject: [PATCH 05/21] Made isPlaying public for everyone to see --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 4dd1c7852..dd3210271 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -31,7 +31,6 @@ public class Speaker : AdminToy, IWrapper // private float allowedSamples; // private int samplesPerSecond; private bool stopPlayback; - private bool isPlaying; /// /// Initializes a new instance of the class. @@ -106,6 +105,11 @@ public float MinDistance set => Base.NetworkMinDistance = value; } + /// + /// Gets a value indicating whether the is playing an audio or not. (Use method Stop() to stop the playback). + /// + public bool IsPlaying { get; internal set; } + /// /// Creates a new Speaker instance. /// @@ -159,7 +163,7 @@ public static Speaker Get(SpeakerToy speakerToy) /// A boolean indicating if playback was successful. public bool Play(string path, float volume, float minDistance, float maxDistance, bool destroyAfter) { - if (isPlaying) + if (IsPlaying) Stop(); if (!File.Exists(path)) @@ -172,7 +176,7 @@ public bool Play(string path, float volume, float minDistance, float maxDistance MinDistance = minDistance; MaxDistance = maxDistance; - isPlaying = true; + IsPlaying = true; Timing.RunCoroutine(PlaybackRoutine(path, destroyAfter)); return true; } @@ -183,7 +187,7 @@ public bool Play(string path, float volume, float minDistance, float maxDistance public void Stop() { stopPlayback = true; - isPlaying = false; + IsPlaying = false; } // Stick please go to bed -Stick @@ -267,7 +271,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) } Log.Info("Playback completed."); - isPlaying = false; + IsPlaying = false; if (destroyAfter) Destroy(); } From 55f3ffa7e1c8058a90be6faadc888f7fe6b54887 Mon Sep 17 00:00:00 2001 From: SticksDev Date: Sat, 30 Nov 2024 23:39:14 -0800 Subject: [PATCH 06/21] Outdated Documentation --- 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 dd3210271..fadf32f27 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -153,7 +153,7 @@ public static Speaker Get(SpeakerToy speakerToy) public bool Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, destroyAfter); /// - /// Plays an audio file using FFmpeg to decode it into raw audio data. + /// Plays a single audio file through the speaker system. /// /// The file path of the audio file. /// The desired playback volume. (0 to ) max limit. From 61e15a616f17811286182273267c120742048c6c Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 1 Dec 2024 09:21:04 +0100 Subject: [PATCH 07/21] Fixes --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index eff6b158e..310c2976c 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -147,7 +147,7 @@ public static Speaker Get(SpeakerToy speakerToy) /// Plays a single audio file through the speaker system. (No Arguments given (assuming you already preset those)). /// /// Path to the audio file to play. - /// Whether or not the Speaker gets destroyed after its done playing. + /// Whether the Speaker gets destroyed after it's done playing. /// A boolean indicating if playback was successful. public bool Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, destroyAfter); @@ -158,7 +158,7 @@ public static Speaker Get(SpeakerToy speakerToy) /// The desired playback volume. (0 to ) max limit. /// The minimum distance at which the audio is audible. /// The maximum distance at which the audio is audible. - /// Whether or not the Speaker gets destroyed after its done playing. + /// Whether the Speaker gets destroyed after it's done playing. /// A boolean indicating if playback was successful. public bool Play(string path, float volume, float minDistance, float maxDistance, bool destroyAfter) { @@ -221,7 +221,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) Log.Info($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); - while (!stopPlayback) + while (!stopPlayback || Round.IsEnded) { int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); if (samplesRead <= 0) From a1186bc583402756c608ad515fecb141b80b43c5 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 1 Dec 2024 09:27:47 +0100 Subject: [PATCH 08/21] Fixes --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index c8489ac11..b66e23358 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -5,11 +5,8 @@ // // ----------------------------------------------------------------------- -using Mirror; - namespace Exiled.API.Features.Toys { - using System; using System.Collections.Generic; using System.IO; using System.Linq; From e72380607e409268b3763eb3ed434f19682dab1f Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 1 Dec 2024 11:36:32 +0100 Subject: [PATCH 09/21] BroadcastTo and removed Info Messages --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 41 ++++++++++------------ 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index b66e23358..f3d72b877 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -28,8 +28,6 @@ namespace Exiled.API.Features.Toys /// public class Speaker : AdminToy, IWrapper { - // private float allowedSamples; - // private int samplesPerSecond; private bool stopPlayback; /// @@ -110,6 +108,11 @@ public float MinDistance /// public bool IsPlaying { get; internal set; } + /// + /// Gets or Sets a list of players that can hear this speaker. + /// + public List BroadcastTo { get; set; } + /// /// Creates a new Speaker instance. /// @@ -125,6 +128,7 @@ public static Speaker Create(byte controllerId, Vector3 position, bool isSpatial Position = position, IsSpatial = isSpatial, Base = { ControllerId = controllerId }, + BroadcastTo = new List(), }; if (spawn) @@ -195,7 +199,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) stopPlayback = false; string fileExtension = Path.GetExtension(filePath).ToLower(); - Log.Info($"Detected file: {filePath}, Extension: {fileExtension}"); + Log.Debug($"Detected file: {filePath}, Extension: {fileExtension}"); const int sampleRate = 48000; // Enforce 48kHz const int channels = 1; // Enforce mono audio @@ -220,7 +224,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) yield break; } - Log.Info($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); + Log.Debug($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); while ((streamBuffer.Count < frameSize * 2 && !stopPlayback) || Round.IsEnded) { @@ -244,7 +248,16 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) int dataLen = encoder.Encode(sendBuffer, encodedBuffer); AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); - audioMessage.SendToAuthenticated(); + + if (BroadcastTo.Count <= 0) + { + audioMessage.SendToAuthenticated(); + } + else + { + foreach (Player p in BroadcastTo) + p.ReferenceHub.connectionToClient.Send(audioMessage); + } Log.Debug($"Sent {dataLen} bytes of encoded audio."); @@ -265,26 +278,10 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) yield break; } - Log.Info("Playback completed."); + Log.Debug("Playback completed."); IsPlaying = false; if (destroyAfter) Destroy(); } - - // Naudio Support :| - // private int ConvertBytesToFloats(byte[] byteBuffer, float[] floatBuffer, int bytesRead, WaveFormat waveFormat) - // { - // int bytesPerSample = waveFormat.BitsPerSample / 8; - // int sampleCount = bytesRead / bytesPerSample; - // - // for (int i = 0; i < sampleCount; i++) - // { - // // Assuming 16-bit PCM data - // short sample = BitConverter.ToInt16(byteBuffer, i * bytesPerSample); - // floatBuffer[i] = sample / 32768f; // Normalize to -1.0 to 1.0 range - // } - // - // return sampleCount; - // } } } \ No newline at end of file From 9e4575ca8f1d1b85c940a8e414f78c8c14bae533 Mon Sep 17 00:00:00 2001 From: SticksDev Date: Wed, 4 Dec 2024 16:21:14 -0800 Subject: [PATCH 10/21] Made all the required changes --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 111 ++++++++++----------- 1 file changed, 52 insertions(+), 59 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index f3d72b877..f9477c696 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -13,6 +13,7 @@ namespace Exiled.API.Features.Toys using AdminToys; using Enums; + using Features; using Interfaces; using MEC; using NVorbis; @@ -116,7 +117,7 @@ public float MinDistance /// /// Creates a new Speaker instance. /// - /// The controller ID for the SpeakerToy. + /// The ControllerId of the SpeakerToy. The ControllerId is used to find out which SpeakerToy is which while playing. If you have the same ID in two SpeakerToys, it will play the same audio file through both SpeakerToys. /// The position to place the SpeakerToy. /// Indicates whether the audio is spatialized. /// Determines if the speaker should be spawned immediately. @@ -153,8 +154,7 @@ public static Speaker Get(SpeakerToy speakerToy) /// /// Path to the audio file to play. /// Whether the Speaker gets destroyed after it's done playing. - /// A boolean indicating if playback was successful. - public bool Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, destroyAfter); + public void Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, destroyAfter); /// /// Plays a single audio file through the speaker system. @@ -164,8 +164,7 @@ public static Speaker Get(SpeakerToy speakerToy) /// The minimum distance at which the audio is audible. /// The maximum distance at which the audio is audible. /// Whether the Speaker gets destroyed after it's done playing. - /// A boolean indicating if playback was successful. - public bool Play(string path, float volume, float minDistance, float maxDistance, bool destroyAfter) + public void Play(string path, float volume, float minDistance, float maxDistance, bool destroyAfter) { if (IsPlaying) Stop(); @@ -173,7 +172,7 @@ public bool Play(string path, float volume, float minDistance, float maxDistance if (!File.Exists(path)) { Log.Warn($"Tried playing audio at {path} but no file was found."); - return false; + return; } Volume = volume; @@ -182,7 +181,6 @@ public bool Play(string path, float volume, float minDistance, float maxDistance IsPlaying = true; Timing.RunCoroutine(PlaybackRoutine(path, destroyAfter)); - return true; } /// @@ -214,69 +212,64 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) float playbackInterval = frameSize / (float)sampleRate; float nextPlaybackTime = Timing.LocalTime; - if (fileExtension == ".ogg") + if (fileExtension != ".ogg") { - using VorbisReader vorbisReader = new(filePath); + Log.Error($"Unsupported file format: {fileExtension}"); + yield break; + } - if (vorbisReader.SampleRate != sampleRate || vorbisReader.Channels != channels) - { - Log.Error($"Invalid OGG file. Expected {sampleRate / 1000}kHz mono, got {vorbisReader.SampleRate / 1000}kHz {vorbisReader.Channels} channel(s)."); - yield break; - } + using VorbisReader vorbisReader = new(filePath); + + if (vorbisReader.SampleRate != sampleRate || vorbisReader.Channels != channels) + { + Log.Error($"Invalid OGG file. Expected {sampleRate / 1000}kHz mono, got {vorbisReader.SampleRate / 1000}kHz {vorbisReader.Channels} channel(s)."); + yield break; + } - Log.Debug($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); + Log.Debug($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); + + while ((streamBuffer.Count < frameSize * 2 && !stopPlayback) || Round.IsEnded) + { + int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); + if (samplesRead <= 0) + break; - while ((streamBuffer.Count < frameSize * 2 && !stopPlayback) || Round.IsEnded) + foreach (float sample in readBuffer.Take(samplesRead)) + streamBuffer.Enqueue(sample); + + while (!stopPlayback && streamBuffer.Count > 0) { - int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); - if (samplesRead <= 0) - break; + if (Timing.LocalTime < nextPlaybackTime) + { + yield return Timing.WaitForOneFrame; + continue; + } - foreach (float sample in readBuffer.Take(samplesRead)) - streamBuffer.Enqueue(sample); + for (int i = 0; i < frameSize && streamBuffer.Count > 0; i++) + sendBuffer[i] = streamBuffer.Dequeue(); - while (!stopPlayback && streamBuffer.Count > 0) + int dataLen = encoder.Encode(sendBuffer, encodedBuffer); + AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); + + if (BroadcastTo.Count <= 0) { - if (Timing.LocalTime < nextPlaybackTime) - { - yield return Timing.WaitForOneFrame; - continue; - } - - for (int i = 0; i < frameSize && streamBuffer.Count > 0; i++) - sendBuffer[i] = streamBuffer.Dequeue(); - - int dataLen = encoder.Encode(sendBuffer, encodedBuffer); - AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); - - if (BroadcastTo.Count <= 0) - { - audioMessage.SendToAuthenticated(); - } - else - { - foreach (Player p in BroadcastTo) - p.ReferenceHub.connectionToClient.Send(audioMessage); - } - - Log.Debug($"Sent {dataLen} bytes of encoded audio."); - - nextPlaybackTime += playbackInterval; - - if (streamBuffer.Count < frameSize && !vorbisReader.IsEndOfStream) - { - samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); - foreach (float sample in readBuffer.Take(samplesRead)) - streamBuffer.Enqueue(sample); - } + audioMessage.SendToAuthenticated(); } + else + { + foreach (Player p in BroadcastTo) + p.ReferenceHub.connectionToClient.Send(audioMessage); + } + + nextPlaybackTime += playbackInterval; + + if (streamBuffer.Count >= frameSize || vorbisReader.IsEndOfStream) + continue; + samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); + foreach (float sample in readBuffer.Take(samplesRead)) + streamBuffer.Enqueue(sample); } } - else - { - Log.Error($"Unsupported file format: {fileExtension}"); - yield break; - } Log.Debug("Playback completed."); IsPlaying = false; From 461ce943f2a7fbd2d98181ce6955a2c8168670ce Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 8 Dec 2024 08:22:44 +0100 Subject: [PATCH 11/21] Speaker Prefab --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index f9477c696..9d35c64e0 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -43,6 +43,11 @@ internal Speaker(SpeakerToy speakerToy) /// public SpeakerToy Base { get; } + /// + /// Gets the prefab. + /// + public static SpeakerToy Prefab => PrefabHelper.GetPrefab(PrefabType.SpeakerToy); + /// /// Gets or sets the controller ID of the SpeakerToy. /// @@ -124,7 +129,7 @@ public float MinDistance /// A new instance. public static Speaker Create(byte controllerId, Vector3 position, bool isSpatial = false, bool spawn = true) { - Speaker speaker = new(Object.Instantiate(ToysHelper.SpeakerBaseObject)) + Speaker speaker = new(Object.Instantiate(Prefab)) { Position = position, IsSpatial = isSpatial, From 5321aa91009581b7ff0d7f5172304597472f3765 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 8 Dec 2024 08:29:19 +0100 Subject: [PATCH 12/21] Hoping i fixed them --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 9d35c64e0..c0286a773 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -39,14 +39,14 @@ internal Speaker(SpeakerToy speakerToy) : base(speakerToy, AdminToyType.Speaker) => Base = speakerToy; /// - /// Gets the base . + /// Gets the prefab. /// - public SpeakerToy Base { get; } + public static SpeakerToy Prefab => PrefabHelper.GetPrefab(PrefabType.SpeakerToy); /// - /// Gets the prefab. + /// Gets the base . /// - public static SpeakerToy Prefab => PrefabHelper.GetPrefab(PrefabType.SpeakerToy); + public SpeakerToy Base { get; } /// /// Gets or sets the controller ID of the SpeakerToy. From 4ae034fb94fe318679f0aec9e162f956e1737253 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 8 Dec 2024 08:33:12 +0100 Subject: [PATCH 13/21] Hoping that i fixed it --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 44 ++--- EXILED/Exiled.API/Features/Toys/ToysHelper.cs | 167 ------------------ 2 files changed, 22 insertions(+), 189 deletions(-) delete mode 100644 EXILED/Exiled.API/Features/Toys/ToysHelper.cs diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index c0286a773..7cb64b213 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -48,15 +48,6 @@ internal Speaker(SpeakerToy speakerToy) /// public SpeakerToy Base { get; } - /// - /// Gets or sets the controller ID of the SpeakerToy. - /// - public byte ControllerID - { - get => Base.NetworkControllerId; - set => Base.NetworkControllerId = value; - } - /// /// Gets or sets the volume of the audio source. /// @@ -109,6 +100,16 @@ public float MinDistance set => Base.NetworkMinDistance = value; } + /// + /// Gets or sets the controller ID of the SpeakerToy. + /// + public byte ControllerID + { + get => Base.NetworkControllerId; + set => Base.NetworkControllerId = value; + } + + /// /// Gets a value indicating whether the is playing an audio or not. (Use method Stop() to stop the playback). /// @@ -120,21 +121,20 @@ public float MinDistance public List BroadcastTo { get; set; } /// - /// Creates a new Speaker instance. + /// Creates a new . /// - /// The ControllerId of the SpeakerToy. The ControllerId is used to find out which SpeakerToy is which while playing. If you have the same ID in two SpeakerToys, it will play the same audio file through both SpeakerToys. - /// The position to place the SpeakerToy. - /// Indicates whether the audio is spatialized. - /// Determines if the speaker should be spawned immediately. - /// A new instance. - public static Speaker Create(byte controllerId, Vector3 position, bool isSpatial = false, bool spawn = true) + /// The position of the . + /// The rotation of the . + /// 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)) + Speaker speaker = new(UnityEngine.Object.Instantiate(Prefab)) { - Position = position, - IsSpatial = isSpatial, - Base = { ControllerId = controllerId }, - BroadcastTo = new List(), + Position = position ?? Vector3.zero, + Rotation = Quaternion.Euler(rotation ?? Vector3.zero), + Scale = scale ?? Vector3.one, }; if (spawn) @@ -178,7 +178,7 @@ public void Play(string path, float volume, float minDistance, float maxDistance { Log.Warn($"Tried playing audio at {path} but no file was found."); return; - } + } Volume = volume; MinDistance = minDistance; diff --git a/EXILED/Exiled.API/Features/Toys/ToysHelper.cs b/EXILED/Exiled.API/Features/Toys/ToysHelper.cs deleted file mode 100644 index a9c1d1e09..000000000 --- a/EXILED/Exiled.API/Features/Toys/ToysHelper.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.API.Features.Toys -{ - using AdminToys; - - using Mirror; - - using UnityEngine; - - /// - /// A helper class for interacting with toys. - /// - public static class ToysHelper - { - private static PrimitiveObjectToy primitiveBaseObject; - private static LightSourceToy lightBaseObject; - private static SpeakerToy speakerObject; - - private static ShootingTarget sportShootingTargetObject; - private static ShootingTarget dboyShootingTargetObject; - private static ShootingTarget binaryShootingTargetObject; - - /// - /// Gets the base to instantiate when creating a new primitive. - /// - public static PrimitiveObjectToy PrimitiveBaseObject - { - get - { - if (primitiveBaseObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if (gameObject.TryGetComponent(out PrimitiveObjectToy component)) - { - primitiveBaseObject = component; - break; - } - } - } - - return primitiveBaseObject; - } - } - - /// - /// Gets the base to instantiate when creating a new light. - /// - public static LightSourceToy LightBaseObject - { - get - { - if (lightBaseObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if (gameObject.TryGetComponent(out LightSourceToy component)) - { - lightBaseObject = component; - break; - } - } - } - - return lightBaseObject; - } - } - - /// - /// Gets the base to instantiate when creating a new speaker. - /// - public static SpeakerToy SpeakerBaseObject - { - get - { - if (speakerObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if (gameObject.TryGetComponent(out SpeakerToy component)) - { - speakerObject = component; - break; - } - } - } - - return speakerObject; - } - } - - /// - /// Gets the base to instantiate when creating a new sport shooting target. - /// - public static ShootingTarget SportShootingTargetObject - { - get - { - if (sportShootingTargetObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if ((gameObject.name == "sportTargetPrefab") && gameObject.TryGetComponent(out ShootingTarget shootingTarget)) - { - sportShootingTargetObject = shootingTarget; - break; - } - } - } - - return sportShootingTargetObject; - } - } - - /// - /// Gets the base to instantiate when creating a new dboy shooting target. - /// - public static ShootingTarget DboyShootingTargetObject - { - get - { - if (dboyShootingTargetObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if ((gameObject.name == "dboyTargetPrefab") && gameObject.TryGetComponent(out ShootingTarget shootingTarget)) - { - dboyShootingTargetObject = shootingTarget; - break; - } - } - } - - return dboyShootingTargetObject; - } - } - - /// - /// Gets the base to instantiate when creating a new binary shooting target. - /// - public static ShootingTarget BinaryShootingTargetObject - { - get - { - if (binaryShootingTargetObject is null) - { - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if ((gameObject.name == "binaryTargetPrefab") && gameObject.TryGetComponent(out ShootingTarget shootingTarget)) - { - binaryShootingTargetObject = shootingTarget; - break; - } - } - } - - return binaryShootingTargetObject; - } - } - } -} \ No newline at end of file From f323cef04dd4b4724db51497488570847a7c3529 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Sun, 8 Dec 2024 08:37:04 +0100 Subject: [PATCH 14/21] Conflits + Error Build Solved --- 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 8e210248c..b8c77605b 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -109,7 +109,6 @@ public byte ControllerID set => Base.NetworkControllerId = value; } - /// /// Gets a value indicating whether the is playing an audio or not. (Use method Stop() to stop the playback). /// @@ -142,7 +141,7 @@ public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scal return speaker; } - + /// /// Gets the associated with a given . /// From 3614921236c408aacb04e08ed25951edbafe7ca7 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Tue, 10 Dec 2024 19:23:36 +0100 Subject: [PATCH 15/21] Removed Round.IsEnded --- 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 b8c77605b..1eabd26c9 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -177,7 +177,7 @@ public void Play(string path, float volume, float minDistance, float maxDistance { Log.Warn($"Tried playing audio at {path} but no file was found."); return; - } + } Volume = volume; MinDistance = minDistance; @@ -232,7 +232,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) Log.Debug($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); - while ((streamBuffer.Count < frameSize * 2 && !stopPlayback) || Round.IsEnded) + while (streamBuffer.Count < frameSize * 2 && !stopPlayback) { int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); if (samplesRead <= 0) From 1a0f63941d71224af8da30fffd947ed2d1858d80 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Tue, 10 Dec 2024 20:52:24 +0100 Subject: [PATCH 16/21] Round Restarting For admintoys --- EXILED/Exiled.Events/Handlers/Internal/Round.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs index 7f64274c5..bd74b6184 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; @@ -70,6 +71,14 @@ public static void OnRestartingRound() TeslaGate.IgnoredTeams.Clear(); API.Features.Round.IgnoredPlayers.Clear(); + + foreach (AdminToy admin in AdminToy.List) + { + if (admin is Speaker speaker && speaker.IsPlaying) + { + speaker.Stop(); + } + } } /// From 6fc52e8e9b071ebd811fc4d7f816a4df7be15ba7 Mon Sep 17 00:00:00 2001 From: NotZer0Two Date: Tue, 10 Dec 2024 20:58:19 +0100 Subject: [PATCH 17/21] Build Error fixed --- 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 1eabd26c9..9360163eb 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -149,8 +149,8 @@ public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scal /// The corresponding Speaker instance. public static Speaker Get(SpeakerToy speakerToy) { - AdminToy adminToy = Map.Toys.FirstOrDefault(x => x.AdminToyBase == speakerToy); - return adminToy is not null ? adminToy as Speaker : new Speaker(speakerToy); + AdminToy adminToy = List.FirstOrDefault(x => x.AdminToyBase == speakerToy); + return adminToy is not null ? adminToy as Speaker : new(speakerToy); } /// From 391846bfa1e6964592ffc8c6c6e892c06b42e799 Mon Sep 17 00:00:00 2001 From: SticksDev Date: Tue, 10 Dec 2024 12:34:05 -0800 Subject: [PATCH 18/21] Updated the Create method, we don't need to have Scale, or Rotation. --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 40 ++++++++++------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 1eabd26c9..0c3f33e79 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -122,18 +122,18 @@ public byte ControllerID /// /// Creates a new . /// + /// The Identification of the . Playing two speakers with the same identification will cause them to play the same audio. /// The position of the . - /// The rotation of the . - /// The scale of the . + /// Whether the should be a 3d spaced audio, or a 2d spaced audio. /// Whether the should be initially spawned. /// The new . - public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) + public static Speaker Create(byte controllerId, Vector3? position, bool isSpatial, bool spawn) { - Speaker speaker = new(UnityEngine.Object.Instantiate(Prefab)) + Speaker speaker = new(Object.Instantiate(Prefab)) { Position = position ?? Vector3.zero, - Rotation = Quaternion.Euler(rotation ?? Vector3.zero), - Scale = scale ?? Vector3.one, + IsSpatial = isSpatial, + Base = { ControllerId = controllerId }, }; if (spawn) @@ -158,17 +158,20 @@ public static Speaker Get(SpeakerToy speakerToy) /// /// Path to the audio file to play. /// Whether the Speaker gets destroyed after it's done playing. - public void Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, destroyAfter); + /// Return's whether the path was correct or not. + public bool Play(string path, bool destroyAfter = false) => Play(path, Volume, MinDistance, MaxDistance, BroadcastTo, destroyAfter); /// /// Plays a single audio file through the speaker system. /// /// The file path of the audio file. /// The desired playback volume. (0 to ) max limit. - /// The minimum distance at which the audio is audible. + /// The minimum distance at which the audio's max volume is able to be heard. /// The maximum distance at which the audio is audible. + /// Whether to play it to a specific list of players. Keep null if you want to play it to all. /// Whether the Speaker gets destroyed after it's done playing. - public void Play(string path, float volume, float minDistance, float maxDistance, bool destroyAfter) + /// Return's whether the inputted path was correct or not. + public bool Play(string path, float volume, float minDistance, float maxDistance, List playersToPlayTo = null, bool destroyAfter = false) { if (IsPlaying) Stop(); @@ -176,15 +179,17 @@ public void Play(string path, float volume, float minDistance, float maxDistance if (!File.Exists(path)) { Log.Warn($"Tried playing audio at {path} but no file was found."); - return; + return false; } Volume = volume; MinDistance = minDistance; MaxDistance = maxDistance; + BroadcastTo = playersToPlayTo; IsPlaying = true; Timing.RunCoroutine(PlaybackRoutine(path, destroyAfter)); + return true; } /// @@ -232,7 +237,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) Log.Debug($"Playing OGG file with Sample Rate: {sampleRate}, Channels: {channels}"); - while (streamBuffer.Count < frameSize * 2 && !stopPlayback) + while (streamBuffer.Count < frameSize * 2 && !stopPlayback && Base.gameObject != null) { int samplesRead = vorbisReader.ReadSamples(readBuffer, 0, readBuffer.Length); if (samplesRead <= 0) @@ -255,15 +260,8 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) int dataLen = encoder.Encode(sendBuffer, encodedBuffer); AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); - if (BroadcastTo.Count <= 0) - { - audioMessage.SendToAuthenticated(); - } - else - { - foreach (Player p in BroadcastTo) - p.ReferenceHub.connectionToClient.Send(audioMessage); - } + foreach (Player p in BroadcastTo ?? Player.List) + p.ReferenceHub.connectionToClient.Send(audioMessage); nextPlaybackTime += playbackInterval; @@ -277,7 +275,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) Log.Debug("Playback completed."); IsPlaying = false; - if (destroyAfter) + if (destroyAfter && Base.gameObject != null) Destroy(); } } From 0ee371c6723d5bb28ded4f4e959478568bb2dd65 Mon Sep 17 00:00:00 2001 From: Yamato <66829532+louis1706@users.noreply.github.com> Date: Mon, 30 Dec 2024 20:10:23 +0100 Subject: [PATCH 19/21] Apply suggestions from code review --- 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 53c9d6ca7..d69d75fdd 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -102,7 +102,7 @@ public float MinDistance /// /// Gets or sets the controller ID of the SpeakerToy. /// - public byte ControllerID + public byte ControllerId { get => Base.NetworkControllerId; set => Base.NetworkControllerId = value; From b81d2e35dc8432b504740786685826374235f6fb Mon Sep 17 00:00:00 2001 From: Yamato Date: Mon, 30 Dec 2024 20:13:51 +0100 Subject: [PATCH 20/21] fix --- 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 d69d75fdd..8b6e9a5a9 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -169,7 +169,7 @@ public static void Play(AudioMessage message, IEnumerable targets = null /// 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); + public void Play(byte[] samples, int? length = null, IEnumerable targets = null) => Play(new AudioMessage(ControllerId, samples, length ?? samples.Length), targets); /// /// Plays a single audio file through the speaker system. (No Arguments given (assuming you already preset those)). @@ -276,7 +276,7 @@ private IEnumerator PlaybackRoutine(string filePath, bool destroyAfter) sendBuffer[i] = streamBuffer.Dequeue(); int dataLen = encoder.Encode(sendBuffer, encodedBuffer); - AudioMessage audioMessage = new(ControllerID, encodedBuffer, dataLen); + AudioMessage audioMessage = new(ControllerId, encodedBuffer, dataLen); foreach (Player p in BroadcastTo ?? Player.List) p.ReferenceHub.connectionToClient.Send(audioMessage); From e95b978af3f45c3c8023dcce80e8a7aa9d76e78f Mon Sep 17 00:00:00 2001 From: Yamato Date: Mon, 30 Dec 2024 20:15:15 +0100 Subject: [PATCH 21/21] ReAdd This Method --- EXILED/Exiled.API/Features/Toys/Speaker.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 8b6e9a5a9..80fb60947 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -118,6 +118,28 @@ public byte ControllerId /// public List BroadcastTo { 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 . + public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) + { + Speaker speaker = new(UnityEngine.Object.Instantiate(Prefab)) + { + Position = position ?? Vector3.zero, + Rotation = Quaternion.Euler(rotation ?? Vector3.zero), + Scale = scale ?? Vector3.one, + }; + + if (spawn) + speaker.Spawn(); + return speaker; + } + /// /// Creates a new . ///