Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: DisCatSharp.Voice #331

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions DisCatSharp.Targets/InternalsVisibleTo.targets
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<InternalsVisibleTo Include="DisCatSharp.Support" />
<InternalsVisibleTo Include="DisCatSharp.Tests" />
<InternalsVisibleTo Include="DisCatSharp.SafetyTests.Internal" />
<InternalsVisibleTo Include="DisCatSharp.Voice" />
<InternalsVisibleTo Include="DisCatSharp.VoiceNext" />
<InternalsVisibleTo Include="DisCatSharp.VoiceNext.Natives" />
<InternalsVisibleTo Include="DisCatSharp.DevTools" />
Expand Down
70 changes: 42 additions & 28 deletions DisCatSharp.VoiceNext/VoiceNextConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
Expand Down Expand Up @@ -417,7 +418,7 @@ internal Task ConnectAsync()
{
Scheme = "wss",
Host = this.WebSocketEndpoint.Hostname,
Query = "encoding=json&v=4"
Query = "encoding=json&v=10"
};

return this._voiceWs.ConnectAsync(gwuri.Uri);
Expand Down Expand Up @@ -638,6 +639,7 @@ private async Task VoiceSenderTask()
/// <returns>A bool.</returns>
private bool ProcessPacket(ReadOnlySpan<byte> data, ref Memory<byte> opus, ref Memory<byte> pcm, IList<ReadOnlyMemory<byte>> pcmPackets, out AudioSender voiceSender, out AudioFormat outputFormat)
{
this._discord.Logger.LogDebug("Processing packet..");
voiceSender = null;
outputFormat = default;

Expand All @@ -648,6 +650,7 @@ private bool ProcessPacket(ReadOnlySpan<byte> data, ref Memory<byte> opus, ref M

if (!this._transmittingSsrCs.TryGetValue(ssrc, out var vtx))
{
this._discord.Logger.LogDebug("Don't know user yet, creating dummy for {ssrc}", ssrc);
var decoder = this._opus.CreateDecoder();

vtx = new AudioSender(ssrc, decoder)
Expand All @@ -656,29 +659,30 @@ private bool ProcessPacket(ReadOnlySpan<byte> data, ref Memory<byte> opus, ref M
User = null
};
}

voiceSender = vtx;
var sequence = vtx.GetTrueSequenceAfterWrapping(shortSequence);
ushort gap = 0;
if (vtx.LastTrueSequence is ulong lastTrueSequence)

try
{
if (sequence <= lastTrueSequence) // out-of-order packet; discard
return false;
voiceSender = vtx;
ushort gap = 0;
if (vtx.LastTrueSequence is ulong lastTrueSequence)
{
if (sequence <= lastTrueSequence) // out-of-order packet; discard
return false;

gap = (ushort)(sequence - 1 - lastTrueSequence);
if (gap >= 5)
this._discord.Logger.LogWarning(VoiceNextEvents.VoiceReceiveFailure, "5 or more voice packets were dropped when receiving");
}
gap = (ushort)(sequence - 1 - lastTrueSequence);
if (gap >= 5)
this._discord.Logger.LogWarning(VoiceNextEvents.VoiceReceiveFailure, "5 or more voice packets were dropped when receiving");
}

Span<byte> nonce = stackalloc byte[Sodium.NonceSize];
this._sodium.GetNonce(data, nonce, this._selectedEncryptionMode);
this._rtp.GetDataFromPacket(data, out var encryptedOpus, this._selectedEncryptionMode);
Span<byte> nonce = stackalloc byte[Sodium.NonceSize];
this._sodium.GetNonce(data, nonce, this._selectedEncryptionMode);
this._rtp.GetDataFromPacket(data, out var encryptedOpus, this._selectedEncryptionMode);

var opusSize = Sodium.CalculateSourceSize(encryptedOpus);
opus = opus[..opusSize];
var opusSpan = opus.Span;
try
{
var opusSize = Sodium.CalculateSourceSize(encryptedOpus);
opus = opus[..opusSize];
var opusSpan = opus.Span;

this._sodium.Decrypt(encryptedOpus, opusSpan, nonce);

// Strip extensions, if any
Expand Down Expand Up @@ -720,6 +724,7 @@ private bool ProcessPacket(ReadOnlySpan<byte> data, ref Memory<byte> opus, ref M

if (gap == 1)
{
this._discord.Logger.LogDebug("Gap 1");
var lastSampleCount = this._opus.GetLastPacketSampleCount(vtx.Decoder);
var fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)];
var fecpcmMem = fecpcm.AsSpan();
Expand All @@ -728,6 +733,7 @@ private bool ProcessPacket(ReadOnlySpan<byte> data, ref Memory<byte> opus, ref M
}
else if (gap > 1)
{
this._discord.Logger.LogDebug("Gap {gap}", gap);
var lastSampleCount = this._opus.GetLastPacketSampleCount(vtx.Decoder);
for (var i = 0; i < gap; i++)
{
Expand All @@ -741,6 +747,12 @@ private bool ProcessPacket(ReadOnlySpan<byte> data, ref Memory<byte> opus, ref M
var pcmSpan = pcm.Span;
this._opus.Decode(vtx.Decoder, opusSpan, ref pcmSpan, false, out outputFormat);
pcm = pcm[..pcmSpan.Length];
this._discord.Logger.LogDebug("Done with processing packet...");
}
catch (Exception ex)
{
this._discord.Logger.LogDebug("Exception in ProcessPacket: {ex}", ex.Message);
this._discord.Logger.LogDebug("Stack: {ex}", ex.StackTrace);
}
finally
{
Expand All @@ -762,6 +774,7 @@ private async Task ProcessVoicePacket(byte[] data)

try
{
this._discord.Logger.LogDebug("Received voice data preparing..");
var pcm = new byte[this.AudioFormat.CalculateMaximumFrameSize()];
var pcmMem = pcm.AsMemory();
var opus = new byte[pcm.Length];
Expand All @@ -770,6 +783,7 @@ private async Task ProcessVoicePacket(byte[] data)
if (!this.ProcessPacket(data, ref opusMem, ref pcmMem, pcmFillers, out var vtx, out var audioFormat))
return;

this._discord.Logger.LogDebug("Received voice data processing..");
foreach (var pcmFiller in pcmFillers)
await this._voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs(this._discord.ServiceProvider)
{
Expand Down Expand Up @@ -1140,19 +1154,19 @@ private async Task HandleDispatch(JObject jo)
switch (opc)
{
case 2: // READY
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)");
var vrp = opp.ToObject<VoiceReadyPayload>();
this._ssrc = vrp.Ssrc;
this.UdpEndpoint = new ConnectionEndpoint(vrp.Address, vrp.Port);
// this is not the valid interval
// oh, discord
//this.HeartbeatInterval = vrp.HeartbeatInterval;
this._heartbeatTask = Task.Run(this.HeartbeatAsync);
this._heartbeatTask = Task.Run(this.HeartbeatAsync, this.TOKEN);
await this.Stage1(vrp).ConfigureAwait(false);
break;

case 4: // SESSION_DESCRIPTION
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SESSION_DESCRIPTION (OP4)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received SESSION_DESCRIPTION (OP4)");
var vsd = opp.ToObject<VoiceSessionDescriptionPayload>();
this._key = vsd.SecretKey;
this._sodium = new Sodium(this._key.AsMemory());
Expand All @@ -1162,7 +1176,7 @@ private async Task HandleDispatch(JObject jo)
case 5: // SPEAKING
// Don't spam OP5
// No longer spam, Discord supposedly doesn't send many of these
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SPEAKING (OP5)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received SPEAKING (OP5): {oop}", opp.ToString(Formatting.Indented));
var spd = opp.ToObject<VoiceSpeakingPayload>();
var foundUserInCache = this._discord.TryGetCachedUserInternal(spd.UserId.Value, out var resolvedUser);
var spk = new UserSpeakingEventArgs(this._discord.ServiceProvider)
Expand Down Expand Up @@ -1201,17 +1215,17 @@ private async Task HandleDispatch(JObject jo)

case 8: // HELLO
// this sends a heartbeat interval that we need to use for heartbeating
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HELLO (OP8)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received HELLO (OP8)");
this._heartbeatInterval = opp["heartbeat_interval"].ToObject<int>();
break;

case 9: // RESUMED
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received RESUMED (OP9)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received RESUMED (OP9)");
this._heartbeatTask = Task.Run(this.HeartbeatAsync);
break;

case 12: // CLIENT_CONNECTED
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_CONNECTED (OP12)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received CLIENT_CONNECTED (OP12)");
var ujpd = opp.ToObject<VoiceUserJoinPayload>();
var usrj = await this._discord.GetUserAsync(ujpd.UserId, true).ConfigureAwait(false);
{
Expand All @@ -1229,7 +1243,7 @@ private async Task HandleDispatch(JObject jo)
break;

case 13: // CLIENT_DISCONNECTED
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_DISCONNECTED (OP13)");
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received CLIENT_DISCONNECTED (OP13)");
var ulpd = opp.ToObject<VoiceUserLeavePayload>();
var txssrc = this._transmittingSsrCs.FirstOrDefault(x => x.Value.Id == ulpd.UserId);
if (this._transmittingSsrCs.ContainsKey(txssrc.Key))
Expand All @@ -1247,7 +1261,7 @@ private async Task HandleDispatch(JObject jo)
break;

default:
this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received unknown voice opcode (OP{0})", opc);
this._discord.Logger.LogDebug(VoiceNextEvents.VoiceDispatch, "Received unknown voice opcode (OP {0}): {data}", opc, opp.ToString(Formatting.Indented));
break;
}
}
Expand Down
6 changes: 6 additions & 0 deletions DisCatSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.SafetyTests", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Lavalink", "DisCatSharp.Lavalink\DisCatSharp.Lavalink.csproj", "{1ADC1D06-3DB8-4741-B740-13161B40CBA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscatSharp.Voice", "DiscatSharp.Voice\DiscatSharp.Voice.csproj", "{69DF6B77-4A27-4D48-BB05-D25DF93A2AD0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -163,6 +165,10 @@ Global
{1ADC1D06-3DB8-4741-B740-13161B40CBA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1ADC1D06-3DB8-4741-B740-13161B40CBA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1ADC1D06-3DB8-4741-B740-13161B40CBA3}.Release|Any CPU.Build.0 = Release|Any CPU
{69DF6B77-4A27-4D48-BB05-D25DF93A2AD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69DF6B77-4A27-4D48-BB05-D25DF93A2AD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69DF6B77-4A27-4D48-BB05-D25DF93A2AD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69DF6B77-4A27-4D48-BB05-D25DF93A2AD0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 0 additions & 1 deletion DisCatSharp/Net/Rest/DiscordApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6026,7 +6026,6 @@ internal async Task<TransportApplication> ModifyCurrentApplicationInfoAsync(
ConverImageBase64 = coverImageb64,
Flags = flags,
InstallParams = installParams

};

var route = $"{Endpoints.APPLICATIONS}{Endpoints.ME}";
Expand Down
27 changes: 27 additions & 0 deletions DiscatSharp.Voice/Class1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

namespace DisCatSharp.Voice;
public class Class1
{

}
47 changes: 47 additions & 0 deletions DiscatSharp.Voice/DiscatSharp.Voice.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../DisCatSharp.Targets/Version.targets" />
<Import Project="../DisCatSharp.Targets/DisCatSharp.targets" />
<Import Project="../DisCatSharp.Targets/Package.targets" />
<Import Project="../DisCatSharp.Targets/NuGet.targets" />
<Import Project="../DisCatSharp.Targets/Library.targets" />
<Import Project="../DisCatSharp.Targets/InternalsVisibleTo.targets" />

<PropertyGroup>
<AssemblyName>DisCatSharp.Voice</AssemblyName>
<RootNamespace>DisCatSharp.Voice</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup>
<PackageId>DisCatSharp.Voice</PackageId>
<Description>
DisCatSharp Voice Extension

Easy made audio player for discord bots.

Documentation: https://docs.dcs.aitsys.dev/articles/modules/audio/voice/intro.html
</Description>
<PackageTags>DisCatSharp,Discord API Wrapper,Discord,Bots,Discord Bots,AITSYS,Net6,Net7,Voice,Audio Player</PackageTags>
<Nullable>annotations</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DisCatSharp.Analyzer.Roselyn" Version="5.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="DisCatSharp.Attributes" Version="10.4.2" />
<PackageReference Include="Microsoft.DependencyValidation.Analyzers" Version="0.11.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="System.Threading.Channels" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DisCatSharp\DisCatSharp.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DisCatSharp.VoiceNext.Natives\DisCatSharp.VoiceNext.Natives.csproj" />
</ItemGroup>
</Project>
Loading