Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
efebf22
Speaker Toy Api
MS-crew Jan 2, 2026
959fe93
.
MS-crew Jan 3, 2026
0c798d4
chore: trigger CI
MS-crew Jan 3, 2026
f02fd3b
added missing doc
MS-crew Jan 3, 2026
b93a350
Change default stream parameter to false in PlayWav
MS-crew Jan 3, 2026
63fc8bf
Improve XML documentation for Channel property
MS-crew Jan 3, 2026
a2d006e
Update WavStreamSource.cs
MS-crew Jan 3, 2026
67b3348
Update Speaker.cs
MS-crew Jan 3, 2026
21c91cc
Update Speaker.cs
MS-crew Jan 3, 2026
052e46d
Update performance
MS-crew Jan 3, 2026
51298ee
Update Speaker.cs
MS-crew Jan 4, 2026
3f7ce95
Implemented audio seeking, event system, and precision timing
MS-crew Jan 5, 2026
9285154
Update Speaker.cs
MS-crew Jan 5, 2026
072aba8
Update WavUtility.cs
MS-crew Jan 6, 2026
6b97e0f
Update Speaker.cs
MS-crew Jan 6, 2026
49df481
Update Speaker.cs
MS-crew Jan 6, 2026
ccca1ab
Update Speaker.cs
MS-crew Jan 6, 2026
85193d9
Update Speaker.cs
MS-crew Jan 6, 2026
89fe70d
Added OnPlaybackLooped event
MS-crew Jan 7, 2026
ae9992f
Update Speaker.cs
MS-crew Jan 11, 2026
889ca19
Update to c# lang version 14
MS-crew Jan 12, 2026
895fba8
Update Speaker.cs
MS-crew Jan 12, 2026
18b30bc
Merge branch 'dev' into SpeakerToyReborn
MS-crew Jan 12, 2026
3abd0b1
Enum 4 byte to 1 byte
MS-crew Jan 12, 2026
88497de
-Replaced 'File.ReadAllBytes' with 'ArrayPool<byte>.Shared'
MS-crew Jan 12, 2026
ae2103f
Refactor
MS-crew Jan 12, 2026
9457f02
Merge remote-tracking branch 'upstream/dev' into SpeakerToyReborn
MS-crew Jan 14, 2026
1bc0db9
Initialize Channel property with ReliableOrdered2
MS-crew Jan 15, 2026
d6cf318
Refactor Speaker class properties
MS-crew Jan 15, 2026
d17ca36
Update Speaker.cs
MS-crew Jan 15, 2026
2b1200c
Update Speaker.cs
MS-crew Jan 15, 2026
a69a69f
Merge branch 'SpeakerToyReborn' of https://github.com/MS-crew/EXILEDP…
MS-crew Jan 16, 2026
974b3f1
ağhhh
MS-crew Jan 16, 2026
e8348b0
Merge branch 'dev' into SpeakerToyReborn
MS-crew Jan 16, 2026
e284fdd
renamed method
MS-crew Jan 16, 2026
bb82462
Merge branch 'SpeakerToyReborn' of https://github.com/MS-crew/EXILEDP…
MS-crew Jan 16, 2026
83494fa
Added Target Player Play Mode
MS-crew Jan 17, 2026
60ae713
.
MS-crew Jan 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion EXILED/EXILED.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<PropertyGroup Condition="$(BuildProperties) == '' OR $(BuildProperties) == 'true'">
<TargetFramework>net48</TargetFramework>
<LangVersion>13.0</LangVersion>
<LangVersion>14.0</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>$(MSBuildThisFileDirectory)\bin\$(Configuration)\</OutputPath>
Expand Down
35 changes: 35 additions & 0 deletions EXILED/Exiled.API/Enums/SpeakerPlayMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// -----------------------------------------------------------------------
// <copyright file="SpeakerPlayMode.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.API.Enums
{
/// <summary>
/// Specifies the available modes for playing audio through a speaker.
/// </summary>
public enum SpeakerPlayMode : byte
{
/// <summary>
/// Play audio globally to all players.
/// </summary>
Global = 0,

/// <summary>
/// Play audio to a specific player.
/// </summary>
Player = 1,

/// <summary>
/// Play audio to a specific list of players.
/// </summary>
PlayerList = 2,

/// <summary>
/// Play audio to players matching a predicate.
/// </summary>
Predicate = 3,
}
}
114 changes: 114 additions & 0 deletions EXILED/Exiled.API/Features/Audio/PreloadedPcmSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// -----------------------------------------------------------------------
// <copyright file="PreloadedPcmSource.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.API.Features.Audio
{
using System;

using Exiled.API.Interfaces;

using VoiceChat;

/// <summary>
/// Represents a preloaded PCM audio source.
/// </summary>
public sealed class PreloadedPcmSource : IPcmSource
{
/// <summary>
/// The PCM data buffer.
/// </summary>
private readonly float[] data;

/// <summary>
/// The current read position in the data buffer.
/// </summary>
private int pos;

/// <summary>
/// Initializes a new instance of the <see cref="PreloadedPcmSource"/> class.
/// </summary>
/// <param name="path">The path to the audio file.</param>
public PreloadedPcmSource(string path)
{
data = WavUtility.WavToPcm(path);
}

/// <summary>
/// Initializes a new instance of the <see cref="PreloadedPcmSource"/> class.
/// </summary>
/// <param name="pcmData">The raw PCM float array.</param>
public PreloadedPcmSource(float[] pcmData)
{
data = pcmData;
}

/// <summary>
/// Gets a value indicating whether the end of the PCM data buffer has been reached.
/// </summary>
public bool Ended => pos >= data.Length;

/// <summary>
/// Gets the total duration of the audio in seconds.
/// </summary>
public double TotalDuration => (double)data.Length / VoiceChatSettings.SampleRate;

/// <summary>
/// Gets or sets the current playback position in seconds.
/// </summary>
public double CurrentTime
{
get => (double)pos / VoiceChatSettings.SampleRate;
set => Seek(value);
}

/// <summary>
/// Reads a sequence of PCM samples from the preloaded buffer into the specified array.
/// </summary>
/// <param name="buffer">The destination array to copy the samples into.</param>
/// <param name="offset">The zero-based index in <paramref name="buffer"/> at which to begin storing the data.</param>
/// <param name="count">The maximum number of samples to read.</param>
/// <returns>The number of samples read into <paramref name="buffer"/>.</returns>
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;
}

/// <summary>
/// Seeks to the specified position in seconds.
/// </summary>
/// <param name="seconds">The target position in seconds.</param>
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;
}

/// <summary>
/// Resets the read position to the beginning of the PCM data buffer.
/// </summary>
public void Reset()
{
pos = 0;
}

/// <inheritdoc/>
public void Dispose()
{
}
}
}
142 changes: 142 additions & 0 deletions EXILED/Exiled.API/Features/Audio/WavStreamSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// -----------------------------------------------------------------------
// <copyright file="WavStreamSource.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.API.Features.Audio
{
using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;

using Exiled.API.Interfaces;

using VoiceChat;

/// <summary>
/// Provides a PCM audio source from a WAV file stream.
/// </summary>
public sealed class WavStreamSource : IPcmSource
{
private readonly long endPosition;
private readonly long startPosition;
private readonly FileStream stream;

private byte[] internalBuffer;

/// <summary>
/// Initializes a new instance of the <see cref="WavStreamSource"/> class.
/// </summary>
/// <param name="path">The path to the audio file.</param>
public WavStreamSource(string path)
{
stream = File.OpenRead(path);
WavUtility.SkipHeader(stream);
startPosition = stream.Position;
endPosition = stream.Length;
internalBuffer = ArrayPool<byte>.Shared.Rent(VoiceChatSettings.PacketSizePerChannel * 2);
}

/// <summary>
/// Gets the total duration of the audio in seconds.
/// </summary>
public double TotalDuration => (endPosition - startPosition) / 2.0 / VoiceChatSettings.SampleRate;

/// <summary>
/// Gets or sets the current playback position in seconds.
/// </summary>
public double CurrentTime
{
get => (stream.Position - startPosition) / 2.0 / VoiceChatSettings.SampleRate;
set => Seek(value);
}

/// <summary>
/// Gets a value indicating whether the end of the stream has been reached.
/// </summary>
public bool Ended => stream.Position >= endPosition;

/// <summary>
/// Reads PCM data from the stream into the specified buffer.
/// </summary>
/// <param name="buffer">The buffer to fill with PCM data.</param>
/// <param name="offset">The offset in the buffer at which to begin writing.</param>
/// <param name="count">The maximum number of samples to read.</param>
/// <returns>The number of samples read.</returns>
public int Read(float[] buffer, int offset, int count)
{
int bytesNeeded = count * 2;

if (internalBuffer.Length < bytesNeeded)
{
ArrayPool<byte>.Shared.Return(internalBuffer);
internalBuffer = ArrayPool<byte>.Shared.Rent(bytesNeeded);
}

int bytesRead = stream.Read(internalBuffer, 0, bytesNeeded);

if (bytesRead == 0)
return 0;

if (bytesRead % 2 != 0)
bytesRead--;

Span<byte> byteSpan = internalBuffer.AsSpan(0, bytesRead);
Span<short> shortSpan = MemoryMarshal.Cast<byte, short>(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] / 32768f;

return samplesToWrite;
}

/// <summary>
/// Seeks to the specified position in the stream.
/// </summary>
/// <param name="seconds">The position in seconds to seek to.</param>
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;
}

/// <summary>
/// Resets the stream position to the start.
/// </summary>
public void Reset()
{
stream.Position = startPosition;
}

/// <summary>
/// Releases all resources used by the <see cref="WavStreamSource"/>.
/// </summary>
public void Dispose()
{
stream?.Dispose();
if (internalBuffer != null)
{
ArrayPool<byte>.Shared.Return(internalBuffer);
internalBuffer = null;
}
}
}
}
Loading
Loading