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