Browse Source

Pin audio buffers at stream creation

voice-allocs
RogueException 7 years ago
parent
commit
805fa00df0
8 changed files with 148 additions and 72 deletions
  1. +27
    -15
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  2. +2
    -5
      src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs
  3. +2
    -5
      src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs
  4. +12
    -20
      src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs
  5. +31
    -11
      src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
  6. +29
    -11
      src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs
  7. +22
    -2
      src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs
  8. +23
    -3
      src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs

+ 27
- 15
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -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;
}
}
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
public void Dispose() => Dispose(true);
}
}

+ 2
- 5
src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs View File

@@ -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;
}


+ 2
- 5
src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs View File

@@ -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;
}


+ 12
- 20
src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs View File

@@ -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;
}
}
}

+ 31
- 11
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs View File

@@ -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();
}
}
}

+ 29
- 11
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs View File

@@ -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();
}
}
}

+ 22
- 2
src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs View File

@@ -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);
}



+ 23
- 3
src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs View File

@@ -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);
}


Loading…
Cancel
Save