Browse Source

Cleaned up audio code

tags/1.0-rc
RogueException 8 years ago
parent
commit
8e0c65498b
21 changed files with 495 additions and 276 deletions
  1. +9
    -0
      src/Discord.Net.Core/Audio/AudioApplication.cs
  2. +23
    -1
      src/Discord.Net.Core/Audio/AudioInStream.cs
  3. +28
    -3
      src/Discord.Net.Core/Audio/AudioOutStream.cs
  4. +2
    -2
      src/Discord.Net.Core/Audio/IAudioClient.cs
  5. +20
    -11
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  6. +1
    -1
      src/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs
  7. +6
    -4
      src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs
  8. +48
    -22
      src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs
  9. +9
    -0
      src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs
  10. +156
    -0
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  11. +18
    -6
      src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
  12. +23
    -16
      src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs
  13. +23
    -0
      src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs
  14. +11
    -5
      src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs
  15. +27
    -48
      src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs
  16. +42
    -0
      src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs
  17. +45
    -0
      src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs
  18. +0
    -119
      src/Discord.Net.WebSocket/Audio/Targets/BufferedAudioTarget.cs
  19. +0
    -22
      src/Discord.Net.WebSocket/Audio/Targets/DirectAudioTarget.cs
  20. +0
    -12
      src/Discord.Net.WebSocket/Audio/Targets/IAudioTarget.cs
  21. +4
    -4
      src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs

+ 9
- 0
src/Discord.Net.Core/Audio/AudioApplication.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Audio
{
public enum AudioApplication : int
{
Voice,
Music,
Mixed
}
}

+ 23
- 1
src/Discord.Net.Core/Audio/AudioInStream.cs View File

@@ -1,8 +1,30 @@
using System.IO;
using System;
using System.IO;
using System.Threading;


namespace Discord.Audio namespace Discord.Audio
{ {
public abstract class AudioInStream : Stream public abstract class AudioInStream : Stream
{ {
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;

public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}

public override void Flush() { throw new NotSupportedException(); }

public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
} }
} }

+ 28
- 3
src/Discord.Net.Core/Audio/AudioOutStream.cs View File

@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -10,7 +11,31 @@ namespace Discord.Audio
public override bool CanSeek => false; public override bool CanSeek => false;
public override bool CanWrite => true; public override bool CanWrite => true;


public virtual void Clear() { }
public virtual Task ClearAsync(CancellationToken cancelToken) { return Task.Delay(0); }
public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}
public override void Flush()
{
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public void Clear()
{
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
}

public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); }
//public virtual Task WriteSilenceAsync(CancellationToken cancellationToken) { return Task.Delay(0); }

public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
} }
} }

+ 2
- 2
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -34,13 +34,13 @@ namespace Discord.Audio
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> /// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
/// <param name="bitrate"></param> /// <param name="bitrate"></param>
/// <returns></returns> /// <returns></returns>
AudioOutStream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000);
AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000);
/// <summary> /// <summary>
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.
/// </summary> /// </summary>
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> /// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
/// <param name="bitrate"></param> /// <param name="bitrate"></param>
/// <returns></returns> /// <returns></returns>
AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null);
AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null);
} }
} }

+ 20
- 11
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Voice; using Discord.API.Voice;
using Discord.Audio.Streams;
using Discord.Logging; using Discord.Logging;
using Discord.Net.Converters; using Discord.Net.Converters;
using Discord.WebSocket; using Discord.WebSocket;
@@ -80,7 +81,7 @@ namespace Discord.Audio
{ {
_audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult();
e.ErrorContext.Handled = true; e.ErrorContext.Handled = true;
};
};


LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false);
} }
@@ -129,26 +130,34 @@ namespace Discord.Audio
public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis) public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis)
{ {
CheckSamplesPerFrame(samplesPerFrame); CheckSamplesPerFrame(samplesPerFrame);
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _connection.CancelToken);
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
return new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger);
} }
public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) public AudioOutStream CreateDirectOpusStream(int samplesPerFrame)
{ {
CheckSamplesPerFrame(samplesPerFrame); CheckSamplesPerFrame(samplesPerFrame);
var target = new DirectAudioTarget(ApiClient);
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
} }
public AudioOutStream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
{ {
CheckSamplesPerFrame(samplesPerFrame); CheckSamplesPerFrame(samplesPerFrame);
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _connection.CancelToken);
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
var bufferedStream = new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger);
return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application);
} }
public AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate)
public AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate)
{ {
CheckSamplesPerFrame(samplesPerFrame); CheckSamplesPerFrame(samplesPerFrame);
var target = new DirectAudioTarget(ApiClient);
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
return new OpusEncodeStream(rtpWriter, channels, samplesPerFrame, bitrate ?? (96 * 1024), application);
} }
private void CheckSamplesPerFrame(int samplesPerFrame) private void CheckSamplesPerFrame(int samplesPerFrame)
{ {


+ 1
- 1
src/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs View File

@@ -1,6 +1,6 @@
namespace Discord.Audio namespace Discord.Audio
{ {
public enum OpusApplication : int
internal enum OpusApplication : int
{ {
Voice = 2048, Voice = 2048,
MusicOrMixed = 2049, MusicOrMixed = 2049,


+ 6
- 4
src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs View File

@@ -1,10 +1,12 @@
namespace Discord.Audio namespace Discord.Audio
{ {
//https://github.com/gcp/opus/blob/master/include/opus_defines.h
internal enum OpusCtl : int internal enum OpusCtl : int
{ {
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
SetBitrate = 4002,
SetBandwidth = 4008,
SetInbandFEC = 4012,
SetPacketLossPercent = 4014,
SetSignal = 4024
} }
} }

+ 48
- 22
src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs View File

@@ -15,17 +15,62 @@ namespace Discord.Audio
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value);
/// <summary> Gets the coding mode of the encoder. </summary> /// <summary> Gets the coding mode of the encoder. </summary>
public OpusApplication Application { get; }
public AudioApplication Application { get; }
public int BitRate { get;}


public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed)
public OpusEncoder(int samplingRate, int channels, int bitrate, AudioApplication application)
: base(samplingRate, channels) : base(samplingRate, channels)
{ {
if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)
throw new ArgumentOutOfRangeException(nameof(bitrate));

Application = application; Application = application;
BitRate = bitrate;

OpusApplication opusApplication;
OpusSignal opusSignal;
switch (application)
{
case AudioApplication.Mixed:
opusApplication = OpusApplication.MusicOrMixed;
opusSignal = OpusSignal.Auto;
break;
case AudioApplication.Music:
opusApplication = OpusApplication.MusicOrMixed;
opusSignal = OpusSignal.Music;
break;
case AudioApplication.Voice:
opusApplication = OpusApplication.Voice;
opusSignal = OpusSignal.Voice;
break;
default:
throw new ArgumentOutOfRangeException(nameof(application));
}


OpusError error; OpusError error;
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
_ptr = CreateEncoder(samplingRate, channels, (int)opusApplication, out error);
if (error != OpusError.OK) if (error != OpusError.OK)
throw new Exception($"Opus Error: {error}"); throw new Exception($"Opus Error: {error}");

var result = EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");

result = EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 5); //%%
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");

result = EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1); //True
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");

result = EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");

/*result = EncoderCtl(_ptr, OpusCtl.SetBandwidth, 1105);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");*/
} }


/// <summary> Produces Opus encoded audio from PCM samples. </summary> /// <summary> Produces Opus encoded audio from PCM samples. </summary>
@@ -44,25 +89,6 @@ namespace Discord.Audio
return result; return result;
} }


/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetForwardErrorCorrection(bool value)
{
var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
}

/// <summary> Gets or sets the encoder's bitrate. </summary>
public void SetBitrate(int value)
{
if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate)
throw new ArgumentOutOfRangeException(nameof(value));

var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
}

protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (_ptr != IntPtr.Zero) if (_ptr != IntPtr.Zero)


+ 9
- 0
src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Audio
{
internal enum OpusSignal : int
{
Auto = -1000,
Voice = 3001,
Music = 3002,
}
}

+ 156
- 0
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs View File

@@ -0,0 +1,156 @@
using Discord.Logging;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio.Streams
{
///<summary> Wraps another stream with a timed buffer. </summary>
public class BufferedWriteStream : AudioOutStream
{
private struct Frame
{
public Frame(byte[] buffer, int bytes)
{
Buffer = buffer;
Bytes = bytes;
}

public readonly byte[] Buffer;
public readonly int Bytes;
}

private static readonly byte[] _silenceFrame = new byte[0];

private readonly AudioOutStream _next;
private readonly CancellationTokenSource _cancelTokenSource;
private readonly CancellationToken _cancelToken;
private readonly Task _task;
private readonly ConcurrentQueue<Frame> _queuedFrames;
private readonly ConcurrentQueue<byte[]> _bufferPool;
private readonly SemaphoreSlim _queueLock;
private readonly Logger _logger;
private readonly int _ticksPerFrame, _queueLength;
private bool _isPreloaded;

internal BufferedWriteStream(AudioOutStream next, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500)
{
//maxFrameSize = 1275 was too limiting at 128*1024
_next = next;
_ticksPerFrame = samplesPerFrame / 48;
_logger = logger;
_queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up

_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token;
_queuedFrames = new ConcurrentQueue<Frame>();
_bufferPool = new ConcurrentQueue<byte[]>();
for (int i = 0; i < _queueLength; i++)
_bufferPool.Enqueue(new byte[maxFrameSize]);
_queueLock = new SemaphoreSlim(_queueLength, _queueLength);

_task = Run();
}

private Task Run()
{
uint num = 0;
return Task.Run(async () =>
{
try
{
while (!_isPreloaded && !_cancelToken.IsCancellationRequested)
await Task.Delay(1).ConfigureAwait(false);

long nextTick = Environment.TickCount;
while (!_cancelToken.IsCancellationRequested)
{
const int limit = 1;
long tick = Environment.TickCount;
long dist = nextTick - tick;
if (dist <= limit)
{
Frame frame;
if (_queuedFrames.TryDequeue(out frame))
{
await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false);
_bufferPool.Enqueue(frame.Buffer);
_queueLock.Release();
nextTick += _ticksPerFrame;
#if DEBUG
var _ = _logger.DebugAsync($"{num++}: Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)");
#endif
}
else if (dist == 0)
{
await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false);
nextTick += _ticksPerFrame;
#if DEBUG
var _ = _logger.DebugAsync($"{num++}: Buffer underrun");
#endif
}
}
else
await Task.Delay((int)(dist - (limit - 1))).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
});
}

public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken)
{
if (cancelToken.CanBeCanceled)
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token;
else
cancelToken = _cancelToken;

await _queueLock.WaitAsync(-1, cancelToken).ConfigureAwait(false);
byte[] buffer;
if (!_bufferPool.TryDequeue(out buffer))
{
#if DEBUG
var _ = _logger.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock
#endif
return;
}
Buffer.BlockCopy(data, offset, buffer, 0, count);
_queuedFrames.Enqueue(new Frame(buffer, count));
#if DEBUG
//var _ await _logger.DebugAsync($"Queued {count} bytes ({_queuedFrames.Count} frames buffered)");
#endif
if (!_isPreloaded && _queuedFrames.Count == _queueLength)
{
#if DEBUG
var _ = _logger.DebugAsync($"Preloaded");
#endif
_isPreloaded = true;
}
}

public override async Task FlushAsync(CancellationToken cancelToken)
{
while (true)
{
cancelToken.ThrowIfCancellationRequested();
if (_queuedFrames.Count == 0)
return;
await Task.Delay(250, cancelToken).ConfigureAwait(false);
}
}
public override Task ClearAsync(CancellationToken cancelToken)
{
Frame ignored;
do
cancelToken.ThrowIfCancellationRequested();
while (_queuedFrames.TryDequeue(out ignored));
return Task.Delay(0);
}
protected override void Dispose(bool disposing)
{
if (disposing)
_cancelTokenSource.Cancel();
}
}
}

+ 18
- 6
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs View File

@@ -1,22 +1,34 @@
namespace Discord.Audio
using System;
using System.Collections.Concurrent;

namespace Discord.Audio.Streams
{ {
internal class OpusDecodeStream : RTPReadStream
///<summary> Converts Opus to PCM </summary>
public class OpusDecodeStream : AudioInStream
{ {
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
private readonly byte[] _buffer; private readonly byte[] _buffer;
private readonly OpusDecoder _decoder; private readonly OpusDecoder _decoder;


internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate,
int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
: base(audioClient, secretKey)
internal OpusDecodeStream(AudioClient audioClient, int samplingRate, int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
{ {
_buffer = new byte[bufferSize]; _buffer = new byte[bufferSize];
_decoder = new OpusDecoder(samplingRate, channels); _decoder = new OpusDecoder(samplingRate, channels);
_queuedData = new BlockingCollection<byte[]>(100);
} }


public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{
var queuedData = _queuedData.Take();
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
return queuedData.Length;
}
public override void Write(byte[] buffer, int offset, int count)
{ {
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0); count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0);
return base.Read(_buffer, 0, count);
var newBuffer = new byte[count];
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
_queuedData.Add(newBuffer);
} }


protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)


+ 23
- 16
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs View File

@@ -2,27 +2,28 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.Audio
namespace Discord.Audio.Streams
{ {
internal class OpusEncodeStream : RTPWriteStream
///<summary> Converts PCM to Opus </summary>
public class OpusEncodeStream : AudioOutStream
{ {
public const int SampleRate = 48000; public const int SampleRate = 48000;
private readonly AudioOutStream _next;
private readonly OpusEncoder _encoder;
private readonly byte[] _buffer;

private int _frameSize; private int _frameSize;
private byte[] _partialFrameBuffer; private byte[] _partialFrameBuffer;
private int _partialFramePos; private int _partialFramePos;


private readonly OpusEncoder _encoder;

internal OpusEncodeStream(IAudioTarget target, byte[] secretKey, int channels, int samplesPerFrame, uint ssrc, int? bitrate = null)
: base(target, secretKey, samplesPerFrame, ssrc)
internal OpusEncodeStream(AudioOutStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000)
{ {
_encoder = new OpusEncoder(SampleRate, channels);
_next = next;
_encoder = new OpusEncoder(SampleRate, channels, bitrate, application);
_frameSize = samplesPerFrame * channels * 2; _frameSize = samplesPerFrame * channels * 2;
_buffer = new byte[bufferSize];
_partialFrameBuffer = new byte[_frameSize]; _partialFrameBuffer = new byte[_frameSize];

_encoder.SetForwardErrorCorrection(true);
if (bitrate != null)
_encoder.SetBitrate(bitrate.Value);
} }


public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
@@ -43,7 +44,7 @@ namespace Discord.Audio
_partialFramePos = 0; _partialFramePos = 0;


int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _frameSize, _buffer, 0); int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _frameSize, _buffer, 0);
await base.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
@@ -54,10 +55,7 @@ namespace Discord.Audio
} }
} }


/*public override void Flush()
{
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
}
/*
public override async Task FlushAsync(CancellationToken cancellationToken) public override async Task FlushAsync(CancellationToken cancellationToken)
{ {
try try
@@ -70,6 +68,15 @@ namespace Discord.Audio
await base.FlushAsync(cancellationToken).ConfigureAwait(false); await base.FlushAsync(cancellationToken).ConfigureAwait(false);
}*/ }*/


public override async Task FlushAsync(CancellationToken cancelToken)
{
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
}
public override async Task ClearAsync(CancellationToken cancelToken)
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);


+ 23
- 0
src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs View File

@@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio.Streams
{
///<summary> Wraps an IAudioClient, sending voice data on write. </summary>
public class OutputStream : AudioOutStream
{
private readonly DiscordVoiceAPIClient _client;
public OutputStream(IAudioClient client)
: this((client as AudioClient).ApiClient) { }
internal OutputStream(DiscordVoiceAPIClient client)
{
_client = client;
}

public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{
cancelToken.ThrowIfCancellationRequested();
await _client.SendAsync(buffer, offset, count).ConfigureAwait(false);
}
}
}

+ 11
- 5
src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs View File

@@ -2,11 +2,13 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO; using System.IO;


namespace Discord.Audio
namespace Discord.Audio.Streams
{ {
internal class RTPReadStream : Stream
///<summary> Reads the payload from an RTP frame </summary>
public class RTPReadStream : AudioInStream
{ {
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
//private readonly BlockingCollection<RTPFrame> _queuedData; //TODO: Replace with max-length ring buffer
private readonly AudioClient _audioClient; private readonly AudioClient _audioClient;
private readonly byte[] _buffer, _nonce, _secretKey; private readonly byte[] _buffer, _nonce, _secretKey;


@@ -23,6 +25,12 @@ namespace Discord.Audio
_nonce = new byte[24]; _nonce = new byte[24];
} }


/*public RTPFrame ReadFrame()
{
var queuedData = _queuedData.Take();
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
return queuedData.Length;
}*/
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
var queuedData = _queuedData.Take(); var queuedData = _queuedData.Take();
@@ -31,10 +39,8 @@ namespace Discord.Audio
} }
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
{ {
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12);
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey);
var newBuffer = new byte[count]; var newBuffer = new byte[count];
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
Buffer.BlockCopy(buffer, 0, newBuffer, 0, count);
_queuedData.Add(newBuffer); _queuedData.Add(newBuffer);
} }




+ 27
- 48
src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs View File

@@ -1,33 +1,32 @@
using System; using System;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.Audio
namespace Discord.Audio.Streams
{ {
internal class RTPWriteStream : AudioOutStream
///<summary> Wraps data in an RTP frame </summary>
public class RTPWriteStream : AudioOutStream
{ {
private readonly IAudioTarget _target;
private readonly byte[] _nonce, _secretKey;
private readonly AudioOutStream _next;
private readonly byte[] _header;
private int _samplesPerFrame; private int _samplesPerFrame;
private uint _ssrc, _timestamp = 0; private uint _ssrc, _timestamp = 0;


protected readonly byte[] _buffer; protected readonly byte[] _buffer;


internal RTPWriteStream(IAudioTarget target, byte[] secretKey, int samplesPerFrame, uint ssrc)
internal RTPWriteStream(AudioOutStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000)
{ {
_target = target;
_secretKey = secretKey;
_next = next;
_samplesPerFrame = samplesPerFrame; _samplesPerFrame = samplesPerFrame;
_ssrc = ssrc; _ssrc = ssrc;
_buffer = new byte[4000];
_nonce = new byte[24];
_nonce[0] = 0x80;
_nonce[1] = 0x78;
_nonce[8] = (byte)(_ssrc >> 24);
_nonce[9] = (byte)(_ssrc >> 16);
_nonce[10] = (byte)(_ssrc >> 8);
_nonce[11] = (byte)(_ssrc >> 0);
_buffer = new byte[bufferSize];
_header = new byte[24];
_header[0] = 0x80;
_header[1] = 0x78;
_header[8] = (byte)(_ssrc >> 24);
_header[9] = (byte)(_ssrc >> 16);
_header[10] = (byte)(_ssrc >> 8);
_header[11] = (byte)(_ssrc >> 0);
} }


public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
@@ -39,48 +38,28 @@ namespace Discord.Audio
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
unchecked unchecked
{ {
if (_nonce[3]++ == byte.MaxValue)
_nonce[2]++;
if (_header[3]++ == byte.MaxValue)
_header[2]++;


_timestamp += (uint)_samplesPerFrame; _timestamp += (uint)_samplesPerFrame;
_nonce[4] = (byte)(_timestamp >> 24);
_nonce[5] = (byte)(_timestamp >> 16);
_nonce[6] = (byte)(_timestamp >> 8);
_nonce[7] = (byte)(_timestamp >> 0);
_header[4] = (byte)(_timestamp >> 24);
_header[5] = (byte)(_timestamp >> 16);
_header[6] = (byte)(_timestamp >> 8);
_header[7] = (byte)(_timestamp >> 0);
} }
Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer
Buffer.BlockCopy(buffer, offset, _buffer, 12, count);


count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey);
Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer
await _target.SendAsync(_buffer, count + 12).ConfigureAwait(false);
await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false);
} }


public override void Flush()
public override async Task FlushAsync(CancellationToken cancelToken)
{ {
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
await _target.FlushAsync(cancellationToken).ConfigureAwait(false);
}

public override void Clear()
{
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
} }
public override async Task ClearAsync(CancellationToken cancelToken) public override async Task ClearAsync(CancellationToken cancelToken)
{ {
await _target.ClearAsync(cancelToken).ConfigureAwait(false);
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
} }

public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
} }
} }

+ 42
- 0
src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Concurrent;

namespace Discord.Audio.Streams
{
///<summary> Decrypts an RTP frame using libsodium </summary>
public class SodiumDecryptStream : AudioInStream
{
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
private readonly AudioClient _audioClient;
private readonly byte[] _buffer, _nonce, _secretKey;

public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;

internal SodiumDecryptStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000)
{
_audioClient = audioClient;
_secretKey = secretKey;
_buffer = new byte[bufferSize];
_queuedData = new BlockingCollection<byte[]>(100);
_nonce = new byte[24];
}

public override int Read(byte[] buffer, int offset, int count)
{
var queuedData = _queuedData.Take();
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
return queuedData.Length;
}
public override void Write(byte[] buffer, int offset, int count)
{
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey);

var newBuffer = new byte[count];
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
_queuedData.Add(newBuffer);
}
}
}

+ 45
- 0
src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs View File

@@ -0,0 +1,45 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio.Streams
{
///<summary> Encrypts an RTP frame using libsodium </summary>
public class SodiumEncryptStream : AudioOutStream
{
private readonly AudioOutStream _next;
private readonly byte[] _nonce, _secretKey;

//protected readonly byte[] _buffer;

internal SodiumEncryptStream(AudioOutStream next, byte[] secretKey/*, int bufferSize = 4000*/)
{
_next = next;
_secretKey = secretKey;
//_buffer = new byte[bufferSize]; //TODO: Can Sodium do an in-place encrypt?
_nonce = new byte[24];
}

public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header
count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _secretKey);
await _next.WriteAsync(buffer, 0, count + 12, cancellationToken).ConfigureAwait(false);
}

public override async Task FlushAsync(CancellationToken cancelToken)
{
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
}
public override async Task ClearAsync(CancellationToken cancelToken)
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}
}
}

+ 0
- 119
src/Discord.Net.WebSocket/Audio/Targets/BufferedAudioTarget.cs View File

@@ -1,119 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio
{
internal class BufferedAudioTarget : IAudioTarget, IDisposable
{
private struct Frame
{
public Frame(byte[] buffer, int bytes)
{
Buffer = buffer;
Bytes = bytes;
}

public readonly byte[] Buffer;
public readonly int Bytes;
}

private static readonly byte[] _silenceFrame = new byte[] { 0xF8, 0xFF, 0xFE };
private Task _task;
private DiscordVoiceAPIClient _client;
private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken;
private ConcurrentQueue<Frame> _queuedFrames;
private ConcurrentQueue<byte[]> _bufferPool;
private SemaphoreSlim _queueLock;
private int _ticksPerFrame;

internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken)
{
_client = client;
_ticksPerFrame = samplesPerFrame / 48;
int queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up

_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token;
_queuedFrames = new ConcurrentQueue<Frame>();
_bufferPool = new ConcurrentQueue<byte[]>();
for (int i = 0; i < queueLength; i++)
_bufferPool.Enqueue(new byte[1275]);
_queueLock = new SemaphoreSlim(queueLength, queueLength);

_task = Run();
}

private Task Run()
{
return Task.Run(async () =>
{
try
{
long nextTick = Environment.TickCount;
while (!_cancelToken.IsCancellationRequested)
{
long tick = Environment.TickCount;
long dist = nextTick - tick;
if (dist <= 0)
{
Frame frame;
if (_queuedFrames.TryDequeue(out frame))
{
await _client.SendAsync(frame.Buffer, frame.Bytes).ConfigureAwait(false);
_bufferPool.Enqueue(frame.Buffer);
_queueLock.Release();
}
else
await _client.SendAsync(_silenceFrame, _silenceFrame.Length).ConfigureAwait(false);
nextTick += _ticksPerFrame;
}
else if (dist > 1)
await Task.Delay((int)dist).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
});
}

public async Task SendAsync(byte[] data, int count)
{
await _queueLock.WaitAsync(-1, _cancelToken).ConfigureAwait(false);
byte[] buffer;
_bufferPool.TryDequeue(out buffer);
Buffer.BlockCopy(data, 0, buffer, 0, count);
_queuedFrames.Enqueue(new Frame(buffer, count));
}
public async Task FlushAsync(CancellationToken cancelToken)
{
while (true)
{
cancelToken.ThrowIfCancellationRequested();
if (_queuedFrames.Count == 0)
return;
await Task.Delay(250, cancelToken).ConfigureAwait(false);
}
}
public Task ClearAsync(CancellationToken cancelToken)
{
Frame ignored;
do
cancelToken.ThrowIfCancellationRequested();
while (_queuedFrames.TryDequeue(out ignored));
return Task.Delay(0);
}
protected void Dispose(bool disposing)
{
if (disposing)
_cancelTokenSource.Cancel();
}
public void Dispose()
{
Dispose(true);
}
}
}

+ 0
- 22
src/Discord.Net.WebSocket/Audio/Targets/DirectAudioTarget.cs View File

@@ -1,22 +0,0 @@
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio
{
internal class DirectAudioTarget : IAudioTarget
{
private readonly DiscordVoiceAPIClient _client;
public DirectAudioTarget(DiscordVoiceAPIClient client)
{
_client = client;
}

public Task SendAsync(byte[] buffer, int count)
=> _client.SendAsync(buffer, count);

public Task FlushAsync(CancellationToken cancelToken)
=> Task.Delay(0);
public Task ClearAsync(CancellationToken cancelToken)
=> Task.Delay(0);
}
}

+ 0
- 12
src/Discord.Net.WebSocket/Audio/Targets/IAudioTarget.cs View File

@@ -1,12 +0,0 @@
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio
{
internal interface IAudioTarget
{
Task SendAsync(byte[] buffer, int count);
Task FlushAsync(CancellationToken cancelToken);
Task ClearAsync(CancellationToken cancelToken);
}
}

+ 4
- 4
src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs View File

@@ -64,7 +64,7 @@ namespace Discord.Audio
}; };


WebSocketClient = webSocketProvider(); WebSocketClient = webSocketProvider();
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); //(Causes issues in .Net 4.6+)
WebSocketClient.BinaryMessage += async (data, index, count) => WebSocketClient.BinaryMessage += async (data, index, count) =>
{ {
using (var compressed = new MemoryStream(data, index + 2, count - 2)) using (var compressed = new MemoryStream(data, index + 2, count - 2))
@@ -117,9 +117,9 @@ namespace Discord.Audio
await WebSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); await WebSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
} }
public async Task SendAsync(byte[] data, int bytes)
public async Task SendAsync(byte[] data, int offset, int bytes)
{ {
await _udp.SendAsync(data, 0, bytes).ConfigureAwait(false);
await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false);
await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false);
} }


@@ -224,7 +224,7 @@ namespace Discord.Audio
packet[1] = (byte)(ssrc >> 16); packet[1] = (byte)(ssrc >> 16);
packet[2] = (byte)(ssrc >> 8); packet[2] = (byte)(ssrc >> 8);
packet[3] = (byte)(ssrc >> 0); packet[3] = (byte)(ssrc >> 0);
await SendAsync(packet, 70).ConfigureAwait(false);
await SendAsync(packet, 0, 70).ConfigureAwait(false);
await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false);
} }




Loading…
Cancel
Save