Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions EXILED/Exiled.API/Enums/SpeakerPlayMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------
// <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 list of players.
/// </summary>
PlayerList = 1,

/// <summary>
/// Play audio to players matching a predicate.
/// </summary>
Predicate = 2,
}
}
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()
{
}
}
}
140 changes: 140 additions & 0 deletions EXILED/Exiled.API/Features/Audio/WavStreamSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// -----------------------------------------------------------------------
// <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 BinaryReader reader;

/// <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)
{
reader = new BinaryReader(File.OpenRead(path));
WavUtility.SkipHeader(reader);
startPosition = reader.BaseStream.Position;
endPosition = reader.BaseStream.Length;
}

/// <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 => (reader.BaseStream.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 => reader.BaseStream.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;

byte[] tempBuffer = ArrayPool<byte>.Shared.Rent(bytesNeeded);

try
{
int bytesRead = reader.Read(tempBuffer, 0, bytesNeeded);

if (bytesRead == 0)
return 0;

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

Span<byte> byteSpan = tempBuffer.AsSpan(0, bytesRead);
Span<short> shortSpan = MemoryMarshal.Cast<byte, short>(byteSpan);

int samplesRead = shortSpan.Length;
for (int i = 0; i < samplesRead; i++)
{
if (offset + i >= buffer.Length)
break;

buffer[offset + i] = shortSpan[i] / 32768f;
}

return samplesRead;
}
finally
{
ArrayPool<byte>.Shared.Return(tempBuffer);
}
}

/// <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--;

reader.BaseStream.Position = newPos;
}

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

/// <summary>
/// Releases all resources used by the <see cref="WavStreamSource"/>.
/// </summary>
public void Dispose()
{
reader.Dispose();
}
}
}
Loading
Loading