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;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;


namespace Discord.Audio namespace Discord.Audio
{ {
@@ -45,14 +46,17 @@ namespace Discord.Audio
private string _url, _sessionId, _token; private string _url, _sessionId, _token;
private ulong _userId; private ulong _userId;
private uint _ssrc; private uint _ssrc;
private byte[] _secretKey;
private GCHandle _secretKeyHandle;
private bool _isSpeaking; private bool _isSpeaking;
private bool _isDisposed;


public SocketGuild Guild { get; } public SocketGuild Guild { get; }
public DiscordVoiceAPIClient ApiClient { get; private set; } public DiscordVoiceAPIClient ApiClient { get; private set; }
public int Latency { get; private set; } public int Latency { get; private set; }
public int UdpLatency { get; private set; } public int UdpLatency { get; private set; }
public ulong ChannelId { get; internal set; } public ulong ChannelId { get; internal set; }
internal byte[] SecretKey { get; private set; }
internal IntPtr SecretKeyPtr { get; private set; }


private DiscordSocketClient Discord => Guild.Discord; private DiscordSocketClient Discord => Guild.Discord;
public ConnectionState ConnectionState => _connection.State; 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); 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) internal async Task StartAsync(string url, ulong userId, string sessionId, string token)
{ {
_url = url; _url = url;
@@ -242,7 +260,12 @@ namespace Discord.Audio
if (data.Mode != DiscordVoiceAPIClient.Mode) if (data.Mode != DiscordVoiceAPIClient.Mode)
throw new InvalidOperationException($"Discord selected an unexpected mode: {data.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; _isSpeaking = false;
await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); await ApiClient.SendSetSpeaking(false).ConfigureAwait(false);
_keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); _keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken);
@@ -386,7 +409,7 @@ namespace Discord.Audio
await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false);
while (!cancelToken.IsCancellationRequested) while (!cancelToken.IsCancellationRequested)
{ {
var now = Environment.TickCount;
int now = Environment.TickCount;


//Did server respond to our last heartbeat? //Did server respond to our last heartbeat?
if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis &&
@@ -427,7 +450,7 @@ namespace Discord.Audio
await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false);
while (!cancelToken.IsCancellationRequested) while (!cancelToken.IsCancellationRequested)
{ {
var now = Environment.TickCount;
int now = Environment.TickCount;


try try
{ {
@@ -474,16 +497,5 @@ namespace Discord.Audio
return buffer; return buffer;
return new byte[OpusConverter.FrameBytes]; 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); 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); CheckError(result);
return result * SampleBytes; 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)); 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); CheckError(result);
return 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 public unsafe static class SecretBox
{ {
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] [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)] [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;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -12,15 +13,31 @@ namespace Discord.Audio.Streams
private readonly AudioStream _next; private readonly AudioStream _next;
private readonly OpusDecoder _decoder; private readonly OpusDecoder _decoder;
private readonly byte[] _buffer; private readonly byte[] _buffer;
private readonly GCHandle _bufferHandle;
private readonly IntPtr _bufferPtr;

private bool _nextMissed; private bool _nextMissed;
private bool _hasHeader; private bool _hasHeader;
private bool _isDisposed;


public OpusDecodeStream(AudioStream next) public OpusDecodeStream(AudioStream next)
{ {
_next = next; _next = next;
_buffer = new byte[OpusConverter.FrameBytes]; _buffer = new byte[OpusConverter.FrameBytes];
_bufferHandle = GCHandle.Alloc(_buffer, GCHandleType.Pinned);
_bufferPtr = _bufferHandle.AddrOfPinnedObject();
_decoder = new OpusDecoder(); _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) public override void WriteHeader(ushort seq, uint timestamp, bool missed)
{ {
@@ -39,17 +56,28 @@ namespace Discord.Audio.Streams


if (!_nextMissed) 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); await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false);
} }
else if (count > 0) 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); await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false);
} }
else 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); await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false);
} }
} }
@@ -62,13 +90,5 @@ namespace Discord.Audio.Streams
{ {
await _next.ClearAsync(cancelToken).ConfigureAwait(false); 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;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -12,26 +13,48 @@ namespace Discord.Audio.Streams
private readonly AudioStream _next; private readonly AudioStream _next;
private readonly OpusEncoder _encoder; private readonly OpusEncoder _encoder;
private readonly byte[] _buffer; private readonly byte[] _buffer;
private readonly GCHandle _bufferHandle;
private readonly IntPtr _bufferPtr;
private int _partialFramePos; private int _partialFramePos;
private ushort _seq; private ushort _seq;
private uint _timestamp; private uint _timestamp;
private bool _isDisposed;

public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss)
{ {
_next = next; _next = next;
_encoder = new OpusEncoder(bitrate, application, packetLoss); _encoder = new OpusEncoder(bitrate, application, packetLoss);
_buffer = new byte[OpusConverter.FrameBytes]; _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) public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{ {
//Assume threadsafe //Assume threadsafe
int encFrameSize = 0;
while (count > 0) while (count > 0)
{ {
if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes)
{ {
//We have enough data and no partial frames. Pass the buffer directly to the encoder //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); _next.WriteHeader(_seq, _timestamp, false);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(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. //We have enough data to complete a previous partial frame.
int partialSize = OpusConverter.FrameBytes - _partialFramePos; int partialSize = OpusConverter.FrameBytes - _partialFramePos;
Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); 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); _next.WriteHeader(_seq, _timestamp, false);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false);


@@ -86,13 +112,5 @@ namespace Discord.Audio.Streams
{ {
await _next.ClearAsync(cancelToken).ConfigureAwait(false); 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;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -10,6 +11,10 @@ namespace Discord.Audio.Streams
private readonly AudioClient _client; private readonly AudioClient _client;
private readonly AudioStream _next; private readonly AudioStream _next;
private readonly byte[] _nonce; private readonly byte[] _nonce;
private readonly GCHandle _nonceHandle;
private readonly IntPtr _noncePtr;

private bool _isDisposed;


public override bool CanRead => true; public override bool CanRead => true;
public override bool CanSeek => false; public override bool CanSeek => false;
@@ -20,17 +25,32 @@ namespace Discord.Audio.Streams
_next = next; _next = next;
_client = (AudioClient)client; _client = (AudioClient)client;
_nonce = new byte[24]; _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) public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{ {
cancelToken.ThrowIfCancellationRequested(); cancelToken.ThrowIfCancellationRequested();


if (_client.SecretKey == null)
if (_client.SecretKeyPtr == null)
return; return;


Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce 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); 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;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -10,17 +11,32 @@ namespace Discord.Audio.Streams
private readonly AudioClient _client; private readonly AudioClient _client;
private readonly AudioStream _next; private readonly AudioStream _next;
private readonly byte[] _nonce; private readonly byte[] _nonce;
private readonly GCHandle _nonceHandle;
private readonly IntPtr _noncePtr;

private bool _hasHeader; private bool _hasHeader;
private ushort _nextSeq; private ushort _nextSeq;
private uint _nextTimestamp; private uint _nextTimestamp;
private bool _isDisposed;


public SodiumEncryptStream(AudioStream next, IAudioClient client) public SodiumEncryptStream(AudioStream next, IAudioClient client)
{ {
_next = next; _next = next;
_client = (AudioClient)client; _client = (AudioClient)client;
_nonce = new byte[24]; _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) public override void WriteHeader(ushort seq, uint timestamp, bool missed)
{ {
if (_hasHeader) if (_hasHeader)
@@ -37,11 +53,15 @@ namespace Discord.Audio.Streams
throw new InvalidOperationException("Received payload without an RTP header"); throw new InvalidOperationException("Received payload without an RTP header");
_hasHeader = false; _hasHeader = false;


if (_client.SecretKey == null)
if (_client.SecretKeyPtr == null)
return; return;
Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header 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); _next.WriteHeader(_nextSeq, _nextTimestamp, false);
await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false);
} }


Loading…
Cancel
Save