@@ -22,7 +22,7 @@ namespace Discord.Audio | |||
/// </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> | |||
/// <returns></returns> | |||
Stream CreateOpusStream(int samplesPerFrame); | |||
Stream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); | |||
/// <summary> | |||
/// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. | |||
/// </summary> | |||
@@ -35,7 +35,7 @@ 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> | |||
Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null); | |||
Stream CreatePCMStream(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> | |||
@@ -13,7 +13,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public class AudioClient : IAudioClient, IDisposable | |||
internal class AudioClient : IAudioClient, IDisposable | |||
{ | |||
public event Func<Task> Connected | |||
{ | |||
@@ -74,7 +74,7 @@ namespace Discord.Audio | |||
ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); | |||
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); | |||
ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); | |||
//ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); | |||
ApiClient.ReceivedEvent += ProcessMessageAsync; | |||
ApiClient.ReceivedPacket += ProcessPacketAsync; | |||
ApiClient.Disconnected += async ex => | |||
@@ -170,10 +170,10 @@ namespace Discord.Audio | |||
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); | |||
} | |||
public Stream CreateOpusStream(int samplesPerFrame) | |||
public Stream CreateOpusStream(int samplesPerFrame, int bufferMillis) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, _cancelTokenSource.Token); | |||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | |||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | |||
} | |||
public Stream CreateDirectOpusStream(int samplesPerFrame) | |||
@@ -182,13 +182,13 @@ namespace Discord.Audio | |||
var target = new DirectAudioTarget(ApiClient); | |||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | |||
} | |||
public Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null) | |||
public Stream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, _cancelTokenSource.Token); | |||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | |||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate); | |||
} | |||
public Stream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null) | |||
public Stream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new DirectAudioTarget(ApiClient); | |||
@@ -1,6 +1,5 @@ | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Diagnostics; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -8,62 +7,98 @@ namespace Discord.Audio | |||
{ | |||
internal class BufferedAudioTarget : IAudioTarget, IDisposable | |||
{ | |||
private static readonly byte[] _silencePacket = new byte[] { 0xF8, 0xFF, 0xFE }; | |||
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 ConcurrentQueue<byte[]> _queue; | |||
private CancellationToken _cancelToken; | |||
private ConcurrentQueue<Frame> _queuedFrames; | |||
private ConcurrentQueue<byte[]> _bufferPool; | |||
private SemaphoreSlim _queueLock; | |||
private int _ticksPerFrame; | |||
internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, CancellationToken cancelToken) | |||
internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken) | |||
{ | |||
_client = client; | |||
long ticksPerFrame = samplesPerFrame / 48; | |||
_ticksPerFrame = samplesPerFrame / 48; | |||
int queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up | |||
_cancelTokenSource = new CancellationTokenSource(); | |||
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; | |||
_queue = new ConcurrentQueue<byte[]>(); //TODO: We need a better queue | |||
_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(ticksPerFrame, cancelToken); | |||
_task = Run(); | |||
} | |||
private Task Run(long ticksPerFrame, CancellationToken cancelToken) | |||
private Task Run() | |||
{ | |||
return Task.Run(async () => | |||
{ | |||
long nextTick = Environment.TickCount; | |||
while (!cancelToken.IsCancellationRequested) | |||
try | |||
{ | |||
long tick = Environment.TickCount; | |||
long dist = nextTick - tick; | |||
if (dist <= 0) | |||
long nextTick = Environment.TickCount; | |||
while (!_cancelToken.IsCancellationRequested) | |||
{ | |||
byte[] buffer; | |||
if (_queue.TryDequeue(out buffer)) | |||
await _client.SendAsync(buffer, buffer.Length).ConfigureAwait(false); | |||
else | |||
await _client.SendAsync(_silencePacket, _silencePacket.Length).ConfigureAwait(false); | |||
nextTick += ticksPerFrame; | |||
long tick = Environment.TickCount; | |||
long dist = nextTick - tick; | |||
if (dist <= 0) | |||
{ | |||
Frame frame; | |||
if (_queuedFrames.TryDequeue(out frame)) | |||
{ | |||
#if NETSTANDARD1_3 | |||
Console.WriteLine("Pop"); | |||
#endif | |||
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); | |||
} | |||
else if (dist > 1) | |||
await Task.Delay((int)dist).ConfigureAwait(false); | |||
} | |||
catch (OperationCanceledException) { } | |||
}); | |||
} | |||
public Task SendAsync(byte[] buffer, int count) | |||
public async Task SendAsync(byte[] data, int count) | |||
{ | |||
byte[] newBuffer = new byte[count]; | |||
Buffer.BlockCopy(buffer, 0, newBuffer, 0, count); | |||
_queue.Enqueue(newBuffer); | |||
return Task.Delay(0); | |||
await _queueLock.WaitAsync(-1, _cancelToken).ConfigureAwait(false); | |||
#if NETSTANDARD1_3 | |||
Console.WriteLine("Push"); | |||
#endif | |||
byte[] buffer; | |||
_bufferPool.TryDequeue(out buffer); | |||
Buffer.BlockCopy(data, 0, buffer, 0, count); | |||
_queuedFrames.Enqueue(new Frame(buffer, count)); | |||
} | |||
public async Task FlushAsync() | |||
{ | |||
while (true) | |||
{ | |||
if (_queue.Count == 0) | |||
if (_queuedFrames.Count == 0) | |||
return; | |||
await Task.Delay(250).ConfigureAwait(false); | |||
} | |||
@@ -32,6 +32,7 @@ namespace Discord.WebSocket | |||
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | |||
private ImmutableArray<GuildEmoji> _emojis; | |||
private ImmutableArray<string> _features; | |||
private AudioClient _audioClient; | |||
internal bool _available; | |||
public string Name { get; private set; } | |||
@@ -42,7 +43,6 @@ namespace Discord.WebSocket | |||
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | |||
public int MemberCount { get; set; } | |||
public int DownloadedMemberCount { get; private set; } | |||
public AudioClient AudioClient { get; private set; } | |||
public ulong? AFKChannelId { get; private set; } | |||
public ulong? EmbedChannelId { get; private set; } | |||
@@ -59,6 +59,7 @@ namespace Discord.WebSocket | |||
public bool IsSynced => _syncPromise.Task.IsCompleted; | |||
public Task SyncPromise => _syncPromise.Task; | |||
public Task DownloaderPromise => _downloaderPromise.Task; | |||
public IAudioClient AudioClient => _audioClient; | |||
public SocketGuildUser CurrentUser | |||
{ | |||
get | |||
@@ -69,7 +70,6 @@ namespace Discord.WebSocket | |||
return null; | |||
} | |||
} | |||
public SocketRole EveryoneRole => GetRole(Id); | |||
public IReadOnlyCollection<SocketGuildChannel> Channels | |||
{ | |||
@@ -476,9 +476,9 @@ namespace Discord.WebSocket | |||
{ | |||
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection | |||
_audioConnectPromise = null; | |||
if (AudioClient != null) | |||
await AudioClient.DisconnectAsync().ConfigureAwait(false); | |||
AudioClient = null; | |||
if (_audioClient != null) | |||
await _audioClient.DisconnectAsync().ConfigureAwait(false); | |||
_audioClient = null; | |||
} | |||
internal async Task FinishConnectAudio(int id, string url, string token) | |||
{ | |||
@@ -487,7 +487,7 @@ namespace Discord.WebSocket | |||
await _audioLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
if (AudioClient == null) | |||
if (_audioClient == null) | |||
{ | |||
var audioClient = new AudioClient(this, id); | |||
var promise = _audioConnectPromise; | |||
@@ -497,7 +497,7 @@ namespace Discord.WebSocket | |||
if (!promise.Task.IsCompleted) | |||
{ | |||
try { audioClient.Dispose(); } catch { } | |||
AudioClient = null; | |||
_audioClient = null; | |||
if (ex != null) | |||
await promise.TrySetExceptionAsync(ex); | |||
else | |||
@@ -535,10 +535,10 @@ namespace Discord.WebSocket | |||
_audioLock.Release(); | |||
}*/ | |||
}; | |||
AudioClient = audioClient; | |||
_audioClient = audioClient; | |||
} | |||
await AudioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); | |||
await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); | |||
await _audioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); | |||
await _audioConnectPromise.TrySetResultAsync(_audioClient).ConfigureAwait(false); | |||
} | |||
catch (OperationCanceledException) | |||
{ | |||