Cleaned up audio code
This commit is contained in:
9
src/Discord.Net.Core/Audio/AudioApplication.cs
Normal file
9
src/Discord.Net.Core/Audio/AudioApplication.cs
Normal file
@@ -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 override void Write(byte[] buffer, int offset, int count)
|
||||||
public virtual Task ClearAsync(CancellationToken cancelToken) { return Task.Delay(0); }
|
{
|
||||||
|
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);
|
var outputStream = new OutputStream(ApiClient);
|
||||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
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);
|
var outputStream = new OutputStream(ApiClient);
|
||||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
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);
|
var outputStream = new OutputStream(ApiClient);
|
||||||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
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);
|
var outputStream = new OutputStream(ApiClient);
|
||||||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
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,
|
SetBitrate = 4002,
|
||||||
GetBitrateRequest = 4003,
|
SetBandwidth = 4008,
|
||||||
SetInbandFECRequest = 4012,
|
SetInbandFEC = 4012,
|
||||||
GetInbandFECRequest = 4013
|
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)
|
||||||
|
|||||||
9
src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs
Normal file
9
src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
internal enum OpusSignal : int
|
||||||
|
{
|
||||||
|
Auto = -1000,
|
||||||
|
Voice = 3001,
|
||||||
|
Music = 3002,
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
Normal file
156
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
internal OpusDecodeStream(AudioClient audioClient, int samplingRate, int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
|
||||||
int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
|
|
||||||
: base(audioClient, secretKey)
|
|
||||||
{
|
{
|
||||||
_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(AudioOutStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000)
|
||||||
|
|
||||||
internal OpusEncodeStream(IAudioTarget target, byte[] secretKey, int channels, int samplesPerFrame, uint ssrc, int? bitrate = null)
|
|
||||||
: base(target, secretKey, samplesPerFrame, ssrc)
|
|
||||||
{
|
{
|
||||||
_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
src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs
Normal file
23
src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 AudioOutStream _next;
|
||||||
private readonly byte[] _nonce, _secretKey;
|
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;
|
_next = next;
|
||||||
_secretKey = secretKey;
|
|
||||||
_samplesPerFrame = samplesPerFrame;
|
_samplesPerFrame = samplesPerFrame;
|
||||||
_ssrc = ssrc;
|
_ssrc = ssrc;
|
||||||
_buffer = new byte[4000];
|
_buffer = new byte[bufferSize];
|
||||||
_nonce = new byte[24];
|
_header = new byte[24];
|
||||||
_nonce[0] = 0x80;
|
_header[0] = 0x80;
|
||||||
_nonce[1] = 0x78;
|
_header[1] = 0x78;
|
||||||
_nonce[8] = (byte)(_ssrc >> 24);
|
_header[8] = (byte)(_ssrc >> 24);
|
||||||
_nonce[9] = (byte)(_ssrc >> 16);
|
_header[9] = (byte)(_ssrc >> 16);
|
||||||
_nonce[10] = (byte)(_ssrc >> 8);
|
_header[10] = (byte)(_ssrc >> 8);
|
||||||
_nonce[11] = (byte)(_ssrc >> 0);
|
_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)
|
if (_header[3]++ == byte.MaxValue)
|
||||||
_nonce[2]++;
|
_header[2]++;
|
||||||
|
|
||||||
_timestamp += (uint)_samplesPerFrame;
|
_timestamp += (uint)_samplesPerFrame;
|
||||||
_nonce[4] = (byte)(_timestamp >> 24);
|
_header[4] = (byte)(_timestamp >> 24);
|
||||||
_nonce[5] = (byte)(_timestamp >> 16);
|
_header[5] = (byte)(_timestamp >> 16);
|
||||||
_nonce[6] = (byte)(_timestamp >> 8);
|
_header[6] = (byte)(_timestamp >> 8);
|
||||||
_nonce[7] = (byte)(_timestamp >> 0);
|
_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);
|
await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false);
|
||||||
Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer
|
|
||||||
await _target.SendAsync(_buffer, count + 12).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Flush()
|
public override async Task FlushAsync(CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
|
||||||
}
|
|
||||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await _target.FlushAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user