diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index c08bef3ae..0d9f82ab4 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; +using System.Runtime.InteropServices; namespace Discord.Audio { @@ -45,14 +46,17 @@ namespace Discord.Audio private string _url, _sessionId, _token; private ulong _userId; private uint _ssrc; + private byte[] _secretKey; + private GCHandle _secretKeyHandle; private bool _isSpeaking; + private bool _isDisposed; public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public int Latency { get; private set; } public int UdpLatency { get; private set; } public ulong ChannelId { get; internal set; } - internal byte[] SecretKey { get; private set; } + internal IntPtr SecretKeyPtr { get; private set; } private DiscordSocketClient Discord => Guild.Discord; public ConnectionState ConnectionState => _connection.State; @@ -93,6 +97,20 @@ namespace Discord.Audio UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); } + internal void Dispose(bool disposing) + { + if (disposing && !_isDisposed) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient.Dispose(); + if (_secretKeyHandle.IsAllocated) + _secretKeyHandle.Free(); + _isDisposed = true; + } + } + /// + public void Dispose() => Dispose(true); + internal async Task StartAsync(string url, ulong userId, string sessionId, string token) { _url = url; @@ -242,7 +260,12 @@ namespace Discord.Audio if (data.Mode != DiscordVoiceAPIClient.Mode) throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); - SecretKey = data.SecretKey; + _secretKey = data.SecretKey; + if (_secretKeyHandle != null) + _secretKeyHandle.Free(); + _secretKeyHandle = GCHandle.Alloc(data.SecretKey, GCHandleType.Pinned); + SecretKeyPtr = _secretKeyHandle.AddrOfPinnedObject(); + _isSpeaking = false; await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); _keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); @@ -386,7 +409,7 @@ namespace Discord.Audio await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - var now = Environment.TickCount; + int now = Environment.TickCount; //Did server respond to our last heartbeat? if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && @@ -427,7 +450,7 @@ namespace Discord.Audio await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - var now = Environment.TickCount; + int now = Environment.TickCount; try { @@ -474,16 +497,5 @@ namespace Discord.Audio return buffer; return new byte[OpusConverter.FrameBytes]; } - - internal void Dispose(bool disposing) - { - if (disposing) - { - StopAsync().GetAwaiter().GetResult(); - ApiClient.Dispose(); - } - } - /// - public void Dispose() => Dispose(true); } } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index 41c48e1ac..bd63ed989 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -20,12 +20,9 @@ namespace Discord.Audio CheckError(error); } - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset, bool decodeFEC) + public unsafe int DecodeFrame(byte* inPtr, int inputOffset, int inputCount, byte* outPtr, int outputOffset, bool decodeFEC) { - int result = 0; - fixed (byte* inPtr = input) - fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); + int result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); CheckError(result); return result * SampleBytes; } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index 1ff5a5d9a..fc0113be9 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -53,12 +53,9 @@ namespace Discord.Audio CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); } - public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output, int outputOffset) + public unsafe int EncodeFrame(byte* inPtr, int inputOffset, byte* outPtr, int outputOffset, int count) { - int result = 0; - fixed (byte* inPtr = input) - fixed (byte* outPtr = output) - result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, output.Length - outputOffset); + int result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, count); CheckError(result); return result; } diff --git a/src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs b/src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs index 4187c9f08..b8a77c681 100644 --- a/src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs +++ b/src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs @@ -6,31 +6,23 @@ namespace Discord.Audio public unsafe static class SecretBox { [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] - private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); + private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte* nonce, byte* secret); [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] - private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); + private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte* nonce, byte* secret); - public static int Encrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + public static int Encrypt(byte* inPtr, int inputOffset, int inputLength, byte* outPtr, int outputOffset, byte* nonce, byte* secret) { - fixed (byte* inPtr = input) - fixed (byte* outPtr = output) - { - int error = SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); - if (error != 0) - throw new Exception($"Sodium Error: {error}"); - return inputLength + 16; - } + int error = SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); + if (error != 0) + throw new Exception($"Sodium Error: {error}"); + return inputLength + 16; } - public static int Decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + public static int Decrypt(byte* inPtr, int inputOffset, int inputLength, byte* outPtr, int outputOffset, byte* nonce, byte* secret) { - fixed (byte* inPtr = input) - fixed (byte* outPtr = output) - { - int error = SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); - if (error != 0) - throw new Exception($"Sodium Error: {error}"); - return inputLength - 16; - } + int error = SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); + if (error != 0) + throw new Exception($"Sodium Error: {error}"); + return inputLength - 16; } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 58c4f4c70..55d923147 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -12,15 +13,31 @@ namespace Discord.Audio.Streams private readonly AudioStream _next; private readonly OpusDecoder _decoder; private readonly byte[] _buffer; + private readonly GCHandle _bufferHandle; + private readonly IntPtr _bufferPtr; + private bool _nextMissed; private bool _hasHeader; + private bool _isDisposed; public OpusDecodeStream(AudioStream next) { _next = next; _buffer = new byte[OpusConverter.FrameBytes]; + _bufferHandle = GCHandle.Alloc(_buffer, GCHandleType.Pinned); + _bufferPtr = _bufferHandle.AddrOfPinnedObject(); _decoder = new OpusDecoder(); } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing && !_isDisposed) + { + _decoder.Dispose(); + _bufferHandle.Free(); + _isDisposed = true; + } + } public override void WriteHeader(ushort seq, uint timestamp, bool missed) { @@ -39,17 +56,28 @@ namespace Discord.Audio.Streams if (!_nextMissed) { - count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); + unsafe + { + fixed (byte* inPtr = buffer) + count = _decoder.DecodeFrame(inPtr, offset, count, (byte*)_bufferPtr, 0, false); + } await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); } else if (count > 0) { - count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); + unsafe + { + fixed(byte* inPtr = buffer) + count = _decoder.DecodeFrame(inPtr, offset, count, (byte*)_bufferPtr, 0, true); + } await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); } else { - count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); + unsafe + { + count = _decoder.DecodeFrame(null, 0, 0, (byte*)_bufferPtr, 0, true); + } await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); } } @@ -62,13 +90,5 @@ namespace Discord.Audio.Streams { await _next.ClearAsync(cancelToken).ConfigureAwait(false); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - _decoder.Dispose(); - } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index f5883ad4b..4531d252c 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -12,26 +13,48 @@ namespace Discord.Audio.Streams private readonly AudioStream _next; private readonly OpusEncoder _encoder; private readonly byte[] _buffer; + private readonly GCHandle _bufferHandle; + private readonly IntPtr _bufferPtr; private int _partialFramePos; private ushort _seq; private uint _timestamp; - + private bool _isDisposed; + public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) { _next = next; _encoder = new OpusEncoder(bitrate, application, packetLoss); _buffer = new byte[OpusConverter.FrameBytes]; + _bufferHandle = GCHandle.Alloc(_buffer, GCHandleType.Pinned); + _bufferPtr = _bufferHandle.AddrOfPinnedObject(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing && !_isDisposed) + { + _encoder.Dispose(); + _bufferHandle.Free(); + _isDisposed = true; + } } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { //Assume threadsafe + int encFrameSize = 0; while (count > 0) { if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) { //We have enough data and no partial frames. Pass the buffer directly to the encoder - int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); + unsafe + { + fixed (byte* inPtr = buffer) + encFrameSize = _encoder.EncodeFrame(inPtr, offset, (byte*)_bufferPtr, 0, OpusConverter.FrameBytes); + } _next.WriteHeader(_seq, _timestamp, false); await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); @@ -45,7 +68,10 @@ namespace Discord.Audio.Streams //We have enough data to complete a previous partial frame. int partialSize = OpusConverter.FrameBytes - _partialFramePos; Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); - int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); + unsafe + { + encFrameSize = _encoder.EncodeFrame((byte*)_bufferPtr, 0, (byte*)_bufferPtr, 0, OpusConverter.FrameBytes); + } _next.WriteHeader(_seq, _timestamp, false); await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); @@ -86,13 +112,5 @@ namespace Discord.Audio.Streams { await _next.ClearAsync(cancelToken).ConfigureAwait(false); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - _encoder.Dispose(); - } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs index 9ed849a5e..669a96c7b 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,10 @@ namespace Discord.Audio.Streams private readonly AudioClient _client; private readonly AudioStream _next; private readonly byte[] _nonce; + private readonly GCHandle _nonceHandle; + private readonly IntPtr _noncePtr; + + private bool _isDisposed; public override bool CanRead => true; public override bool CanSeek => false; @@ -20,17 +25,32 @@ namespace Discord.Audio.Streams _next = next; _client = (AudioClient)client; _nonce = new byte[24]; + _nonceHandle = GCHandle.Alloc(_nonce, GCHandleType.Pinned); + _noncePtr = _nonceHandle.AddrOfPinnedObject(); + } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing && !_isDisposed) + { + _nonceHandle.Free(); + _isDisposed = true; + } } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); - if (_client.SecretKey == null) + if (_client.SecretKeyPtr == null) return; Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce - count = SecretBox.Decrypt(buffer, offset + 12, count - 12, buffer, offset + 12, _nonce, _client.SecretKey); + unsafe + { + fixed (byte* ptr = buffer) + count = SecretBox.Decrypt(ptr, offset + 12, count - 12, ptr, offset + 12, (byte*)_noncePtr, (byte*)_client.SecretKeyPtr); + } await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index bacc9be47..526406022 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -10,17 +11,32 @@ namespace Discord.Audio.Streams private readonly AudioClient _client; private readonly AudioStream _next; private readonly byte[] _nonce; + private readonly GCHandle _nonceHandle; + private readonly IntPtr _noncePtr; + private bool _hasHeader; private ushort _nextSeq; private uint _nextTimestamp; + private bool _isDisposed; public SodiumEncryptStream(AudioStream next, IAudioClient client) { _next = next; _client = (AudioClient)client; _nonce = new byte[24]; + _nonceHandle = GCHandle.Alloc(_nonce, GCHandleType.Pinned); + _noncePtr = _nonceHandle.AddrOfPinnedObject(); + } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing && !_isDisposed) + { + _nonceHandle.Free(); + _isDisposed = true; + } } - + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) @@ -37,11 +53,15 @@ namespace Discord.Audio.Streams throw new InvalidOperationException("Received payload without an RTP header"); _hasHeader = false; - if (_client.SecretKey == null) + if (_client.SecretKeyPtr == null) return; Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header - count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); + unsafe + { + fixed (byte* ptr = buffer) + count = SecretBox.Encrypt(ptr, offset + 12, count - 12, ptr, 12, (byte*)_noncePtr, (byte*)_client.SecretKeyPtr); + } _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); }