diff --git a/Documentation/guides/basic-concepts/Voice/VoiceModule.cs b/Documentation/guides/basic-concepts/Voice/VoiceModule.cs index 8901f99d..4bf47893 100644 --- a/Documentation/guides/basic-concepts/Voice/VoiceModule.cs +++ b/Documentation/guides/basic-concepts/Voice/VoiceModule.cs @@ -129,7 +129,6 @@ public async Task EchoAsync() voiceState.ChannelId.GetValueOrDefault(), new VoiceClientConfiguration { - ReceiveHandler = new VoiceReceiveHandler(), // Required to receive voice Logger = new ConsoleLogger(), }); @@ -141,14 +140,9 @@ public async Task EchoAsync() voiceClient.VoiceReceive += args => { - // If the timestamp is null, the packet was lost. - // We skip it, which mirrors the packet loss to the echo recipients. - if (args.Timestamp is not { } timestamp) - return default; - - // Pass current user voice directly to SendAsync to create echo + // Send the received voice back if the received voice is from the user that invoked the command if (voiceClient.Cache.SsrcUsers.TryGetValue(args.Ssrc, out var voiceUserId) && voiceUserId == userId) - voiceClient.SendVoice(args.SequenceNumber, timestamp, args.Frame); + voiceClient.SendVoice(args.SequenceNumber, args.Timestamp, args.Frame); return default; }; diff --git a/Documentation/guides/basic-concepts/voice.md b/Documentation/guides/basic-concepts/voice.md index 7d1700de..605af76c 100644 --- a/Documentation/guides/basic-concepts/voice.md +++ b/Documentation/guides/basic-concepts/voice.md @@ -13,4 +13,4 @@ Follow the [installation guide](installing-native-dependencies.md) to install th [!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L13-L110)] ### Receiving Voice -[!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L112-L158)] +[!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L112-L152)] diff --git a/NetCord/Gateway/Voice/EventArgs/VoiceReceiveEventArgs.cs b/NetCord/Gateway/Voice/EventArgs/VoiceReceiveEventArgs.cs index 3fa1834a..c8f9d11c 100644 --- a/NetCord/Gateway/Voice/EventArgs/VoiceReceiveEventArgs.cs +++ b/NetCord/Gateway/Voice/EventArgs/VoiceReceiveEventArgs.cs @@ -1,26 +1,35 @@ +using System.Runtime.InteropServices; + namespace NetCord.Gateway.Voice; -public readonly ref struct VoiceReceiveEventArgs(byte[]? buffer, int frameIndex, int frameLength, uint ssrc, uint? timestamp, ushort sequenceNumber) +[StructLayout(LayoutKind.Auto)] +public readonly ref struct VoiceReceiveEventArgs { - internal readonly byte[]? _buffer = buffer; + public VoiceReceiveEventArgs(ReadOnlySpan frame, uint ssrc, uint timestamp, ushort sequenceNumber) + { + Frame = frame; + Ssrc = ssrc; + Timestamp = timestamp; + SequenceNumber = sequenceNumber; + } /// /// The voice frame data. /// - public ReadOnlySpan Frame => new(_buffer, frameIndex, frameLength); + public readonly ReadOnlySpan Frame { get; } /// /// The synchronization source (SSRC) of the sender of the voice frame. /// - public uint Ssrc => ssrc; + public readonly uint Ssrc { get; } /// - /// The timestamp of the voice frame. when the frame was lost. + /// The timestamp of the voice frame. /// - public uint? Timestamp => timestamp; + public readonly uint Timestamp { get; } /// /// The sequence number of the voice frame. /// - public ushort SequenceNumber => sequenceNumber; + public readonly ushort SequenceNumber { get; } } diff --git a/NetCord/Gateway/Voice/IVoiceReceiveHandler.cs b/NetCord/Gateway/Voice/IVoiceReceiveHandler.cs deleted file mode 100644 index 225d1e59..00000000 --- a/NetCord/Gateway/Voice/IVoiceReceiveHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NetCord.Gateway.Voice; - -public interface IVoiceReceiveHandler -{ - public bool RequiresExternalSocketAddress { get; } - - public VoicePacketHandlingResult HandlePacket(VoiceClient client, RtpPacket packet); -} diff --git a/NetCord/Gateway/Voice/NullVoiceReceiveHandler.cs b/NetCord/Gateway/Voice/NullVoiceReceiveHandler.cs deleted file mode 100644 index ecf74e77..00000000 --- a/NetCord/Gateway/Voice/NullVoiceReceiveHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace NetCord.Gateway.Voice; - -public class NullVoiceReceiveHandler : IVoiceReceiveHandler -{ - public static NullVoiceReceiveHandler Instance { get; } = new(); - - private NullVoiceReceiveHandler() - { - } - - public bool RequiresExternalSocketAddress => false; - - public VoicePacketHandlingResult HandlePacket(VoiceClient client, RtpPacket packet) - { - return default; - } -} diff --git a/NetCord/Gateway/Voice/OpusDecoder.cs b/NetCord/Gateway/Voice/OpusDecoder.cs index 3405689d..df01e5c0 100644 --- a/NetCord/Gateway/Voice/OpusDecoder.cs +++ b/NetCord/Gateway/Voice/OpusDecoder.cs @@ -33,12 +33,13 @@ public OpusDecoder(VoiceChannels channels) /// Input payload. Use to indicate packet loss. /// Output signal. /// Number of samples per channel in the output signal. + /// Whether to decode using forward error correction data, if available. /// The number of decoded samples per channel. - public int Decode(ReadOnlySpan data, Span pcm, int frameSize) + public int Decode(ReadOnlySpan data, Span pcm, int frameSize, bool decodeFec) { ValidatePcm(pcm.Length, frameSize, PcmFormat.Short, _channels, nameof(pcm)); - int result = OpusDecode(_decoder, data, data.Length, pcm, frameSize, 0); + int result = OpusDecode(_decoder, data, data.Length, pcm, frameSize, decodeFec ? 1 : 0); ValidateResult(result); @@ -51,10 +52,11 @@ public int Decode(ReadOnlySpan data, Span pcm, int frameSize) /// Input payload. Use to indicate packet loss. /// Output signal. /// Number of samples per channel in the output signal. + /// Whether to decode using forward error correction data, if available. /// The number of decoded samples per channel. - public int Decode(ReadOnlySpan data, Span pcm, int frameSize) + public int Decode(ReadOnlySpan data, Span pcm, int frameSize, bool decodeFec) { - return Decode(data, MemoryMarshal.AsBytes(pcm), frameSize); + return Decode(data, MemoryMarshal.AsBytes(pcm), frameSize, decodeFec); } /// @@ -63,12 +65,13 @@ public int Decode(ReadOnlySpan data, Span pcm, int frameSize) /// Input payload. Use to indicate packet loss. /// Output signal. /// Number of samples per channel in the output signal. + /// Whether to decode using forward error correction data, if available. /// The number of decoded samples per channel. - public int DecodeFloat(ReadOnlySpan data, Span pcm, int frameSize) + public int DecodeFloat(ReadOnlySpan data, Span pcm, int frameSize, bool decodeFec) { ValidatePcm(pcm.Length, frameSize, PcmFormat.Float, _channels, nameof(pcm)); - int result = OpusDecodeFloat(_decoder, data, data.Length, pcm, frameSize, 0); + int result = OpusDecodeFloat(_decoder, data, data.Length, pcm, frameSize, decodeFec ? 1 : 0); ValidateResult(result); @@ -81,10 +84,11 @@ public int DecodeFloat(ReadOnlySpan data, Span pcm, int frameSize) /// Input payload. Use to indicate packet loss. /// Output signal. /// Number of samples per channel in the output signal. + /// Whether to decode using forward error correction data, if available. /// The number of decoded samples per channel. - public int DecodeFloat(ReadOnlySpan data, Span pcm, int frameSize) + public int DecodeFloat(ReadOnlySpan data, Span pcm, int frameSize, bool decodeFec) { - return DecodeFloat(data, MemoryMarshal.AsBytes(pcm), frameSize); + return DecodeFloat(data, MemoryMarshal.AsBytes(pcm), frameSize, decodeFec); } public void Dispose() diff --git a/NetCord/Gateway/Voice/Streams/OpusDecodeStream.cs b/NetCord/Gateway/Voice/Streams/OpusDecodeStream.cs index 5dbbd210..3d57a739 100644 --- a/NetCord/Gateway/Voice/Streams/OpusDecodeStream.cs +++ b/NetCord/Gateway/Voice/Streams/OpusDecodeStream.cs @@ -36,12 +36,12 @@ public OpusDecodeStream(Stream next, PcmFormat format, VoiceChannels channels, b private int Decode(ReadOnlySpan data, Span pcm) { - return _decoder.Decode(data, pcm, _frameSize); + return _decoder.Decode(data, pcm, _frameSize, false); } private int DecodeFloat(ReadOnlySpan data, Span pcm) { - return _decoder.DecodeFloat(data, pcm, _frameSize); + return _decoder.DecodeFloat(data, pcm, _frameSize, false); } public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) diff --git a/NetCord/Gateway/Voice/VoiceClient.cs b/NetCord/Gateway/Voice/VoiceClient.cs index c312eec7..e802bfa8 100644 --- a/NetCord/Gateway/Voice/VoiceClient.cs +++ b/NetCord/Gateway/Voice/VoiceClient.cs @@ -105,7 +105,6 @@ public void Dispose() private readonly IUdpConnectionProvider _udpConnectionProvider; private readonly IVoiceEncryptionProvider _encryptionProvider; - private readonly IVoiceReceiveHandler _receiveHandler; private readonly TimeSpan _externalSocketAddressDiscoveryTimeout; private readonly GCHandle _loggerHandle; @@ -124,7 +123,6 @@ public void Dispose() Cache = cacheProvider.Create(); _udpConnectionProvider = configuration.UdpConnectionProvider ?? UdpConnectionProvider.Instance; _encryptionProvider = configuration.EncryptionProvider ?? VoiceEncryptionProvider.Instance; - _receiveHandler = configuration.ReceiveHandler ?? NullVoiceReceiveHandler.Instance; _externalSocketAddressDiscoveryTimeout = configuration.ExternalSocketAddressDiscoveryTimeout ?? new(5 * TimeSpan.TicksPerSecond); _loggerHandle = new(_logger); } @@ -312,8 +310,8 @@ private async ValueTask HandleJsonPayloadAsync(State state, ConnectionState conn var updateLatencyTask = UpdateLatencyAsync(latency).ConfigureAwait(false); var ready = payload.Data.GetValueOrDefault().ToObject(Serialization.Default.JsonReady); - var (ip, port) = (ready.Ip, ready.Port); - var udpConnection = _udpConnectionProvider.CreateConnection(ip, port); + var (serverIp, serverPort) = (ready.Ip, ready.Port); + var udpConnection = _udpConnectionProvider.CreateConnection(serverIp, serverPort); var encryption = _encryptionProvider.GetEncryption(ready.Modes); UdpState newUdpState; @@ -344,23 +342,23 @@ private async ValueTask HandleJsonPayloadAsync(State state, ConnectionState conn var buffer = ArrayPool.Shared.Rent(ushort.MaxValue); + string? externalIp; + ushort externalPort; + try { - if (_receiveHandler.RequiresExternalSocketAddress) - { - Log(LogLevel.Debug, null, null, static (s, e) => "Getting external socket address."); - - (ip, port) = await GetExternalSocketAddressAsync(udpConnection, ssrc, buffer).ConfigureAwait(false); - if (ip is null) - { - Log(LogLevel.Error, null, null, static (s, e) => "Failed to get the external socket address. Aborting the client."); + Log(LogLevel.Debug, null, null, static (s, e) => "Getting external socket address."); - Abort(); - return; - } + (externalIp, externalPort) = await GetExternalSocketAddressAsync(udpConnection, ssrc, buffer).ConfigureAwait(false); + if (externalIp is null) + { + Log(LogLevel.Error, null, null, static (s, e) => "Failed to get the external socket address. Aborting the client."); - Log(LogLevel.Debug, (Ip: ip, Port: port), null, static (s, e) => $"External socket address: {s.Ip}:{s.Port}."); + Abort(); + return; } + + Log(LogLevel.Debug, (Ip: externalIp, Port: externalPort), null, static (s, e) => $"External socket address: {s.Ip}:{s.Port}."); } catch { @@ -372,7 +370,7 @@ private async ValueTask HandleJsonPayloadAsync(State state, ConnectionState conn Log(LogLevel.Debug, null, null, static (s, e) => "Selecting a protocol."); - VoicePayloadProperties protocolPayload = new(VoiceOpcode.SelectProtocol, new("udp", new(ip, port, encryptionName))); + VoicePayloadProperties protocolPayload = new(VoiceOpcode.SelectProtocol, new("udp", new(externalIp, externalPort, encryptionName))); await SendConnectionPayloadAsync(connectionState, protocolPayload.Serialize(Serialization.Default.VoicePayloadPropertiesProtocolProperties), _internalTextPayloadProperties).ConfigureAwait(false); await updateLatencyTask; @@ -579,127 +577,156 @@ private static (string Ip, ushort Port) GetSocketAddress(ReadOnlySpan data return (ip, port); } - private async void HandleDatagramReceive(ReadOnlyMemory datagram) + private void HandleDatagramReceive(ReadOnlySpan datagram) { - if (_udpState is not { Encryption: var encryption, DaveSession: var session }) + var datagramLength = datagram.Length; + + if (datagramLength < 12) + { + Log(LogLevel.Warning, datagramLength, null, static (s, e) => $"Received an RTP packet with an invalid length of {s} bytes."); return; + } - var handlers = _voiceReceive; - if (handlers.IsEmpty) + RtpPacket packet = new(datagram); + + if (datagramLength < packet.ExtendedHeaderLength) + { + Log(LogLevel.Warning, datagramLength, null, static (s, e) => $"Received an RTP packet with an invalid length of {s} bytes for the given header length."); return; + } try { - RtpPacketStorage packetStorage = new(datagram); + switch (packet.PayloadType) + { + case 0x78: + { + HandleVoicePacket(packet); + break; + } + } + } + catch (Exception ex) + { + Log(LogLevel.Error, null, ex, static (s, e) => $"An error occurred while handling a received RTP packet.{Environment.NewLine}{e}"); + } + } - var packet = packetStorage.Packet; + private void HandleVoicePacket(RtpPacket packet) + { + var voiceReceive = _voiceReceive; + if (voiceReceive.IsEmpty) + return; - var ssrc = packet.Ssrc; + if (_udpState is not { Encryption: var encryption, DaveSession: var session }) + return; - var result = _receiveHandler.HandlePacket(this, packet); - if (!result.Handle) - return; + if (!TryGetVoiceData(packet, encryption, session, out var buffer, out var bytesWritten)) + return; - if (session.GetDecryptor(ssrc) is not { } decryptor) - return; + VoiceReceiveEventArgs args = new(buffer.AsSpan(0, bytesWritten), + packet.Ssrc, + packet.Timestamp, + packet.SequenceNumber); - var framesMissed = result.FramesMissed; + HandleTask(InvokeEventAsync(voiceReceive, args, nameof(_voiceReceive))); - var sequenceNumber = packet.SequenceNumber; + ArrayPool.Shared.Return(buffer); - if (framesMissed is 0) - { - await InvokeEventForReceivedFrameAsync().ConfigureAwait(false); - return; - } + static async void HandleTask(ValueTask task) + { + await task.ConfigureAwait(false); + } + } + + private bool TryGetVoiceData(RtpPacket packet, IVoiceEncryption encryption, DaveSession session, [MaybeNullWhen(false)] out byte[] frame, out int frameLength) + { + var ssrc = packet.Ssrc; + if (session.GetDecryptor(ssrc) is not { } decryptor) + goto Fail; - var tasks = ArrayPool.Shared.Rent(framesMissed + 1); + var plaintextLength = packet.PayloadLength - encryption.Expansion; -#pragma warning disable CA2012 // Use ValueTasks correctly - for (ushort i = 0; i < framesMissed; i++) - tasks[i] = InvokeEventAsync(handlers, new(null, 0, 0, ssrc, null, (ushort)(sequenceNumber - framesMissed + i)), nameof(_voiceReceive)); + if (plaintextLength < 0) + { + Log(LogLevel.Warning, null, null, static (s, e) => "Failed to decrypt RTP packet because the payload is too small."); - tasks[framesMissed] = InvokeEventForReceivedFrameAsync(); -#pragma warning restore CA2012 // Use ValueTasks correctly + goto Fail; + } - await HandleTasksThatDoNotThrowAsync(tasks, framesMissed).ConfigureAwait(false); + var array = ArrayPool.Shared.Rent(plaintextLength); + var plaintext = array.AsSpan(0, plaintextLength); - ValueTask InvokeEventForReceivedFrameAsync() - { - var packet = packetStorage.Packet; + if (!encryption.TryDecrypt(packet, plaintext)) + { + Log(LogLevel.Warning, ssrc, null, static (s, e) => $"Failed to decrypt RTP packet. SSRC: {s}"); - var plaintextLength = packet.PayloadLength - encryption.Expansion; - var array = ArrayPool.Shared.Rent(plaintextLength); - var plaintext = array.AsSpan(0, plaintextLength); + goto FailWithArrayReturn; + } - if (!encryption.TryDecrypt(packet, plaintext)) - { - Log(LogLevel.Warning, ssrc, null, static (s, e) => $"Failed to decrypt RTP packet. SSRC: {s}"); + // Checked in HandleDatagramReceive that the datagram length is at least the extended header length - ArrayPool.Shared.Return(array); + int extensionLength = packet.Extension + ? 4 * BinaryPrimitives.ReadUInt16BigEndian(packet.Datagram[(packet.HeaderLength + 2)..]) + : 0; - return default; - } + int paddingLength = 0; - int extensionLength = packet.Extension - ? 4 * BinaryPrimitives.ReadUInt16BigEndian(packet.Datagram[(packet.HeaderLength + 2)..]) - : 0; + if (packet.Padding) + { + if (plaintextLength is 0) + { + Log(LogLevel.Warning, null, null, static (s, e) => "Failed to decrypt RTP packet because the payload is empty but the padding bit is set."); - int paddingLength = packet.Padding - ? plaintext[^1] - : 0; + goto FailWithArrayReturn; + } - var plaintextData = plaintext[extensionLength..^paddingLength]; + paddingLength = plaintext[^1]; + } - if (plaintextData.IsEmpty) - { - ArrayPool.Shared.Return(array); + if (extensionLength + paddingLength > plaintextLength) + { + Log(LogLevel.Warning, null, null, static (s, e) => "Failed to decrypt RTP packet because the header extension length and padding length combined are larger than the payload."); - return default; - } + goto FailWithArrayReturn; + } - int daveArrayLength = decryptor.GetMaxPlaintextByteSize(Dave.MediaType.Audio, plaintextData.Length); - byte[]? toReturn = null; - var daveArray = daveArrayLength > array.Length ? (toReturn = ArrayPool.Shared.Rent(daveArrayLength)) : array; - var result = decryptor.Decrypt(Dave.MediaType.Audio, ssrc, plaintextData, daveArray, out int bytesWritten); + var plaintextData = plaintext[extensionLength..^paddingLength]; - if (result is not Dave.DecryptorResultCode.Success) - { - ArrayPool.Shared.Return(array); + if (plaintextData.IsEmpty) + goto FailWithArrayReturn; - if (toReturn is not null) - ArrayPool.Shared.Return(toReturn); + int daveArrayLength = decryptor.GetMaxPlaintextByteSize(Dave.MediaType.Audio, plaintextData.Length); + byte[]? toReturn = null; + var daveArray = daveArrayLength > array.Length ? (toReturn = ArrayPool.Shared.Rent(daveArrayLength)) : array; + var result = decryptor.Decrypt(Dave.MediaType.Audio, ssrc, plaintextData, daveArray, out frameLength); - Log(LogLevel.Warning, (Result: result, Ssrc: ssrc), null, static (s, e) => $"Failed to decrypt DAVE frame with '{s.Result}'. SSRC: {s.Ssrc}"); + if (result is not Dave.DecryptorResultCode.Success) + { + ArrayPool.Shared.Return(array); - return default; - } + if (toReturn is not null) + ArrayPool.Shared.Return(toReturn); - if (toReturn is not null) - ArrayPool.Shared.Return(array); + Log(LogLevel.Warning, (Result: result, Ssrc: ssrc), null, static (s, e) => $"Failed to decrypt DAVE frame with '{s.Result}'. SSRC: {s.Ssrc}"); - return InvokeEventWithDisposalAsync(handlers, new VoiceReceiveEventArgs(daveArray, 0, bytesWritten, ssrc, packet.Timestamp, sequenceNumber), args => - { - ArrayPool.Shared.Return(args._buffer!); - }, nameof(_voiceReceive)); - } + goto Fail; } - catch (Exception ex) - { - Log(LogLevel.Error, null, ex, static (s, e) => - { - return $"An error occurred while handling a datagram.{Environment.NewLine}{e}"; - }); - } - } - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - private static async ValueTask HandleTasksThatDoNotThrowAsync(ValueTask[] tasks, ushort maxIndex) - { - for (ushort i = 0; i <= maxIndex; i++) - await tasks[i].ConfigureAwait(false); + if (toReturn is not null) + ArrayPool.Shared.Return(array); + + frame = daveArray; + + return true; + + FailWithArrayReturn: + ArrayPool.Shared.Return(array); - ArrayPool.Shared.Return(tasks); + Fail: + frame = null; + frameLength = 0; + return false; } public ValueTask EnterSpeakingStateAsync(SpeakingProperties speaking, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default) diff --git a/NetCord/Gateway/Voice/VoiceClientConfiguration.cs b/NetCord/Gateway/Voice/VoiceClientConfiguration.cs index 0aba338e..f730d75a 100644 --- a/NetCord/Gateway/Voice/VoiceClientConfiguration.cs +++ b/NetCord/Gateway/Voice/VoiceClientConfiguration.cs @@ -17,7 +17,6 @@ public class VoiceClientConfiguration : IWebSocketClientConfiguration public VoiceApiVersion? Version { get; init; } public IVoiceClientCacheProvider? CacheProvider { get; init; } public IVoiceEncryptionProvider? EncryptionProvider { get; init; } - public IVoiceReceiveHandler? ReceiveHandler { get; init; } public IVoiceLogger? Logger { get; init; } public TimeSpan? ExternalSocketAddressDiscoveryTimeout { get; init; } diff --git a/NetCord/Gateway/Voice/VoiceReceiveHandler.cs b/NetCord/Gateway/Voice/VoiceReceiveHandler.cs deleted file mode 100644 index e85ba82f..00000000 --- a/NetCord/Gateway/Voice/VoiceReceiveHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace NetCord.Gateway.Voice; - -public class VoiceReceiveHandler : IVoiceReceiveHandler -{ - private readonly Dictionary _lastSequenceNumbers = []; - - public bool RequiresExternalSocketAddress => true; - - public VoicePacketHandlingResult HandlePacket(VoiceClient client, RtpPacket packet) - { - ushort framesMissed; - bool handle; - - if (packet.PayloadType is not 0x78) - goto Fail; - - var lastSequenceNumbers = _lastSequenceNumbers; - var sequenceNumber = packet.SequenceNumber; - - if (lastSequenceNumbers.TryGetValue(packet.Ssrc, out var lastSequenceNumber)) - { - var sequenceNumberDiff = (short)(sequenceNumber - lastSequenceNumber); - if (sequenceNumberDiff <= 0) - goto Fail; - - framesMissed = (ushort)(sequenceNumberDiff - 1); - } - else - framesMissed = 0; - - handle = true; - lastSequenceNumbers[packet.Ssrc] = sequenceNumber; - - Ret: - return new(framesMissed, handle); - - Fail: - framesMissed = 0; - handle = false; - goto Ret; - } -} diff --git a/Tests/NetCord.Test/ApplicationCommands/VoiceCommands.cs b/Tests/NetCord.Test/ApplicationCommands/VoiceCommands.cs index 5fb05892..0cb1f052 100644 --- a/Tests/NetCord.Test/ApplicationCommands/VoiceCommands.cs +++ b/Tests/NetCord.Test/ApplicationCommands/VoiceCommands.cs @@ -46,7 +46,6 @@ private async Task JoinAsync(IVoiceGuildChannel? channel, VoiceEncr voiceClient = await client.JoinVoiceChannelAsync(guild.Id, channelId, new() { EncryptionProvider = encryptionProvider, - ReceiveHandler = new VoiceReceiveHandler(), Logger = new ConsoleLogger(LogLevel.Debug), //CacheProvider = ConcurrentVoiceClientCacheProvider.Empty, }); @@ -142,10 +141,7 @@ public async Task EchoAsync(IVoiceGuildChannel? channel = null, VoiceEncryption? voiceClient.VoiceReceive += args => { - if (args.Timestamp is { } timestamp) - voiceClient.SendVoice(args.SequenceNumber, timestamp, args.Frame); - else - Console.WriteLine($"Frame {args.SequenceNumber} got lost"); + voiceClient.SendVoice(args.SequenceNumber, args.Timestamp, args.Frame); return default; @@ -183,11 +179,25 @@ public async Task RecordAsync(IVoiceGuildChannel? channel = null, }); await RespondAsync(InteractionCallback.Message("Recording!")); - using OpusDecodeStream opusDecodeStream = new(ffmpeg.StandardInput.BaseStream, pcmFormat, voiceChannels); + using OpusDecoder decoder = new(voiceChannels); + + var frameSize = Opus.GetSamplesPerChannel(Opus.MaxFrameDuration); + var bufferSize = Opus.GetFrameBufferSize(frameSize, pcmFormat, voiceChannels); + var buffer = new byte[bufferSize]; + + Func, Span, int, bool, int> decode = pcmFormat is PcmFormat.Short ? decoder.Decode : decoder.DecodeFloat; voiceClient.VoiceReceive += args => { - opusDecodeStream.Write(args.Frame); + voiceClient.SendVoice(args.SequenceNumber, args.Timestamp, args.Frame); + + var samples = decode(args.Frame, + buffer.AsSpan(0, bufferSize), + frameSize, + false); + + ffmpeg.StandardInput.BaseStream.Write(buffer.AsSpan(0, Opus.GetFrameBufferSize(samples, pcmFormat, voiceChannels))); + return default; };