diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 4e62571e5..34255168a 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -133,7 +133,7 @@ namespace Discord #if !DNXCORE50 if (_config.EnableVoice) { - _voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval, config.EnableDebug); + _voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval, _config.VoiceBufferLength, _config.EnableDebug); _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); _voiceWebSocket.Disconnected += async (s, e) => { @@ -643,17 +643,18 @@ namespace Discord } /// Sends a PCM frame to the voice server. - /// PCM frame to send. + /// PCM frame to send. This must be a 48Kz 20ms /// Number of bytes in this frame. - public Task SendVoicePCM(byte[] data, int count) + /// Will block until + public void SendVoicePCM(byte[] data, int count) { CheckReady(); if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client."); - if (count == 0) return TaskHelper.CompletedTask; + if (count == 0) return; if (_isDebugMode) RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output."); - return _voiceWebSocket.SendPCMFrame(data, count); + _voiceWebSocket.SendPCMFrame(data, count); } /// Clears the PCM buffer. diff --git a/src/Discord.Net/DiscordClientConfig.cs b/src/Discord.Net/DiscordClientConfig.cs index 698e6a3bd..6e92d0e7b 100644 --- a/src/Discord.Net/DiscordClientConfig.cs +++ b/src/Discord.Net/DiscordClientConfig.cs @@ -24,6 +24,9 @@ public bool UseMessageQueue { get; set; } = false; /// Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. public int MessageQueueInterval { get; set; } = 100; + /// Gets or sets the max buffer length (in milliseconds) for outgoing voice packets. + /// This value is the target maximum but is not guaranteed. The buffer will often go a bit above this value. + public int VoiceBufferLength { get; set; } = 1000; public DiscordClientConfig() { } } diff --git a/src/Discord.Net/DiscordVoiceSocket.cs b/src/Discord.Net/DiscordVoiceSocket.cs index b439afbb5..8acb58b51 100644 --- a/src/Discord.Net/DiscordVoiceSocket.cs +++ b/src/Discord.Net/DiscordVoiceSocket.cs @@ -19,23 +19,14 @@ namespace Discord { internal sealed partial class DiscordVoiceSocket : DiscordWebSocket { - private struct Packet - { - public byte[] Data; - public int Count; - public Packet(byte[] data, int count) - { - Data = data; - Count = count; - } - } - - private ManualResetEventSlim _connectWaitOnLogin; + private readonly int _targetAudioBufferLength; + private ManualResetEventSlim _connectWaitOnLogin; private uint _ssrc; - private readonly Random _rand = new Random(); + private readonly Random _rand = new Random(); private OpusEncoder _encoder; - private ConcurrentQueue _sendQueue; + private ConcurrentQueue _sendQueue; + private ManualResetEventSlim _sendQueueWait; private UdpClient _udp; private IPEndPoint _endpoint; private bool _isReady, _isClearing; @@ -43,17 +34,21 @@ namespace Discord private string _myIp; private ushort _sequence; private string _mode; + private byte[] _encodingBuffer; #if USE_THREAD private Thread _sendThread; #endif - public DiscordVoiceSocket(DiscordClient client, int timeout, int interval, bool isDebug) + public DiscordVoiceSocket(DiscordClient client, int timeout, int interval, int audioBufferLength, bool isDebug) : base(client, timeout, interval, isDebug) { _connectWaitOnLogin = new ManualResetEventSlim(false); - _sendQueue = new ConcurrentQueue(); + _sendQueue = new ConcurrentQueue(); + _sendQueueWait = new ManualResetEventSlim(true); _encoder = new OpusEncoder(48000, 1, 20, Application.Audio); - } + _encodingBuffer = new byte[4000]; + _targetAudioBufferLength = audioBufferLength / 20; + } protected override void OnConnect() { @@ -81,9 +76,9 @@ namespace Discord #endif return new Task[] { - ReceiveAsync(), + ReceiveVoiceAsync(), #if !USE_THREAD - SendAsync(), + SendVoiceAsync(), #endif WatcherAsync() }.Concat(base.CreateTasks()).ToArray(); @@ -118,7 +113,7 @@ namespace Discord SetConnected(); } - private async Task ReceiveAsync() + private async Task ReceiveVoiceAsync() { var cancelSource = _disconnectToken; var cancelToken = cancelSource.Token; @@ -138,17 +133,17 @@ namespace Discord } #if USE_THREAD - private void SendAsync(CancellationTokenSource cancelSource) + private void SendVoiceAsync(CancellationTokenSource cancelSource) { var cancelToken = cancelSource.Token; #else - private async Task SendAsync() + private async Task SendVoiceAsync() { var cancelSource = _disconnectToken; var cancelToken = cancelSource.Token; await Task.Yield(); - Packet packet; + byte[] packet; try { while (!cancelToken.IsCancellationRequested && !_isReady) @@ -165,7 +160,7 @@ namespace Discord uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; Stopwatch sw = Stopwatch.StartNew(); - byte[] rtpPacket = new byte[4012]; + byte[] rtpPacket = new byte[_encodingBuffer.Length + 12]; rtpPacket[0] = 0x80; //Flags; rtpPacket[1] = 0x78; //Payload Type rtpPacket[8] = (byte)((_ssrc >> 24) & 0xFF); @@ -175,6 +170,10 @@ namespace Discord while (!cancelToken.IsCancellationRequested) { + //If we have less than our target data buffered, request more + if (_sendQueue.Count < _targetAudioBufferLength) + _sendQueueWait.Set(); + double ticksToNextFrame = nextTicks - sw.ElapsedTicks; if (ticksToNextFrame <= 0.0) { @@ -191,11 +190,11 @@ namespace Discord rtpPacket[5] = (byte)((timestamp >> 16) & 0xFF); rtpPacket[6] = (byte)((timestamp >> 8) & 0xFF); rtpPacket[7] = (byte)((timestamp >> 0) & 0xFF); - Buffer.BlockCopy(packet.Data, 0, rtpPacket, 12, packet.Count); + Buffer.BlockCopy(packet, 0, rtpPacket, 12, packet.Length); #if USE_THREAD _udp.Send(rtpPacket, packet.Count + 12); #else - await _udp.SendAsync(rtpPacket, packet.Count + 12); + await _udp.SendAsync(rtpPacket, packet.Length + 12); #endif } timestamp = unchecked(timestamp + samplesPerFrame); @@ -369,31 +368,34 @@ namespace Discord } } - public Task SendPCMFrame(byte[] data, int count) + public void SendPCMFrame(byte[] data, int count) { if (count != _encoder.FrameSize) throw new InvalidOperationException($"Invalid frame size. Got {count}, expected {_encoder.FrameSize}."); byte[] payload; - int encodedLength; lock (_encoder) { - payload = new byte[4000]; - encodedLength = _encoder.EncodeFrame(data, payload); + int encodedLength = _encoder.EncodeFrame(data, _encodingBuffer); if (_mode == "xsalsa20_poly1305") { //TODO: Encode } - } - _sendQueue.Enqueue(new Packet(payload, encodedLength)); - return Task.Delay(0); + payload = new byte[encodedLength]; + Buffer.BlockCopy(_encodingBuffer, 0, payload, 0, encodedLength); + } + + _sendQueueWait.Wait(_disconnectToken.Token); + _sendQueue.Enqueue(payload); + if (_sendQueue.Count >= _targetAudioBufferLength) + _sendQueueWait.Reset(); } public void ClearPCMFrames() { _isClearing = true; - Packet ignored; + byte[] ignored; while (_sendQueue.TryDequeue(out ignored)) { } _isClearing = false; }