@@ -0,0 +1,9 @@ | |||
namespace Discord.Audio | |||
{ | |||
public enum AudioApplication : int | |||
{ | |||
Voice, | |||
Music, | |||
Mixed | |||
} | |||
} |
@@ -1,8 +1,30 @@ | |||
using System.IO; | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
namespace Discord.Audio | |||
{ | |||
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(); } | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using System.IO; | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -10,7 +11,31 @@ namespace Discord.Audio | |||
public override bool CanSeek => false; | |||
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(); } | |||
} | |||
} |
@@ -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="bitrate"></param> | |||
/// <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> | |||
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. | |||
/// </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="bitrate"></param> | |||
/// <returns></returns> | |||
AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null); | |||
AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null); | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using Discord.API.Voice; | |||
using Discord.Audio.Streams; | |||
using Discord.Logging; | |||
using Discord.Net.Converters; | |||
using Discord.WebSocket; | |||
@@ -80,7 +81,7 @@ namespace Discord.Audio | |||
{ | |||
_audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||
e.ErrorContext.Handled = true; | |||
}; | |||
}; | |||
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) | |||
{ | |||
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) | |||
{ | |||
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); | |||
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); | |||
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) | |||
{ | |||
@@ -1,6 +1,6 @@ | |||
namespace Discord.Audio | |||
{ | |||
public enum OpusApplication : int | |||
internal enum OpusApplication : int | |||
{ | |||
Voice = 2048, | |||
MusicOrMixed = 2049, | |||
@@ -1,10 +1,12 @@ | |||
namespace Discord.Audio | |||
{ | |||
//https://github.com/gcp/opus/blob/master/include/opus_defines.h | |||
internal enum OpusCtl : int | |||
{ | |||
SetBitrateRequest = 4002, | |||
GetBitrateRequest = 4003, | |||
SetInbandFECRequest = 4012, | |||
GetInbandFECRequest = 4013 | |||
SetBitrate = 4002, | |||
SetBandwidth = 4008, | |||
SetInbandFEC = 4012, | |||
SetPacketLossPercent = 4014, | |||
SetSignal = 4024 | |||
} | |||
} |
@@ -15,17 +15,62 @@ namespace Discord.Audio | |||
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); | |||
/// <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) | |||
{ | |||
if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) | |||
throw new ArgumentOutOfRangeException(nameof(bitrate)); | |||
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; | |||
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error); | |||
_ptr = CreateEncoder(samplingRate, channels, (int)opusApplication, out error); | |||
if (error != OpusError.OK) | |||
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> | |||
@@ -44,25 +89,6 @@ namespace Discord.Audio | |||
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) | |||
{ | |||
if (_ptr != IntPtr.Zero) | |||
@@ -0,0 +1,9 @@ | |||
namespace Discord.Audio | |||
{ | |||
internal enum OpusSignal : int | |||
{ | |||
Auto = -1000, | |||
Voice = 3001, | |||
Music = 3002, | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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 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]; | |||
_decoder = new OpusDecoder(samplingRate, channels); | |||
_queuedData = new BlockingCollection<byte[]>(100); | |||
} | |||
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); | |||
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) | |||
@@ -2,27 +2,28 @@ | |||
using System.Threading; | |||
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; | |||
private readonly AudioOutStream _next; | |||
private readonly OpusEncoder _encoder; | |||
private readonly byte[] _buffer; | |||
private int _frameSize; | |||
private byte[] _partialFrameBuffer; | |||
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; | |||
_buffer = new byte[bufferSize]; | |||
_partialFrameBuffer = new byte[_frameSize]; | |||
_encoder.SetForwardErrorCorrection(true); | |||
if (bitrate != null) | |||
_encoder.SetBitrate(bitrate.Value); | |||
} | |||
public override void Write(byte[] buffer, int offset, int count) | |||
@@ -43,7 +44,7 @@ namespace Discord.Audio | |||
_partialFramePos = 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 | |||
{ | |||
@@ -54,10 +55,7 @@ namespace Discord.Audio | |||
} | |||
} | |||
/*public override void Flush() | |||
{ | |||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
/* | |||
public override async Task FlushAsync(CancellationToken cancellationToken) | |||
{ | |||
try | |||
@@ -70,6 +68,15 @@ namespace Discord.Audio | |||
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) | |||
{ | |||
base.Dispose(disposing); | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -2,11 +2,13 @@ | |||
using System.Collections.Concurrent; | |||
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<RTPFrame> _queuedData; //TODO: Replace with max-length ring buffer | |||
private readonly AudioClient _audioClient; | |||
private readonly byte[] _buffer, _nonce, _secretKey; | |||
@@ -23,6 +25,12 @@ namespace Discord.Audio | |||
_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) | |||
{ | |||
var queuedData = _queuedData.Take(); | |||
@@ -31,10 +39,8 @@ namespace Discord.Audio | |||
} | |||
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]; | |||
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); | |||
Buffer.BlockCopy(buffer, 0, newBuffer, 0, count); | |||
_queuedData.Add(newBuffer); | |||
} | |||
@@ -1,33 +1,32 @@ | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
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 uint _ssrc, _timestamp = 0; | |||
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; | |||
_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) | |||
@@ -39,48 +38,28 @@ namespace Discord.Audio | |||
cancellationToken.ThrowIfCancellationRequested(); | |||
unchecked | |||
{ | |||
if (_nonce[3]++ == byte.MaxValue) | |||
_nonce[2]++; | |||
if (_header[3]++ == byte.MaxValue) | |||
_header[2]++; | |||
_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) | |||
{ | |||
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(); } | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -64,7 +64,7 @@ namespace Discord.Audio | |||
}; | |||
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) => | |||
{ | |||
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 _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); | |||
} | |||
@@ -224,7 +224,7 @@ namespace Discord.Audio | |||
packet[1] = (byte)(ssrc >> 16); | |||
packet[2] = (byte)(ssrc >> 8); | |||
packet[3] = (byte)(ssrc >> 0); | |||
await SendAsync(packet, 70).ConfigureAwait(false); | |||
await SendAsync(packet, 0, 70).ConfigureAwait(false); | |||
await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); | |||
} | |||