Skip to content

Commit

Permalink
One step closer to sending audio
Browse files Browse the repository at this point in the history
  • Loading branch information
OoLunar committed Jun 24, 2024
1 parent f752748 commit 903a09a
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 25 deletions.
15 changes: 14 additions & 1 deletion examples/HelloWorld/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using DSharpPlus.VoiceLink.AudioCodecs;
using DSharpPlus.VoiceLink.Enums;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
Expand Down Expand Up @@ -62,14 +63,26 @@ public static async Task Main()
clientBuilder.ConfigureLogging(builder => builder.AddSerilog());

DiscordClient client = clientBuilder.Build();
VoiceLinkExtension voiceLinkExtension = client.UseVoiceLink();
VoiceLinkExtension voiceLinkExtension = client.UseVoiceLink(new()
{
AudioCodecFactory = (serviceProvider) => new OpusAudioCodec()
});

voiceLinkExtension.UserSpeaking += async (sender, e) =>
{
FileStream fileStream = File.Open($"test/{e.Member.Id}.pcm", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
await e.VoiceUser.AudioStream.CopyToAsync(fileStream);
await fileStream.DisposeAsync();
};

voiceLinkExtension.ConnectionCreated += async (sender, e) =>
{
FileStream fileStream = File.Open("res/test.opus", FileMode.Open, FileAccess.Read, FileShare.Read);
_ = fileStream.CopyToAsync(e.Connection.AudioInput.AsStream(true));
await e.Connection.StartSpeakingAsync();
await fileStream.DisposeAsync();
};

client.GuildDownloadCompleted += async (sender, e) =>

Check warning on line 86 in examples/HelloWorld/src/Program.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'DiscordClient.GuildDownloadCompleted' is obsolete: 'Events on DiscordClient are deprecated and will be removed within the v5 development cycle. Please use the ConfigureEventHandlers methods on your preferred construction method instead.'

Check warning on line 86 in examples/HelloWorld/src/Program.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'DiscordClient.GuildDownloadCompleted' is obsolete: 'Events on DiscordClient are deprecated and will be removed within the v5 development cycle. Please use the ConfigureEventHandlers methods on your preferred construction method instead.'

Check warning on line 86 in examples/HelloWorld/src/Program.cs

View workflow job for this annotation

GitHub Actions / Document Commit

'DiscordClient.GuildDownloadCompleted' is obsolete: 'Events on DiscordClient are deprecated and will be removed within the v5 development cycle. Please use the ConfigureEventHandlers methods on your preferred construction method instead.'

Check warning on line 86 in examples/HelloWorld/src/Program.cs

View workflow job for this annotation

GitHub Actions / Document Commit

'DiscordClient.GuildDownloadCompleted' is obsolete: 'Events on DiscordClient are deprecated and will be removed within the v5 development cycle. Please use the ConfigureEventHandlers methods on your preferred construction method instead.'
{
if (!ulong.TryParse(Environment.GetEnvironmentVariable("DISCORD_GUILD"), out ulong guildId))
Expand Down
Binary file added res/test.opus
Binary file not shown.
Binary file added res/test.raw
Binary file not shown.
4 changes: 3 additions & 1 deletion src/DSharpPlus.VoiceLink/AudioCodecs/IAudioCodec.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Buffers;

namespace DSharpPlus.VoiceLink.AudioCodecs
{
public delegate IAudioCodec AudioCodecFactory(IServiceProvider serviceProvider);
public interface IAudioCodec
{
public int GetMaxBufferSize();
public int Decode(bool hasPacketLoss, ReadOnlySpan<byte> input, Span<byte> output);
public int EncodeOpus(ReadOnlySequence<byte> input, Span<byte> output);
public int DecodeOpus(bool hasPacketLoss, ReadOnlySpan<byte> input, Span<byte> output);
}
}
23 changes: 19 additions & 4 deletions src/DSharpPlus.VoiceLink/AudioCodecs/OpusAudioCodec.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
using System;
using System.Buffers;

namespace DSharpPlus.VoiceLink.AudioCodecs
{
public class OpusAudioCodec : IAudioCodec
{
private const int CHANNELS = 2;
private const int MAX_FRAME_SIZE = 5760;
private const int MAX_BUFFER_SIZE = MAX_FRAME_SIZE * 2 * CHANNELS;
public const int CHANNELS = 2;
public const int MAX_FRAME_SIZE = 5760;
public const int MAX_BUFFER_SIZE = MAX_FRAME_SIZE * 2 * CHANNELS;
public static readonly byte[] SilenceFrame = [0xF8, 0xFF, 0xFE];

public int GetMaxBufferSize() => MAX_BUFFER_SIZE;
public int Decode(bool hasPacketLoss, ReadOnlySpan<byte> input, Span<byte> output)
public int DecodeOpus(bool hasPacketLoss, ReadOnlySpan<byte> input, Span<byte> output)
{
if (hasPacketLoss)
{
SilenceFrame.AsSpan().CopyTo(output);
return SilenceFrame.Length;
}

input.CopyTo(output);
return input.Length;
}

public int EncodeOpus(ReadOnlySequence<byte> input, Span<byte> output)
{
int frameSize = (int)Math.Min(input.Length, MAX_FRAME_SIZE);
input.Slice(0, frameSize).CopyTo(output);
return frameSize;
}
}
}
27 changes: 24 additions & 3 deletions src/DSharpPlus.VoiceLink/AudioCodecs/Pcm16BitAudioCodec.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using DSharpPlus.VoiceLink.Opus;

namespace DSharpPlus.VoiceLink.AudioCodecs
Expand All @@ -17,23 +19,42 @@ public class Pcm16BitAudioCodec : IAudioCodec
// 960 samples
private const int FRAME_SIZE = (int)(SAMPLE_RATE * FRAME_DURATION);

// 20 milliseconds of audio data, 3840 bytes
// 20 milliseconds of audio data, 1920 bytes
private const int SINGLE_CHANNEL_BUFFER_SIZE = FRAME_SIZE * BYTES_PER_SAMPLE;

public int Channels { get; init; }
public int BufferSize { get; init; }
private OpusEncoder _opusEncoder { get; init; }
private OpusDecoder _opusDecoder { get; init; }

public Pcm16BitAudioCodec(int channels = 2)
{
Channels = channels;
BufferSize = SINGLE_CHANNEL_BUFFER_SIZE * Channels;
_opusDecoder = OpusDecoder.Create(OpusSampleRate.Opus48000Hz, channels);
}

/// <inheritdoc/>
public int GetMaxBufferSize() => SINGLE_CHANNEL_BUFFER_SIZE * Channels;
public int GetMaxBufferSize() => BufferSize;

/// <inheritdoc/>
public int Decode(bool hasPacketLoss, ReadOnlySpan<byte> input, Span<byte> output)
public int EncodeOpus(ReadOnlySequence<byte> input, Span<byte> output)
{
int sliceSize = Math.Min((int)input.Length, BufferSize);
byte[] buffer = ArrayPool<byte>.Shared.Rent(sliceSize);
try
{
input.Slice(0, sliceSize).CopyTo(buffer);
return _opusEncoder.Encode(Unsafe.As<byte[], short[]>(ref buffer), FRAME_SIZE, ref output);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

/// <inheritdoc/>
public int DecodeOpus(bool hasPacketLoss, ReadOnlySpan<byte> input, Span<byte> output)
{
_opusDecoder.Decode(input, output, FRAME_SIZE, hasPacketLoss);
return output.Length;
Expand Down
2 changes: 0 additions & 2 deletions src/DSharpPlus.VoiceLink/Opus/OpusEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public unsafe int Encode(ReadOnlySpan<short> pcm, int frameSize, ref Span<byte>
}

// Trim the data to the encoded length
data = data[..encodedLength];
return encodedLength;
}

Expand All @@ -73,7 +72,6 @@ public unsafe int EncodeFloat(ReadOnlySpan<float> pcm, int frameSize, ref Span<b
}

// Trim the data to the encoded length
data = data[..encodedLength];
return encodedLength;
}

Expand Down
3 changes: 2 additions & 1 deletion src/DSharpPlus.VoiceLink/Payloads/VoiceSpeakingPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ namespace DSharpPlus.VoiceLink.Payloads
{
public sealed record VoiceSpeakingPayload
{
public required ulong UserId { get; init; }
public ulong UserId { get; init; }
public required uint Ssrc { get; init; }
public required VoiceSpeakingIndicators Speaking { get; init; }
public required int Delay { get; init; }
}
}
2 changes: 1 addition & 1 deletion src/DSharpPlus.VoiceLink/Rtp/RtcpUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public static class RtcpUtilities
/// </summary>
/// <param name="source">The data to reference.</param>
/// <returns>Whether the data contains a valid RTCP header.</returns>
public static bool IsRtcpReceiverReport(ReadOnlySpan<byte> source) => source.Length >= 8 && source[1] == 201;
public static bool HasRtcpReceiverReport(ReadOnlySpan<byte> source) => source.Length >= 8 && source[1] == 201;

public static RtcpHeader DecodeHeader(ReadOnlySpan<byte> source)
{
Expand Down
10 changes: 5 additions & 5 deletions src/DSharpPlus.VoiceLink/Rtp/RtpUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class RtpUtilities
/// </summary>
/// <param name="source">The data to reference.</param>
/// <returns>Whether the data contains a valid RTP header.</returns>
public static bool IsRtpHeader(ReadOnlySpan<byte> source) => source.Length >= 12 && (source[0] == Version || source[0] == VersionWithExtension) && source[1] == DiscordPayloadType;
public static bool HasRtpHeader(ReadOnlySpan<byte> source) => source.Length >= 12 && (source[0] == Version || source[0] == VersionWithExtension) && source[1] == DiscordPayloadType;

/// <summary>
/// Encodes a RTP header into the given buffer.
Expand All @@ -29,7 +29,7 @@ public static class RtpUtilities
/// <param name="ssrc">The srrc of the audio frame.</param>
/// <param name="target">Which buffer to write to.</param>
/// <exception cref="ArgumentException">The target buffer must have a minimum of 12 bytes for the RTP header to fit.</exception>
public static void EncodeHeader(ushort sequence, uint timestamp, uint ssrc, Span<byte> target)
public static void EncodeHeader(RtpHeader header, Span<byte> target)
{
if (target.Length < 12)
{
Expand All @@ -39,9 +39,9 @@ public static void EncodeHeader(ushort sequence, uint timestamp, uint ssrc, Span
target.Clear();
target[0] = Version;
target[1] = DiscordPayloadType;
BinaryPrimitives.WriteUInt16BigEndian(target[2..4], sequence);
BinaryPrimitives.WriteUInt32BigEndian(target[4..8], timestamp);
BinaryPrimitives.WriteUInt32BigEndian(target[8..12], ssrc);
BinaryPrimitives.WriteUInt16BigEndian(target[2..4], header.Sequence);
BinaryPrimitives.WriteUInt32BigEndian(target[4..8], header.Timestamp);
BinaryPrimitives.WriteUInt32BigEndian(target[8..12], header.Ssrc);
}

/// <summary>
Expand Down
8 changes: 7 additions & 1 deletion src/DSharpPlus.VoiceLink/VoiceLinkConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using DSharpPlus.VoiceLink.AudioCodecs;
using DSharpPlus.VoiceLink.VoiceEncryptionCiphers;

Expand All @@ -11,13 +12,18 @@ public sealed record VoiceLinkConfiguration
public int MaxHeartbeatQueueSize { get; set; } = 5;

/// <summary>
///
/// Which voice encryption cipher to use for voice data encryption/decryption.
/// </summary>
public IVoiceEncryptionCipher VoiceEncryptionCipher { get; set; } = new XSalsa20Poly1305EncryptionCipher();

/// <summary>
/// A delegate which creates a new audio codec instance. The audio codec is responsible for encoding and decoding audio data into the user's desired format.
/// </summary>
public AudioCodecFactory AudioCodecFactory { get; set; } = _ => new Pcm16BitAudioCodec();

/// <summary>
/// When <see cref="VoiceLinkConnection.StartSpeakingAsync"/> should timeout after attempting to read <see cref="VoiceLinkConnection.AudioInput"/> for too long.
/// </summary>
public TimeSpan SpeakingTimeout { get; set; } = TimeSpan.FromMilliseconds(200);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ private static async ValueTask ReadyAsync(VoiceLinkConnection connection, ReadRe
VoiceReadyPayload voiceReadyPayload = connection._websocketPipe.Reader.Parse<VoiceGatewayDispatch<VoiceReadyPayload>>(result).Data;

// Insert our SSRC code
connection._logger.LogDebug("Connection {GuildId}: Bot's SSRC code is {Ssrc}.", connection.Guild.Id, voiceReadyPayload.Ssrc);
connection._ssrc = voiceReadyPayload.Ssrc;
connection._speakers.Add(voiceReadyPayload.Ssrc, new(connection, voiceReadyPayload.Ssrc, connection.Member, connection._audioDecoderFactory(connection.Extension.Client.ServiceProvider)));
connection._logger.LogDebug("Connection {GuildId}: Bot's SSRC code is {Ssrc}.", connection.Guild.Id, voiceReadyPayload.Ssrc);

// Setup UDP while also doing ip discovery
connection._logger.LogDebug("Connection {GuildId}: Setting up UDP, sending ip discovery...", connection.Guild.Id);
Expand Down
Loading

0 comments on commit 903a09a

Please sign in to comment.