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
|
||||
{
|
||||
public abstract class AudioInStream : Stream
|
||||
{
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override void Flush() { throw new NotSupportedException(); }
|
||||
|
||||
public override long Length { get { throw new NotSupportedException(); } }
|
||||
public override long Position
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override void SetLength(long value) { throw new NotSupportedException(); }
|
||||
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -10,7 +11,31 @@ namespace Discord.Audio
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public virtual void Clear() { }
|
||||
public virtual Task ClearAsync(CancellationToken cancelToken) { return Task.Delay(0); }
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
public override void Flush()
|
||||
{
|
||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
public void Clear()
|
||||
{
|
||||
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); }
|
||||
//public virtual Task WriteSilenceAsync(CancellationToken cancellationToken) { return Task.Delay(0); }
|
||||
|
||||
public override long Length { get { throw new NotSupportedException(); } }
|
||||
public override long Position
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
|
||||
public override void SetLength(long value) { throw new NotSupportedException(); }
|
||||
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ namespace Discord.Audio
|
||||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
|
||||
/// <param name="bitrate"></param>
|
||||
/// <returns></returns>
|
||||
AudioOutStream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000);
|
||||
AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000);
|
||||
/// <summary>
|
||||
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.
|
||||
/// </summary>
|
||||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
|
||||
/// <param name="bitrate"></param>
|
||||
/// <returns></returns>
|
||||
AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null);
|
||||
AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Discord.API.Voice;
|
||||
using Discord.Audio.Streams;
|
||||
using Discord.Logging;
|
||||
using Discord.Net.Converters;
|
||||
using Discord.WebSocket;
|
||||
@@ -80,7 +81,7 @@ namespace Discord.Audio
|
||||
{
|
||||
_audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult();
|
||||
e.ErrorContext.Handled = true;
|
||||
};
|
||||
};
|
||||
|
||||
LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false);
|
||||
}
|
||||
@@ -129,26 +130,34 @@ namespace Discord.Audio
|
||||
public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis)
|
||||
{
|
||||
CheckSamplesPerFrame(samplesPerFrame);
|
||||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _connection.CancelToken);
|
||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
||||
var outputStream = new OutputStream(ApiClient);
|
||||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
|
||||
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
|
||||
return new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger);
|
||||
}
|
||||
public AudioOutStream CreateDirectOpusStream(int samplesPerFrame)
|
||||
{
|
||||
CheckSamplesPerFrame(samplesPerFrame);
|
||||
var target = new DirectAudioTarget(ApiClient);
|
||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
||||
var outputStream = new OutputStream(ApiClient);
|
||||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
|
||||
return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
|
||||
}
|
||||
public AudioOutStream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
|
||||
public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
|
||||
{
|
||||
CheckSamplesPerFrame(samplesPerFrame);
|
||||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _connection.CancelToken);
|
||||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
||||
var outputStream = new OutputStream(ApiClient);
|
||||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
|
||||
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
|
||||
var bufferedStream = new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger);
|
||||
return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application);
|
||||
}
|
||||
public AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate)
|
||||
public AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate)
|
||||
{
|
||||
CheckSamplesPerFrame(samplesPerFrame);
|
||||
var target = new DirectAudioTarget(ApiClient);
|
||||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
||||
var outputStream = new OutputStream(ApiClient);
|
||||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey);
|
||||
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc);
|
||||
return new OpusEncodeStream(rtpWriter, channels, samplesPerFrame, bitrate ?? (96 * 1024), application);
|
||||
}
|
||||
private void CheckSamplesPerFrame(int samplesPerFrame)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Discord.Audio
|
||||
{
|
||||
public enum OpusApplication : int
|
||||
internal enum OpusApplication : int
|
||||
{
|
||||
Voice = 2048,
|
||||
MusicOrMixed = 2049,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
namespace Discord.Audio
|
||||
{
|
||||
//https://github.com/gcp/opus/blob/master/include/opus_defines.h
|
||||
internal enum OpusCtl : int
|
||||
{
|
||||
SetBitrateRequest = 4002,
|
||||
GetBitrateRequest = 4003,
|
||||
SetInbandFECRequest = 4012,
|
||||
GetInbandFECRequest = 4013
|
||||
SetBitrate = 4002,
|
||||
SetBandwidth = 4008,
|
||||
SetInbandFEC = 4012,
|
||||
SetPacketLossPercent = 4014,
|
||||
SetSignal = 4024
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,17 +15,62 @@ namespace Discord.Audio
|
||||
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value);
|
||||
|
||||
/// <summary> Gets the coding mode of the encoder. </summary>
|
||||
public OpusApplication Application { get; }
|
||||
public AudioApplication Application { get; }
|
||||
public int BitRate { get;}
|
||||
|
||||
public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed)
|
||||
public OpusEncoder(int samplingRate, int channels, int bitrate, AudioApplication application)
|
||||
: base(samplingRate, channels)
|
||||
{
|
||||
if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)
|
||||
throw new ArgumentOutOfRangeException(nameof(bitrate));
|
||||
|
||||
Application = application;
|
||||
BitRate = bitrate;
|
||||
|
||||
OpusApplication opusApplication;
|
||||
OpusSignal opusSignal;
|
||||
switch (application)
|
||||
{
|
||||
case AudioApplication.Mixed:
|
||||
opusApplication = OpusApplication.MusicOrMixed;
|
||||
opusSignal = OpusSignal.Auto;
|
||||
break;
|
||||
case AudioApplication.Music:
|
||||
opusApplication = OpusApplication.MusicOrMixed;
|
||||
opusSignal = OpusSignal.Music;
|
||||
break;
|
||||
case AudioApplication.Voice:
|
||||
opusApplication = OpusApplication.Voice;
|
||||
opusSignal = OpusSignal.Voice;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(application));
|
||||
}
|
||||
|
||||
OpusError error;
|
||||
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
|
||||
_ptr = CreateEncoder(samplingRate, channels, (int)opusApplication, out error);
|
||||
if (error != OpusError.OK)
|
||||
throw new Exception($"Opus Error: {error}");
|
||||
|
||||
var result = EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal);
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");
|
||||
|
||||
result = EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 5); //%%
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");
|
||||
|
||||
result = EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1); //True
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");
|
||||
|
||||
result = EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate);
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");
|
||||
|
||||
/*result = EncoderCtl(_ptr, OpusCtl.SetBandwidth, 1105);
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");*/
|
||||
}
|
||||
|
||||
/// <summary> Produces Opus encoded audio from PCM samples. </summary>
|
||||
@@ -44,25 +89,6 @@ namespace Discord.Audio
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
|
||||
public void SetForwardErrorCorrection(bool value)
|
||||
{
|
||||
var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0);
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");
|
||||
}
|
||||
|
||||
/// <summary> Gets or sets the encoder's bitrate. </summary>
|
||||
public void SetBitrate(int value)
|
||||
{
|
||||
if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value);
|
||||
if (result < 0)
|
||||
throw new Exception($"Opus Error: {(OpusError)result}");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_ptr != IntPtr.Zero)
|
||||
|
||||
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 OpusDecoder _decoder;
|
||||
|
||||
internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate,
|
||||
int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
|
||||
: base(audioClient, secretKey)
|
||||
internal OpusDecodeStream(AudioClient audioClient, int samplingRate, int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
|
||||
{
|
||||
_buffer = new byte[bufferSize];
|
||||
_decoder = new OpusDecoder(samplingRate, channels);
|
||||
_queuedData = new BlockingCollection<byte[]>(100);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var queuedData = _queuedData.Take();
|
||||
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
|
||||
return queuedData.Length;
|
||||
}
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0);
|
||||
return base.Read(_buffer, 0, count);
|
||||
var newBuffer = new byte[count];
|
||||
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
|
||||
_queuedData.Add(newBuffer);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -2,27 +2,28 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Audio
|
||||
namespace Discord.Audio.Streams
|
||||
{
|
||||
internal class OpusEncodeStream : RTPWriteStream
|
||||
///<summary> Converts PCM to Opus </summary>
|
||||
public class OpusEncodeStream : AudioOutStream
|
||||
{
|
||||
public const int SampleRate = 48000;
|
||||
|
||||
private readonly AudioOutStream _next;
|
||||
private readonly OpusEncoder _encoder;
|
||||
private readonly byte[] _buffer;
|
||||
|
||||
private int _frameSize;
|
||||
private byte[] _partialFrameBuffer;
|
||||
private int _partialFramePos;
|
||||
|
||||
private readonly OpusEncoder _encoder;
|
||||
|
||||
internal OpusEncodeStream(IAudioTarget target, byte[] secretKey, int channels, int samplesPerFrame, uint ssrc, int? bitrate = null)
|
||||
: base(target, secretKey, samplesPerFrame, ssrc)
|
||||
internal OpusEncodeStream(AudioOutStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000)
|
||||
{
|
||||
_encoder = new OpusEncoder(SampleRate, channels);
|
||||
_next = next;
|
||||
_encoder = new OpusEncoder(SampleRate, channels, bitrate, application);
|
||||
_frameSize = samplesPerFrame * channels * 2;
|
||||
_buffer = new byte[bufferSize];
|
||||
_partialFrameBuffer = new byte[_frameSize];
|
||||
|
||||
_encoder.SetForwardErrorCorrection(true);
|
||||
if (bitrate != null)
|
||||
_encoder.SetBitrate(bitrate.Value);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
@@ -43,7 +44,7 @@ namespace Discord.Audio
|
||||
_partialFramePos = 0;
|
||||
|
||||
int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _frameSize, _buffer, 0);
|
||||
await base.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false);
|
||||
await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -54,10 +55,7 @@ namespace Discord.Audio
|
||||
}
|
||||
}
|
||||
|
||||
/*public override void Flush()
|
||||
{
|
||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
/*
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -70,6 +68,15 @@ namespace Discord.Audio
|
||||
await base.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}*/
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancelToken)
|
||||
{
|
||||
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
public override async Task ClearAsync(CancellationToken cancelToken)
|
||||
{
|
||||
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
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.IO;
|
||||
|
||||
namespace Discord.Audio
|
||||
namespace Discord.Audio.Streams
|
||||
{
|
||||
internal class RTPReadStream : Stream
|
||||
///<summary> Reads the payload from an RTP frame </summary>
|
||||
public class RTPReadStream : AudioInStream
|
||||
{
|
||||
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
|
||||
//private readonly BlockingCollection<RTPFrame> _queuedData; //TODO: Replace with max-length ring buffer
|
||||
private readonly AudioClient _audioClient;
|
||||
private readonly byte[] _buffer, _nonce, _secretKey;
|
||||
|
||||
@@ -23,6 +25,12 @@ namespace Discord.Audio
|
||||
_nonce = new byte[24];
|
||||
}
|
||||
|
||||
/*public RTPFrame ReadFrame()
|
||||
{
|
||||
var queuedData = _queuedData.Take();
|
||||
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
|
||||
return queuedData.Length;
|
||||
}*/
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var queuedData = _queuedData.Take();
|
||||
@@ -31,10 +39,8 @@ namespace Discord.Audio
|
||||
}
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12);
|
||||
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey);
|
||||
var newBuffer = new byte[count];
|
||||
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
|
||||
Buffer.BlockCopy(buffer, 0, newBuffer, 0, count);
|
||||
_queuedData.Add(newBuffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Audio
|
||||
namespace Discord.Audio.Streams
|
||||
{
|
||||
internal class RTPWriteStream : AudioOutStream
|
||||
///<summary> Wraps data in an RTP frame </summary>
|
||||
public class RTPWriteStream : AudioOutStream
|
||||
{
|
||||
private readonly IAudioTarget _target;
|
||||
private readonly byte[] _nonce, _secretKey;
|
||||
private readonly AudioOutStream _next;
|
||||
private readonly byte[] _header;
|
||||
private int _samplesPerFrame;
|
||||
private uint _ssrc, _timestamp = 0;
|
||||
|
||||
protected readonly byte[] _buffer;
|
||||
|
||||
internal RTPWriteStream(IAudioTarget target, byte[] secretKey, int samplesPerFrame, uint ssrc)
|
||||
internal RTPWriteStream(AudioOutStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000)
|
||||
{
|
||||
_target = target;
|
||||
_secretKey = secretKey;
|
||||
_next = next;
|
||||
_samplesPerFrame = samplesPerFrame;
|
||||
_ssrc = ssrc;
|
||||
_buffer = new byte[4000];
|
||||
_nonce = new byte[24];
|
||||
_nonce[0] = 0x80;
|
||||
_nonce[1] = 0x78;
|
||||
_nonce[8] = (byte)(_ssrc >> 24);
|
||||
_nonce[9] = (byte)(_ssrc >> 16);
|
||||
_nonce[10] = (byte)(_ssrc >> 8);
|
||||
_nonce[11] = (byte)(_ssrc >> 0);
|
||||
_buffer = new byte[bufferSize];
|
||||
_header = new byte[24];
|
||||
_header[0] = 0x80;
|
||||
_header[1] = 0x78;
|
||||
_header[8] = (byte)(_ssrc >> 24);
|
||||
_header[9] = (byte)(_ssrc >> 16);
|
||||
_header[10] = (byte)(_ssrc >> 8);
|
||||
_header[11] = (byte)(_ssrc >> 0);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
@@ -39,48 +38,28 @@ namespace Discord.Audio
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
unchecked
|
||||
{
|
||||
if (_nonce[3]++ == byte.MaxValue)
|
||||
_nonce[2]++;
|
||||
if (_header[3]++ == byte.MaxValue)
|
||||
_header[2]++;
|
||||
|
||||
_timestamp += (uint)_samplesPerFrame;
|
||||
_nonce[4] = (byte)(_timestamp >> 24);
|
||||
_nonce[5] = (byte)(_timestamp >> 16);
|
||||
_nonce[6] = (byte)(_timestamp >> 8);
|
||||
_nonce[7] = (byte)(_timestamp >> 0);
|
||||
_header[4] = (byte)(_timestamp >> 24);
|
||||
_header[5] = (byte)(_timestamp >> 16);
|
||||
_header[6] = (byte)(_timestamp >> 8);
|
||||
_header[7] = (byte)(_timestamp >> 0);
|
||||
}
|
||||
Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer
|
||||
Buffer.BlockCopy(buffer, offset, _buffer, 12, count);
|
||||
|
||||
count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey);
|
||||
Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer
|
||||
await _target.SendAsync(_buffer, count + 12).ConfigureAwait(false);
|
||||
await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
public override async Task FlushAsync(CancellationToken cancelToken)
|
||||
{
|
||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _target.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
public override async Task ClearAsync(CancellationToken cancelToken)
|
||||
{
|
||||
await _target.ClearAsync(cancelToken).ConfigureAwait(false);
|
||||
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override long Length { get { throw new NotSupportedException(); } }
|
||||
public override long Position
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
|
||||
public override void SetLength(long value) { throw new NotSupportedException(); }
|
||||
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Discord.Audio.Streams
|
||||
{
|
||||
///<summary> Decrypts an RTP frame using libsodium </summary>
|
||||
public class SodiumDecryptStream : AudioInStream
|
||||
{
|
||||
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
|
||||
private readonly AudioClient _audioClient;
|
||||
private readonly byte[] _buffer, _nonce, _secretKey;
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
internal SodiumDecryptStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000)
|
||||
{
|
||||
_audioClient = audioClient;
|
||||
_secretKey = secretKey;
|
||||
_buffer = new byte[bufferSize];
|
||||
_queuedData = new BlockingCollection<byte[]>(100);
|
||||
_nonce = new byte[24];
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var queuedData = _queuedData.Take();
|
||||
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
|
||||
return queuedData.Length;
|
||||
}
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce
|
||||
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey);
|
||||
|
||||
var newBuffer = new byte[count];
|
||||
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
|
||||
_queuedData.Add(newBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Audio.Streams
|
||||
{
|
||||
///<summary> Encrypts an RTP frame using libsodium </summary>
|
||||
public class SodiumEncryptStream : AudioOutStream
|
||||
{
|
||||
private readonly AudioOutStream _next;
|
||||
private readonly byte[] _nonce, _secretKey;
|
||||
|
||||
//protected readonly byte[] _buffer;
|
||||
|
||||
internal SodiumEncryptStream(AudioOutStream next, byte[] secretKey/*, int bufferSize = 4000*/)
|
||||
{
|
||||
_next = next;
|
||||
_secretKey = secretKey;
|
||||
//_buffer = new byte[bufferSize]; //TODO: Can Sodium do an in-place encrypt?
|
||||
_nonce = new byte[24];
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header
|
||||
count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _secretKey);
|
||||
await _next.WriteAsync(buffer, 0, count + 12, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancelToken)
|
||||
{
|
||||
await _next.FlushAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
public override async Task ClearAsync(CancellationToken cancelToken)
|
||||
{
|
||||
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Audio
|
||||
{
|
||||
internal class BufferedAudioTarget : IAudioTarget, IDisposable
|
||||
{
|
||||
private struct Frame
|
||||
{
|
||||
public Frame(byte[] buffer, int bytes)
|
||||
{
|
||||
Buffer = buffer;
|
||||
Bytes = bytes;
|
||||
}
|
||||
|
||||
public readonly byte[] Buffer;
|
||||
public readonly int Bytes;
|
||||
}
|
||||
|
||||
private static readonly byte[] _silenceFrame = new byte[] { 0xF8, 0xFF, 0xFE };
|
||||
|
||||
private Task _task;
|
||||
private DiscordVoiceAPIClient _client;
|
||||
private CancellationTokenSource _cancelTokenSource;
|
||||
private CancellationToken _cancelToken;
|
||||
private ConcurrentQueue<Frame> _queuedFrames;
|
||||
private ConcurrentQueue<byte[]> _bufferPool;
|
||||
private SemaphoreSlim _queueLock;
|
||||
private int _ticksPerFrame;
|
||||
|
||||
internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken)
|
||||
{
|
||||
_client = client;
|
||||
_ticksPerFrame = samplesPerFrame / 48;
|
||||
int queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up
|
||||
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token;
|
||||
_queuedFrames = new ConcurrentQueue<Frame>();
|
||||
_bufferPool = new ConcurrentQueue<byte[]>();
|
||||
for (int i = 0; i < queueLength; i++)
|
||||
_bufferPool.Enqueue(new byte[1275]);
|
||||
_queueLock = new SemaphoreSlim(queueLength, queueLength);
|
||||
|
||||
_task = Run();
|
||||
}
|
||||
|
||||
private Task Run()
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
long nextTick = Environment.TickCount;
|
||||
while (!_cancelToken.IsCancellationRequested)
|
||||
{
|
||||
long tick = Environment.TickCount;
|
||||
long dist = nextTick - tick;
|
||||
if (dist <= 0)
|
||||
{
|
||||
Frame frame;
|
||||
if (_queuedFrames.TryDequeue(out frame))
|
||||
{
|
||||
await _client.SendAsync(frame.Buffer, frame.Bytes).ConfigureAwait(false);
|
||||
_bufferPool.Enqueue(frame.Buffer);
|
||||
_queueLock.Release();
|
||||
}
|
||||
else
|
||||
await _client.SendAsync(_silenceFrame, _silenceFrame.Length).ConfigureAwait(false);
|
||||
nextTick += _ticksPerFrame;
|
||||
}
|
||||
else if (dist > 1)
|
||||
await Task.Delay((int)dist).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] data, int count)
|
||||
{
|
||||
await _queueLock.WaitAsync(-1, _cancelToken).ConfigureAwait(false);
|
||||
byte[] buffer;
|
||||
_bufferPool.TryDequeue(out buffer);
|
||||
Buffer.BlockCopy(data, 0, buffer, 0, count);
|
||||
_queuedFrames.Enqueue(new Frame(buffer, count));
|
||||
}
|
||||
|
||||
public async Task FlushAsync(CancellationToken cancelToken)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
if (_queuedFrames.Count == 0)
|
||||
return;
|
||||
await Task.Delay(250, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
public Task ClearAsync(CancellationToken cancelToken)
|
||||
{
|
||||
Frame ignored;
|
||||
do
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
while (_queuedFrames.TryDequeue(out ignored));
|
||||
return Task.Delay(0);
|
||||
}
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
_cancelTokenSource.Cancel();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Audio
|
||||
{
|
||||
internal class DirectAudioTarget : IAudioTarget
|
||||
{
|
||||
private readonly DiscordVoiceAPIClient _client;
|
||||
public DirectAudioTarget(DiscordVoiceAPIClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public Task SendAsync(byte[] buffer, int count)
|
||||
=> _client.SendAsync(buffer, count);
|
||||
|
||||
public Task FlushAsync(CancellationToken cancelToken)
|
||||
=> Task.Delay(0);
|
||||
public Task ClearAsync(CancellationToken cancelToken)
|
||||
=> Task.Delay(0);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Audio
|
||||
{
|
||||
internal interface IAudioTarget
|
||||
{
|
||||
Task SendAsync(byte[] buffer, int count);
|
||||
Task FlushAsync(CancellationToken cancelToken);
|
||||
Task ClearAsync(CancellationToken cancelToken);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ namespace Discord.Audio
|
||||
};
|
||||
|
||||
WebSocketClient = webSocketProvider();
|
||||
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
|
||||
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); //(Causes issues in .Net 4.6+)
|
||||
WebSocketClient.BinaryMessage += async (data, index, count) =>
|
||||
{
|
||||
using (var compressed = new MemoryStream(data, index + 2, count - 2))
|
||||
@@ -117,9 +117,9 @@ namespace Discord.Audio
|
||||
await WebSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false);
|
||||
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
|
||||
}
|
||||
public async Task SendAsync(byte[] data, int bytes)
|
||||
public async Task SendAsync(byte[] data, int offset, int bytes)
|
||||
{
|
||||
await _udp.SendAsync(data, 0, bytes).ConfigureAwait(false);
|
||||
await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false);
|
||||
await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace Discord.Audio
|
||||
packet[1] = (byte)(ssrc >> 16);
|
||||
packet[2] = (byte)(ssrc >> 8);
|
||||
packet[3] = (byte)(ssrc >> 0);
|
||||
await SendAsync(packet, 70).ConfigureAwait(false);
|
||||
await SendAsync(packet, 0, 70).ConfigureAwait(false);
|
||||
await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user