Browse Source

Added a limit to BufferedAudioTarget's internal buffer.

tags/1.0-rc
RogueException 8 years ago
parent
commit
58cb8cfb20
4 changed files with 82 additions and 47 deletions
  1. +2
    -2
      src/Discord.Net.Core/Audio/IAudioClient.cs
  2. +7
    -7
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  3. +63
    -28
      src/Discord.Net.WebSocket/Audio/Targets/BufferedAudioTarget.cs
  4. +10
    -10
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs

+ 2
- 2
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -22,7 +22,7 @@ namespace Discord.Audio
/// </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>
/// <returns></returns> /// <returns></returns>
Stream CreateOpusStream(int samplesPerFrame);
Stream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000);
/// <summary> /// <summary>
/// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.
/// </summary> /// </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="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>
Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null);
Stream CreatePCMStream(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>


+ 7
- 7
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -13,7 +13,7 @@ using System.Threading.Tasks;


namespace Discord.Audio namespace Discord.Audio
{ {
public class AudioClient : IAudioClient, IDisposable
internal class AudioClient : IAudioClient, IDisposable
{ {
public event Func<Task> Connected public event Func<Task> Connected
{ {
@@ -74,7 +74,7 @@ namespace Discord.Audio


ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").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.ReceivedEvent += ProcessMessageAsync;
ApiClient.ReceivedPacket += ProcessPacketAsync; ApiClient.ReceivedPacket += ProcessPacketAsync;
ApiClient.Disconnected += async ex => ApiClient.Disconnected += async ex =>
@@ -170,10 +170,10 @@ namespace Discord.Audio
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); 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); 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); return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
} }
public Stream CreateDirectOpusStream(int samplesPerFrame) public Stream CreateDirectOpusStream(int samplesPerFrame)
@@ -182,13 +182,13 @@ namespace Discord.Audio
var target = new DirectAudioTarget(ApiClient); var target = new DirectAudioTarget(ApiClient);
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); 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); 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); 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); CheckSamplesPerFrame(samplesPerFrame);
var target = new DirectAudioTarget(ApiClient); var target = new DirectAudioTarget(ApiClient);


+ 63
- 28
src/Discord.Net.WebSocket/Audio/Targets/BufferedAudioTarget.cs View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -8,62 +7,98 @@ namespace Discord.Audio
{ {
internal class BufferedAudioTarget : IAudioTarget, IDisposable 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 Task _task;
private DiscordVoiceAPIClient _client; private DiscordVoiceAPIClient _client;
private CancellationTokenSource _cancelTokenSource; 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; _client = client;
long ticksPerFrame = samplesPerFrame / 48;
_ticksPerFrame = samplesPerFrame / 48;
int queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up


_cancelTokenSource = new CancellationTokenSource(); _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 () => 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() public async Task FlushAsync()
{ {
while (true) while (true)
{ {
if (_queue.Count == 0)
if (_queuedFrames.Count == 0)
return; return;
await Task.Delay(250).ConfigureAwait(false); await Task.Delay(250).ConfigureAwait(false);
} }


+ 10
- 10
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -32,6 +32,7 @@ namespace Discord.WebSocket
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
private ImmutableArray<GuildEmoji> _emojis; private ImmutableArray<GuildEmoji> _emojis;
private ImmutableArray<string> _features; private ImmutableArray<string> _features;
private AudioClient _audioClient;
internal bool _available; internal bool _available;


public string Name { get; private set; } public string Name { get; private set; }
@@ -42,7 +43,6 @@ namespace Discord.WebSocket
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }
public int MemberCount { get; set; } public int MemberCount { get; set; }
public int DownloadedMemberCount { get; private set; } public int DownloadedMemberCount { get; private set; }
public AudioClient AudioClient { get; private set; }


public ulong? AFKChannelId { get; private set; } public ulong? AFKChannelId { get; private set; }
public ulong? EmbedChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; }
@@ -59,6 +59,7 @@ namespace Discord.WebSocket
public bool IsSynced => _syncPromise.Task.IsCompleted; public bool IsSynced => _syncPromise.Task.IsCompleted;
public Task SyncPromise => _syncPromise.Task; public Task SyncPromise => _syncPromise.Task;
public Task DownloaderPromise => _downloaderPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task;
public IAudioClient AudioClient => _audioClient;
public SocketGuildUser CurrentUser public SocketGuildUser CurrentUser
{ {
get get
@@ -69,7 +70,6 @@ namespace Discord.WebSocket
return null; return null;
} }
} }

public SocketRole EveryoneRole => GetRole(Id); public SocketRole EveryoneRole => GetRole(Id);
public IReadOnlyCollection<SocketGuildChannel> Channels public IReadOnlyCollection<SocketGuildChannel> Channels
{ {
@@ -476,9 +476,9 @@ namespace Discord.WebSocket
{ {
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
_audioConnectPromise = null; _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) internal async Task FinishConnectAudio(int id, string url, string token)
{ {
@@ -487,7 +487,7 @@ namespace Discord.WebSocket
await _audioLock.WaitAsync().ConfigureAwait(false); await _audioLock.WaitAsync().ConfigureAwait(false);
try try
{ {
if (AudioClient == null)
if (_audioClient == null)
{ {
var audioClient = new AudioClient(this, id); var audioClient = new AudioClient(this, id);
var promise = _audioConnectPromise; var promise = _audioConnectPromise;
@@ -497,7 +497,7 @@ namespace Discord.WebSocket
if (!promise.Task.IsCompleted) if (!promise.Task.IsCompleted)
{ {
try { audioClient.Dispose(); } catch { } try { audioClient.Dispose(); } catch { }
AudioClient = null;
_audioClient = null;
if (ex != null) if (ex != null)
await promise.TrySetExceptionAsync(ex); await promise.TrySetExceptionAsync(ex);
else else
@@ -535,10 +535,10 @@ namespace Discord.WebSocket
_audioLock.Release(); _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) catch (OperationCanceledException)
{ {


Loading…
Cancel
Save