diff --git a/EXILED/EXILED.props b/EXILED/EXILED.props index 85f0b0aa18..f69fead8ca 100644 --- a/EXILED/EXILED.props +++ b/EXILED/EXILED.props @@ -7,7 +7,7 @@ net48 - 13.0 + 14.0 x64 false $(MSBuildThisFileDirectory)\bin\$(Configuration)\ diff --git a/EXILED/Exiled.API/Enums/SpeakerPlayMode.cs b/EXILED/Exiled.API/Enums/SpeakerPlayMode.cs new file mode 100644 index 0000000000..9bafa513e1 --- /dev/null +++ b/EXILED/Exiled.API/Enums/SpeakerPlayMode.cs @@ -0,0 +1,35 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Enums +{ + /// + /// Specifies the available modes for playing audio through a speaker. + /// + public enum SpeakerPlayMode : byte + { + /// + /// Play audio globally to all players. + /// + Global = 0, + + /// + /// Play audio to a specific player. + /// + Player = 1, + + /// + /// Play audio to a specific list of players. + /// + PlayerList = 2, + + /// + /// Play audio to players matching a predicate. + /// + Predicate = 3, + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Audio/PreloadedPcmSource.cs b/EXILED/Exiled.API/Features/Audio/PreloadedPcmSource.cs new file mode 100644 index 0000000000..7be9d09a30 --- /dev/null +++ b/EXILED/Exiled.API/Features/Audio/PreloadedPcmSource.cs @@ -0,0 +1,114 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Audio +{ + using System; + + using Exiled.API.Interfaces; + + using VoiceChat; + + /// + /// Represents a preloaded PCM audio source. + /// + public sealed class PreloadedPcmSource : IPcmSource + { + /// + /// The PCM data buffer. + /// + private readonly float[] data; + + /// + /// The current read position in the data buffer. + /// + private int pos; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the audio file. + public PreloadedPcmSource(string path) + { + data = WavUtility.WavToPcm(path); + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw PCM float array. + public PreloadedPcmSource(float[] pcmData) + { + data = pcmData; + } + + /// + /// Gets a value indicating whether the end of the PCM data buffer has been reached. + /// + public bool Ended => pos >= data.Length; + + /// + /// Gets the total duration of the audio in seconds. + /// + public double TotalDuration => (double)data.Length / VoiceChatSettings.SampleRate; + + /// + /// Gets or sets the current playback position in seconds. + /// + public double CurrentTime + { + get => (double)pos / VoiceChatSettings.SampleRate; + set => Seek(value); + } + + /// + /// Reads a sequence of PCM samples from the preloaded buffer into the specified array. + /// + /// The destination array to copy the samples into. + /// The zero-based index in at which to begin storing the data. + /// The maximum number of samples to read. + /// The number of samples read into . + public int Read(float[] buffer, int offset, int count) + { + int read = Math.Min(count, data.Length - pos); + Array.Copy(data, pos, buffer, offset, read); + pos += read; + + return read; + } + + /// + /// Seeks to the specified position in seconds. + /// + /// The target position in seconds. + public void Seek(double seconds) + { + long targetIndex = (long)(seconds * VoiceChatSettings.SampleRate); + + if (targetIndex < 0) + targetIndex = 0; + + if (targetIndex > data.Length) + targetIndex = data.Length; + + pos = (int)targetIndex; + } + + /// + /// Resets the read position to the beginning of the PCM data buffer. + /// + public void Reset() + { + pos = 0; + } + + /// + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Audio/WavStreamSource.cs b/EXILED/Exiled.API/Features/Audio/WavStreamSource.cs new file mode 100644 index 0000000000..d910093f1b --- /dev/null +++ b/EXILED/Exiled.API/Features/Audio/WavStreamSource.cs @@ -0,0 +1,144 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Audio +{ + using System; + using System.Buffers; + using System.IO; + using System.Runtime.InteropServices; + + using Exiled.API.Interfaces; + + using VoiceChat; + + /// + /// Provides a PCM audio source from a WAV file stream. + /// + public sealed class WavStreamSource : IPcmSource + { + private const float Divide = 1f / 32768f; + + private readonly long endPosition; + private readonly long startPosition; + private readonly FileStream stream; + + private byte[] internalBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the audio file. + public WavStreamSource(string path) + { + stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, FileOptions.SequentialScan); + WavUtility.SkipHeader(stream); + startPosition = stream.Position; + endPosition = stream.Length; + internalBuffer = ArrayPool.Shared.Rent(VoiceChatSettings.PacketSizePerChannel * 2); + } + + /// + /// Gets the total duration of the audio in seconds. + /// + public double TotalDuration => (endPosition - startPosition) / 2.0 / VoiceChatSettings.SampleRate; + + /// + /// Gets or sets the current playback position in seconds. + /// + public double CurrentTime + { + get => (stream.Position - startPosition) / 2.0 / VoiceChatSettings.SampleRate; + set => Seek(value); + } + + /// + /// Gets a value indicating whether the end of the stream has been reached. + /// + public bool Ended => stream.Position >= endPosition; + + /// + /// Reads PCM data from the stream into the specified buffer. + /// + /// The buffer to fill with PCM data. + /// The offset in the buffer at which to begin writing. + /// The maximum number of samples to read. + /// The number of samples read. + public int Read(float[] buffer, int offset, int count) + { + int bytesNeeded = count * 2; + + if (internalBuffer.Length < bytesNeeded) + { + ArrayPool.Shared.Return(internalBuffer); + internalBuffer = ArrayPool.Shared.Rent(bytesNeeded); + } + + int bytesRead = stream.Read(internalBuffer, 0, bytesNeeded); + + if (bytesRead == 0) + return 0; + + if (bytesRead % 2 != 0) + bytesRead--; + + Span byteSpan = internalBuffer.AsSpan(0, bytesRead); + Span shortSpan = MemoryMarshal.Cast(byteSpan); + + int samplesInDestination = buffer.Length - offset; + int samplesToWrite = Math.Min(shortSpan.Length, samplesInDestination); + + for (int i = 0; i < samplesToWrite; i++) + buffer[offset + i] = shortSpan[i] * Divide; + + return samplesToWrite; + } + + /// + /// Seeks to the specified position in the stream. + /// + /// The position in seconds to seek to. + public void Seek(double seconds) + { + long targetSample = (long)(seconds * VoiceChatSettings.SampleRate); + long targetByte = targetSample * 2; + + long newPos = startPosition + targetByte; + if (newPos > endPosition) + newPos = endPosition; + + if (newPos < startPosition) + newPos = startPosition; + + if (newPos % 2 != 0) + newPos--; + + stream.Position = newPos; + } + + /// + /// Resets the stream position to the start. + /// + public void Reset() + { + stream.Position = startPosition; + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + stream?.Dispose(); + if (internalBuffer != null) + { + ArrayPool.Shared.Return(internalBuffer); + internalBuffer = null; + } + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Audio/WavUtility.cs b/EXILED/Exiled.API/Features/Audio/WavUtility.cs new file mode 100644 index 0000000000..c1b1bc3f73 --- /dev/null +++ b/EXILED/Exiled.API/Features/Audio/WavUtility.cs @@ -0,0 +1,116 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Audio +{ + using System; + using System.Buffers; + using System.Buffers.Binary; + using System.IO; + using System.Runtime.InteropServices; + + using VoiceChat; + + /// + /// Provides utility methods for working with WAV audio files. + /// + public static class WavUtility + { + private const float Divide = 1f / 32768f; + + /// + /// Converts a WAV file at the specified path to a PCM float array. + /// + /// The file path of the WAV file to convert. + /// An array of floats representing the PCM data. + public static float[] WavToPcm(string path) + { + using FileStream fs = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + int length = (int)fs.Length; + + byte[] rentedBuffer = ArrayPool.Shared.Rent(length); + + try + { + int bytesRead = fs.Read(rentedBuffer, 0, length); + + using MemoryStream ms = new(rentedBuffer, 0, bytesRead); + + SkipHeader(ms); + + int headerOffset = (int)ms.Position; + int dataLength = bytesRead - headerOffset; + + Span audioDataSpan = rentedBuffer.AsSpan(headerOffset, dataLength); + Span samples = MemoryMarshal.Cast(audioDataSpan); + + float[] pcm = new float[samples.Length]; + + for (int i = 0; i < samples.Length; i++) + pcm[i] = samples[i] * Divide; + + return pcm; + } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } + } + + /// + /// Skips the WAV file header and validates that the format is PCM16 mono with the specified sample rate. + /// + /// The to read from. + public static void SkipHeader(Stream stream) + { + Span headerBuffer = stackalloc byte[12]; + stream.Read(headerBuffer); + + Span chunkHeader = stackalloc byte[8]; + while (true) + { + int read = stream.Read(chunkHeader); + if (read < 8) + break; + + uint chunkId = BinaryPrimitives.ReadUInt32LittleEndian(chunkHeader[..4]); + int chunkSize = BinaryPrimitives.ReadInt32LittleEndian(chunkHeader.Slice(4, 4)); + + // 'fmt ' chunk + if (chunkId == 0x20746D66) + { + Span fmtData = stackalloc byte[16]; + stream.Read(fmtData); + + short format = BinaryPrimitives.ReadInt16LittleEndian(fmtData[..2]); + short channels = BinaryPrimitives.ReadInt16LittleEndian(fmtData.Slice(2, 2)); + int rate = BinaryPrimitives.ReadInt32LittleEndian(fmtData.Slice(4, 4)); + 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."); + + if (chunkSize > 16) + stream.Seek(chunkSize - 16, SeekOrigin.Current); + } + + // 'data' chunk + else if (chunkId == 0x61746164) + { + return; + } + else + { + stream.Seek(chunkSize, SeekOrigin.Current); + } + + if (stream.Position >= stream.Length) + throw new InvalidDataException("WAV file does not contain a 'data' chunk."); + } + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/Generic/EnumClass.cs b/EXILED/Exiled.API/Features/Core/Generic/EnumClass.cs index 5f883d0bcf..0f3077d874 100644 --- a/EXILED/Exiled.API/Features/Core/Generic/EnumClass.cs +++ b/EXILED/Exiled.API/Features/Core/Generic/EnumClass.cs @@ -67,10 +67,10 @@ public string Name .GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public) .Where(t => t.FieldType == typeof(TObject)); - foreach (FieldInfo field in fields) + foreach (FieldInfo @field in fields) { - TObject instance = (TObject)field.GetValue(null); - instance.name = field.Name; + TObject instance = (TObject)@field.GetValue(null); + instance.name = @field.Name; } isDefined = true; diff --git a/EXILED/Exiled.API/Features/Core/Generic/UnmanagedEnumClass.cs b/EXILED/Exiled.API/Features/Core/Generic/UnmanagedEnumClass.cs index 7928f38783..ce82315efa 100644 --- a/EXILED/Exiled.API/Features/Core/Generic/UnmanagedEnumClass.cs +++ b/EXILED/Exiled.API/Features/Core/Generic/UnmanagedEnumClass.cs @@ -67,10 +67,10 @@ public string Name .GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public) .Where(t => t.FieldType == typeof(TObject)); - foreach (FieldInfo field in fields) + foreach (FieldInfo @field in fields) { - TObject instance = (TObject)field.GetValue(null); - instance.name = field.Name; + TObject instance = (TObject)@field.GetValue(null); + instance.name = @field.Name; } isDefined = true; diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 8b0af6bd79..4478c1143b 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -7,20 +7,53 @@ namespace Exiled.API.Features.Toys { + using System; using System.Collections.Generic; + using System.IO; using AdminToys; + using Enums; - using Exiled.API.Interfaces; + + using Exiled.API.Features.Audio; + + using Interfaces; + + using MEC; + + using Mirror; + using UnityEngine; + + using VoiceChat; + using VoiceChat.Codec; + using VoiceChat.Codec.Enums; using VoiceChat.Networking; - using VoiceChat.Playbacks; + + using Object = UnityEngine.Object; /// /// A wrapper class for . /// public class Speaker : AdminToy, IWrapper { + private const int FrameSize = VoiceChatSettings.PacketSizePerChannel; + private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate; + + private float[] frame; + private byte[] encoded; + private float[] resampleBuffer; + + private double resampleTime; + private int resampleBufferFilled; + + private IPcmSource source; + private OpusEncoder encoder; + private CoroutineHandle playBackRoutine; + + private bool isPitchDefault = true; + private bool isPlayBackInitialized = false; + /// /// Initializes a new instance of the class. /// @@ -28,6 +61,37 @@ public class Speaker : AdminToy, IWrapper internal Speaker(SpeakerToy speakerToy) : base(speakerToy, AdminToyType.Speaker) => Base = speakerToy; + /// + /// Invoked when the audio playback starts. + /// + public event Action OnPlaybackStarted; + + /// + /// Invoked when the audio playback is paused. + /// + public event Action OnPlaybackPaused; + + /// + /// Invoked when the audio playback is resumed from a paused state. + /// + public event Action OnPlaybackResumed; + + /// + /// Invoked when the audio playback loops back to the beginning. + /// + public event Action OnPlaybackLooped; + + /// + /// Invoked when the audio track finishes playing. + /// If looping is enabled, this triggers every time the track finished. + /// + public event Action OnPlaybackFinished; + + /// + /// Invoked when the audio playback stops completely (either manually or end of file). + /// + public event Action OnPlaybackStopped; + /// /// Gets the prefab. /// @@ -38,6 +102,123 @@ internal Speaker(SpeakerToy speakerToy) /// public SpeakerToy Base { get; } + /// + /// Gets or sets the network channel used for sending audio packets from this speaker . + /// + public int Channel { get; set; } = Channels.ReliableOrdered2; + + /// + /// Gets or sets a value indicating whether the audio playback should loop when it reaches the end. + /// + public bool Loop { get; set; } + + /// + /// Gets or sets a value indicating whether the speaker should be destroyed after playback finishes. + /// + public bool DestroyAfter { get; set; } + + /// + /// Gets or sets the play mode for this speaker, determining how audio is sent to players. + /// + public SpeakerPlayMode PlayMode { get; set; } + + /// + /// Gets or sets the target player who will hear the audio played by this speaker when is set to . + /// + public Player TargetPlayer { get; set; } + + /// + /// Gets or sets the list of target players who will hear the audio played by this speaker when is set to . + /// + public HashSet TargetPlayers { get; set; } + + /// + /// Gets or sets the predicate used to determine which players will hear the audio when is set to . + /// The predicate should return true for players who should receive the audio. + /// + public Func Predicate { get; set; } + + /// + /// Gets a value indicating whether gets is a sound playing on this speaker or not. + /// + public bool IsPlaying => playBackRoutine.IsRunning && !IsPaused; + + /// + /// Gets or sets a value indicating whether the playback is paused. + /// + /// + /// A where true means the playback is paused; false means it is not paused. + /// + public bool IsPaused + { + get => playBackRoutine.IsAliveAndPaused; + set + { + if (!playBackRoutine.IsRunning) + return; + + if (playBackRoutine.IsAliveAndPaused == value) + return; + + playBackRoutine.IsAliveAndPaused = value; + if (value) + OnPlaybackPaused?.Invoke(); + else + OnPlaybackResumed?.Invoke(); + } + } + + /// + /// Gets or sets the current playback time in seconds. + /// Returns 0 if not playing. + /// + public double CurrentTime + { + get => source?.CurrentTime ?? 0.0; + set + { + if (source != null) + { + source.CurrentTime = value; + resampleTime = 0.0; + resampleBufferFilled = 0; + } + } + } + + /// + /// Gets the total duration of the current track in seconds. + /// Returns 0 if not playing. + /// + public double TotalDuration => source?.TotalDuration ?? 0.0; + + /// + /// Gets the path to the last audio file played on this speaker. + /// + public string LastTrack { get; private set; } + + /// + /// Gets or sets the playback pitch. + /// + /// + /// A representing the pitch level of the audio source, + /// where 1.0 is normal pitch, less than 1.0 is lower pitch (slower), and greater than 1.0 is higher pitch (faster). + /// + public float Pitch + { + get; + set + { + field = Mathf.Max(0.1f, Mathf.Abs(value)); + isPitchDefault = Mathf.Abs(field - 1.0f) < 0.0001f; + if (isPitchDefault) + { + resampleTime = 0.0; + resampleBufferFilled = 0; + } + } + } + /// /// Gets or sets the volume of the audio source. /// @@ -109,7 +290,7 @@ public byte ControllerId /// The new . public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, 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), @@ -138,7 +319,7 @@ public static Speaker Create(Transform transform, bool spawn, bool worldPosition Scale = transform.localScale.normalized, }; - if(spawn) + if (spawn) speaker.Spawn(); return speaker; @@ -162,5 +343,245 @@ public static void Play(AudioMessage message, IEnumerable targets = null /// 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.) + /// + /// The path to the wav file. + /// 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) + { + if (!File.Exists(path)) + throw new FileNotFoundException("The specified file does not exist.", path); + + if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) + throw new NotSupportedException($"The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file."); + + TryInitializePlayBack(); + Stop(); + + Loop = loop; + LastTrack = path; + DestroyAfter = destroyAfter; + source = stream ? new WavStreamSource(path) : new PreloadedPcmSource(path); + playBackRoutine = Timing.RunCoroutine(PlayBackCoroutine().CancelWith(GameObject)); + } + + /// + /// Stops playback. + /// + public void Stop() + { + if (playBackRoutine.IsRunning) + { + Timing.KillCoroutines(playBackRoutine); + OnPlaybackStopped?.Invoke(); + } + + source?.Dispose(); + source = null; + } + + private void TryInitializePlayBack() + { + if (isPlayBackInitialized) + return; + + isPlayBackInitialized = true; + + frame = new float[FrameSize]; + resampleBuffer = Array.Empty(); + encoder = new(OpusApplicationType.Audio); + encoded = new byte[VoiceChatSettings.MaxEncodedSize]; + + AdminToyBase.OnRemoved += OnToyRemoved; + } + + private IEnumerator PlayBackCoroutine() + { + OnPlaybackStarted?.Invoke(); + + resampleTime = 0.0; + resampleBufferFilled = 0; + + float timeAccumulator = 0f; + + while (true) + { + timeAccumulator += Time.deltaTime; + + while (timeAccumulator >= FrameTime) + { + timeAccumulator -= FrameTime; + + if (isPitchDefault) + { + int read = source.Read(frame, 0, FrameSize); + if (read < FrameSize) + Array.Clear(frame, read, FrameSize - read); + } + else + { + ResampleFrame(); + } + + int len = encoder.Encode(frame, encoded); + + if (len > 2) + SendPacket(len); + + if (!source.Ended) + continue; + + OnPlaybackFinished?.Invoke(LastTrack); + + if (Loop) + { + source.Reset(); + OnPlaybackLooped?.Invoke(); + resampleTime = resampleBufferFilled = 0; + continue; + } + + if (DestroyAfter) + Destroy(); + else + Stop(); + + yield break; + } + + yield return Timing.WaitForOneFrame; + } + } + + private void ResampleFrame() + { + int requiredSize = (int)(FrameSize * Mathf.Abs(Pitch) * 2) + 10; + + if (resampleBuffer.Length < requiredSize) + { + resampleBuffer = new float[requiredSize]; + resampleTime = 0.0; + resampleBufferFilled = 0; + } + + int outputIdx = 0; + + while (outputIdx < FrameSize) + { + if (resampleBufferFilled == 0) + { + int toRead = resampleBuffer.Length - 4; + int actualRead = source.Read(resampleBuffer, 0, toRead); + + if (actualRead == 0) + { + while (outputIdx < FrameSize) + frame[outputIdx++] = 0f; + return; + } + + resampleBufferFilled = actualRead; + resampleTime = 0.0; + } + + int currentSample = (int)resampleTime; + + if (currentSample >= resampleBufferFilled - 1) + { + if (resampleBufferFilled > 0) + { + resampleBuffer[0] = resampleBuffer[resampleBufferFilled - 1]; + + int toRead = resampleBuffer.Length - 5; + int actualRead = source.Read(resampleBuffer, 1, toRead); + + if (actualRead == 0) + { + while (outputIdx < FrameSize) + frame[outputIdx++] = 0f; + return; + } + + resampleBufferFilled = actualRead + 1; + resampleTime -= currentSample; + } + else + { + resampleBufferFilled = 0; + } + + continue; + } + + double frac = resampleTime - currentSample; + float sample1 = resampleBuffer[currentSample]; + float sample2 = resampleBuffer[currentSample + 1]; + + frame[outputIdx++] = (float)(sample1 + ((sample2 - sample1) * frac)); + + resampleTime += Pitch; + } + } + + private void SendPacket(int len) + { + AudioMessage msg = new(ControllerId, encoded, len); + + switch (PlayMode) + { + case SpeakerPlayMode.Global: + NetworkServer.SendToReady(msg, Channel); + break; + + case SpeakerPlayMode.Player: + TargetPlayer?.Connection.Send(msg, Channel); + break; + + case SpeakerPlayMode.PlayerList: + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + NetworkMessages.Pack(msg, writer); + ArraySegment segment = writer.ToArraySegment(); + + foreach (Player ply in TargetPlayers) + { + ply?.Connection.Send(segment, Channel); + } + } + + break; + + case SpeakerPlayMode.Predicate: + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + NetworkMessages.Pack(msg, writer); + ArraySegment segment = writer.ToArraySegment(); + + foreach (Player ply in Player.List) + { + if (Predicate(ply)) + ply.Connection.Send(segment, Channel); + } + } + + break; + } + } + + private void OnToyRemoved(AdminToyBase toy) + { + if (toy != Base) + return; + + AdminToyBase.OnRemoved -= OnToyRemoved; + + Stop(); + + encoder?.Dispose(); + } } -} +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Interfaces/IPcmSource.cs b/EXILED/Exiled.API/Interfaces/IPcmSource.cs new file mode 100644 index 0000000000..680f568410 --- /dev/null +++ b/EXILED/Exiled.API/Interfaces/IPcmSource.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Interfaces +{ + using System; + + /// + /// Represents a source of PCM audio data. + /// + public interface IPcmSource : IDisposable + { + /// + /// Gets a value indicating whether the end of the PCM source has been reached. + /// + bool Ended { get; } + + /// + /// Gets the total duration of the audio in seconds. + /// + double TotalDuration { get; } + + /// + /// Gets or sets the current playback position in seconds. + /// + double CurrentTime { get; set; } + + /// + /// Reads a sequence of PCM samples into the specified buffer. + /// + /// The buffer to read the samples into. + /// The zero-based index in the buffer at which to begin storing the data read from the source. + /// The maximum number of samples to read. + /// The total number of samples read into the buffer. + int Read(float[] buffer, int offset, int count); + + /// + /// Seeks to the specified position in the PCM source. + /// + /// The position in seconds to seek to. + void Seek(double seconds); + + /// + /// Resets the PCM source to its initial state, allowing reading from the beginning. + /// + void Reset(); + } +}