| @@ -70,5 +70,16 @@ namespace Discord.API.Models | |||||
| public SocketInfo SocketData = new SocketInfo(); | public SocketInfo SocketData = new SocketInfo(); | ||||
| } | } | ||||
| } | } | ||||
| public sealed class IsTalking : WebSocketMessage<IsTalking.Data> | |||||
| { | |||||
| public IsTalking() : base(5) { } | |||||
| public class Data | |||||
| { | |||||
| [JsonProperty(PropertyName = "delay")] | |||||
| public int Delay; | |||||
| [JsonProperty(PropertyName = "speaking")] | |||||
| public bool IsSpeaking; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -27,5 +27,15 @@ namespace Discord.API.Models | |||||
| [JsonProperty(PropertyName = "mode")] | [JsonProperty(PropertyName = "mode")] | ||||
| public string Mode; | public string Mode; | ||||
| } | } | ||||
| public sealed class IsTalking | |||||
| { | |||||
| [JsonProperty(PropertyName = "user_id")] | |||||
| public string UserId; | |||||
| [JsonProperty(PropertyName = "ssrc")] | |||||
| public uint SSRC; | |||||
| [JsonProperty(PropertyName = "speaking")] | |||||
| public bool IsSpeaking; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -287,7 +287,7 @@ namespace Discord | |||||
| user => { } | user => { } | ||||
| ); | ); | ||||
| _webSocket = new DiscordTextWebSocket(_config.ConnectionTimeout, _config.WebSocketInterval); | |||||
| _webSocket = new DiscordTextWebSocket(this, _config.ConnectionTimeout, _config.WebSocketInterval); | |||||
| _webSocket.Connected += (s, e) => RaiseConnected(); | _webSocket.Connected += (s, e) => RaiseConnected(); | ||||
| _webSocket.Disconnected += async (s, e) => | _webSocket.Disconnected += async (s, e) => | ||||
| { | { | ||||
| @@ -312,7 +312,7 @@ namespace Discord | |||||
| }; | }; | ||||
| _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | ||||
| _voiceWebSocket = new DiscordVoiceSocket(_config.VoiceConnectionTimeout, _config.WebSocketInterval); | |||||
| _voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval); | |||||
| _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | ||||
| _voiceWebSocket.Disconnected += (s, e) => | _voiceWebSocket.Disconnected += (s, e) => | ||||
| { | { | ||||
| @@ -1321,9 +1321,12 @@ namespace Discord | |||||
| } | } | ||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| public void SendVoiceWAV(byte[] buffer, int count) | |||||
| /// <summary> Sends a PCM frame to the voice server. </summary> | |||||
| /// <param name="data">PCM frame to send.</param> | |||||
| /// <param name="count">Number of bytes in this frame. </param> | |||||
| public void SendVoicePCM(byte[] data, int count) | |||||
| { | { | ||||
| _voiceWebSocket.SendWAV(buffer, count); | |||||
| _voiceWebSocket.SendPCMFrame(data, count); | |||||
| } | } | ||||
| #endif | #endif | ||||
| @@ -13,8 +13,8 @@ namespace Discord | |||||
| { | { | ||||
| private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | ||||
| public DiscordTextWebSocket(int timeout, int interval) | |||||
| : base(timeout, interval) | |||||
| public DiscordTextWebSocket(DiscordClient client, int timeout, int interval) | |||||
| : base(client, timeout, interval) | |||||
| { | { | ||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | _connectWaitOnLogin = new ManualResetEventSlim(false); | ||||
| _connectWaitOnLogin2 = new ManualResetEventSlim(false); | _connectWaitOnLogin2 = new ManualResetEventSlim(false); | ||||
| @@ -1,16 +1,16 @@ | |||||
| using Discord.API.Models; | using Discord.API.Models; | ||||
| using Discord.Helpers; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | using System.Net; | ||||
| using System.Net.Sockets; | using System.Net.Sockets; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||||
| using System.Text; | using System.Text; | ||||
| using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| using Opus.Net; | using Opus.Net; | ||||
| #endif | #endif | ||||
| @@ -19,50 +19,63 @@ namespace Discord | |||||
| { | { | ||||
| internal sealed partial class DiscordVoiceSocket : DiscordWebSocket | 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 ManualResetEventSlim _connectWaitOnLogin; | ||||
| private UdpClient _udp; | |||||
| private ConcurrentQueue<byte[]> _sendQueue; | |||||
| private string _myIp; | |||||
| private IPEndPoint _endpoint; | |||||
| private byte[] _secretKey; | |||||
| private string _mode; | |||||
| private bool _isFirst; | |||||
| private ushort _sequence; | |||||
| private uint _ssrc; | private uint _ssrc; | ||||
| private long _startTicks; | |||||
| private readonly Random _rand = new Random(); | private readonly Random _rand = new Random(); | ||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| private OpusEncoder _encoder; | private OpusEncoder _encoder; | ||||
| private Queue<Packet> _sendQueue; | |||||
| private UdpClient _udp; | |||||
| private IPEndPoint _endpoint; | |||||
| private bool _isReady; | |||||
| private byte[] _secretKey; | |||||
| private string _myIp; | |||||
| private ushort _sequence; | |||||
| private string _mode; | |||||
| #endif | #endif | ||||
| public DiscordVoiceSocket(int timeout, int interval) | |||||
| : base(timeout, interval) | |||||
| public DiscordVoiceSocket(DiscordClient client, int timeout, int interval) | |||||
| : base(client, timeout, interval) | |||||
| { | { | ||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | _connectWaitOnLogin = new ManualResetEventSlim(false); | ||||
| _sendQueue = new ConcurrentQueue<byte[]>(); | |||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| _encoder = OpusEncoder.Create(24000, 1, Application.Voip); | |||||
| _sendQueue = new Queue<Packet>(); | |||||
| _encoder = new OpusEncoder(48000, 1, 20, Application.Audio); | |||||
| #endif | #endif | ||||
| } | } | ||||
| #if !DNXCORE50 | |||||
| protected override void OnConnect() | protected override void OnConnect() | ||||
| { | { | ||||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | ||||
| _udp.AllowNatTraversal(true); | _udp.AllowNatTraversal(true); | ||||
| _isFirst = true; | |||||
| } | } | ||||
| protected override void OnDisconnect() | protected override void OnDisconnect() | ||||
| { | { | ||||
| _udp = null; | _udp = null; | ||||
| } | } | ||||
| #endif | |||||
| protected override Task[] CreateTasks(CancellationToken cancelToken) | protected override Task[] CreateTasks(CancellationToken cancelToken) | ||||
| { | { | ||||
| return new Task[] | return new Task[] | ||||
| { | { | ||||
| #if !DNXCORE50 | |||||
| Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | ||||
| Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | ||||
| #endif | |||||
| Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | ||||
| }.Concat(base.CreateTasks(cancelToken)).ToArray(); | }.Concat(base.CreateTasks(cancelToken)).ToArray(); | ||||
| } | } | ||||
| @@ -73,8 +86,6 @@ namespace Discord | |||||
| _connectWaitOnLogin.Reset(); | _connectWaitOnLogin.Reset(); | ||||
| _sequence = 0; | |||||
| VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | ||||
| msg.Payload.ServerId = serverId; | msg.Payload.ServerId = serverId; | ||||
| msg.Payload.SessionId = sessionId; | msg.Payload.SessionId = sessionId; | ||||
| @@ -95,10 +106,10 @@ namespace Discord | |||||
| SetConnected(); | SetConnected(); | ||||
| } | } | ||||
| #if !DNXCORE50 | |||||
| private async Task ReceiveAsync() | private async Task ReceiveAsync() | ||||
| { | { | ||||
| var cancelToken = _disconnectToken.Token; | var cancelToken = _disconnectToken.Token; | ||||
| try | try | ||||
| { | { | ||||
| while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
| @@ -115,17 +126,69 @@ namespace Discord | |||||
| var cancelToken = _disconnectToken.Token; | var cancelToken = _disconnectToken.Token; | ||||
| try | try | ||||
| { | { | ||||
| byte[] bytes; | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| while (!cancelToken.IsCancellationRequested && !_isReady) | |||||
| { | { | ||||
| while (_sendQueue.TryDequeue(out bytes)) | |||||
| await _udp.SendAsync(bytes, bytes.Length); | |||||
| lock (_sendQueue) | |||||
| { | |||||
| while (_sendQueue.Count > 0) | |||||
| { | |||||
| var packet = _sendQueue.Dequeue(); | |||||
| _udp.Send(packet.Data, packet.Count); | |||||
| } | |||||
| } | |||||
| await Task.Delay(_sendInterval); | await Task.Delay(_sendInterval); | ||||
| } | } | ||||
| if (cancelToken.IsCancellationRequested) | |||||
| return; | |||||
| uint timestamp = 0; | |||||
| double nextTicks = 0.0; | |||||
| double ticksPerFrame = Stopwatch.Frequency / 1000.0 * _encoder.FrameLength; | |||||
| uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; | |||||
| Stopwatch sw = Stopwatch.StartNew(); | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| { | |||||
| byte[] rtpPacket = new byte[4012]; | |||||
| rtpPacket[0] = 0x80; //Flags; | |||||
| rtpPacket[1] = 0x78; //Payload Type | |||||
| rtpPacket[8] = (byte)((_ssrc >> 24) & 0xFF); | |||||
| rtpPacket[9] = (byte)((_ssrc >> 16) & 0xFF); | |||||
| rtpPacket[10] = (byte)((_ssrc >> 8) & 0xFF); | |||||
| rtpPacket[11] = (byte)((_ssrc >> 0) & 0xFF); | |||||
| if (sw.ElapsedTicks > nextTicks) | |||||
| { | |||||
| lock (_sendQueue) | |||||
| { | |||||
| while (sw.ElapsedTicks > nextTicks) | |||||
| { | |||||
| if (_sendQueue.Count > 0) | |||||
| { | |||||
| var packet = _sendQueue.Dequeue(); | |||||
| ushort sequence = unchecked(_sequence++); | |||||
| rtpPacket[2] = (byte)((sequence >> 8) & 0xFF); | |||||
| rtpPacket[3] = (byte)((sequence >> 0) & 0xFF); | |||||
| rtpPacket[4] = (byte)((timestamp >> 24) & 0xFF); | |||||
| 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); | |||||
| _udp.Send(rtpPacket, packet.Count + 12); | |||||
| } | |||||
| timestamp = unchecked(timestamp + samplesPerFrame); | |||||
| nextTicks += ticksPerFrame; | |||||
| } | |||||
| } | |||||
| } | |||||
| /*else | |||||
| await Task.Delay(1);*/ | |||||
| } | |||||
| } | } | ||||
| catch { } | catch { } | ||||
| finally { _disconnectToken.Cancel(); } | finally { _disconnectToken.Cancel(); } | ||||
| } | } | ||||
| #endif | |||||
| private async Task WatcherAsync() | private async Task WatcherAsync() | ||||
| { | { | ||||
| try | try | ||||
| @@ -133,14 +196,16 @@ namespace Discord | |||||
| await Task.Delay(-1, _disconnectToken.Token); | await Task.Delay(-1, _disconnectToken.Token); | ||||
| } | } | ||||
| catch (TaskCanceledException) { } | catch (TaskCanceledException) { } | ||||
| #if DNXCORE50 | |||||
| finally { _udp.Dispose(); } | |||||
| #else | |||||
| #if !DNXCORE50 | |||||
| finally { _udp.Close(); } | finally { _udp.Close(); } | ||||
| #endif | #endif | ||||
| } | } | ||||
| #if DNXCORE50 | |||||
| protected override Task ProcessMessage(string json) | |||||
| #else | |||||
| protected override async Task ProcessMessage(string json) | protected override async Task ProcessMessage(string json) | ||||
| #endif | |||||
| { | { | ||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | ||||
| switch (msg.Operation) | switch (msg.Operation) | ||||
| @@ -149,18 +214,17 @@ namespace Discord | |||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>(); | var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>(); | ||||
| _heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
| _ssrc = payload.SSRC; | |||||
| #if !DNXCORE50 | |||||
| _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host)).FirstOrDefault(), payload.Port); | _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host)).FirstOrDefault(), payload.Port); | ||||
| //_mode = payload.Modes.LastOrDefault(); | //_mode = payload.Modes.LastOrDefault(); | ||||
| _mode = "plain"; | _mode = "plain"; | ||||
| _udp.Connect(_endpoint); | _udp.Connect(_endpoint); | ||||
| lock(_rand) | |||||
| { | |||||
| _sequence = (ushort)_rand.Next(0, ushort.MaxValue); | |||||
| _startTicks = DateTime.UtcNow.Ticks - _rand.Next(); | |||||
| } | |||||
| _ssrc = payload.SSRC; | |||||
| _sendQueue.Enqueue(new byte[70] { | |||||
| lock (_rand) | |||||
| _sequence = (ushort)_rand.Next(0, ushort.MaxValue); | |||||
| _isReady = false; | |||||
| _sendQueue.Enqueue(new Packet(new byte[70] { | |||||
| (byte)((_ssrc >> 24) & 0xFF), | (byte)((_ssrc >> 24) & 0xFF), | ||||
| (byte)((_ssrc >> 16) & 0xFF), | (byte)((_ssrc >> 16) & 0xFF), | ||||
| (byte)((_ssrc >> 8) & 0xFF), | (byte)((_ssrc >> 8) & 0xFF), | ||||
| @@ -170,30 +234,40 @@ namespace Discord | |||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 | ||||
| }); | |||||
| }, 70)); | |||||
| #else | |||||
| _connectWaitOnLogin.Set(); | |||||
| #endif | |||||
| } | } | ||||
| break; | break; | ||||
| #if !DNXCORE50 | |||||
| case 4: //SESSION_DESCRIPTION | case 4: //SESSION_DESCRIPTION | ||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>(); | var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>(); | ||||
| _secretKey = payload.SecretKey; | _secretKey = payload.SecretKey; | ||||
| SendIsTalking(true); | |||||
| _connectWaitOnLogin.Set(); | _connectWaitOnLogin.Set(); | ||||
| } | } | ||||
| break; | break; | ||||
| #endif | |||||
| default: | default: | ||||
| RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | ||||
| break; | break; | ||||
| } | } | ||||
| #if DNXCORE50 | |||||
| return Task.CompletedTask; | |||||
| #endif | |||||
| } | } | ||||
| #if !DNXCORE50 | |||||
| private void ProcessUdpMessage(UdpReceiveResult msg) | private void ProcessUdpMessage(UdpReceiveResult msg) | ||||
| { | { | ||||
| if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint)) | if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint)) | ||||
| { | { | ||||
| byte[] buffer = msg.Buffer; | byte[] buffer = msg.Buffer; | ||||
| int length = msg.Buffer.Length; | int length = msg.Buffer.Length; | ||||
| if (_isFirst) | |||||
| if (!_isReady) | |||||
| { | { | ||||
| _isFirst = false; | |||||
| _isReady = true; | |||||
| if (length != 70) | if (length != 70) | ||||
| throw new Exception($"Unexpected message length. Expected 70, got {length}."); | throw new Exception($"Unexpected message length. Expected 70, got {length}."); | ||||
| @@ -256,36 +330,29 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| #if !DNXCORE50 | |||||
| public void SendWAV(byte[] buffer, int count) | |||||
| public void SendPCMFrame(byte[] data, int count) | |||||
| { | { | ||||
| int encodedLength; | |||||
| byte[] payload = _encoder.Encode(buffer, count, out encodedLength); | |||||
| if (count != _encoder.FrameSize) | |||||
| throw new InvalidOperationException($"Invalid frame size. Got {count}, expected {_encoder.FrameSize}."); | |||||
| byte[] payload = new byte[4000]; | |||||
| int encodedLength = _encoder.EncodeFrame(data, payload); | |||||
| if (_mode == "xsalsa20_poly1305") | if (_mode == "xsalsa20_poly1305") | ||||
| { | { | ||||
| //TODO: Encode | //TODO: Encode | ||||
| } | } | ||||
| lock (_sendQueue) | |||||
| _sendQueue.Enqueue(new Packet(payload, encodedLength)); | |||||
| } | |||||
| byte[] packet = new byte[12 + encodedLength]; | |||||
| Buffer.BlockCopy(payload, 0, packet, 12, encodedLength); | |||||
| ushort sequence = _sequence++; | |||||
| long timestamp = (DateTime.UtcNow.Ticks - _startTicks) >> 2; //200ns resolution | |||||
| packet[0] = 0x80; //Flags; | |||||
| packet[1] = 0x78; //Payload Type | |||||
| packet[2] = (byte)((sequence >> 8) & 0xFF); | |||||
| packet[3] = (byte)((sequence >> 0) & 0xFF); | |||||
| packet[4] = (byte)((timestamp >> 24) & 0xFF); | |||||
| packet[5] = (byte)((timestamp >> 16) & 0xFF); | |||||
| packet[6] = (byte)((timestamp >> 8) & 0xFF); | |||||
| packet[7] = (byte)((timestamp >> 0) & 0xFF); | |||||
| packet[8] = (byte)((_ssrc >> 24) & 0xFF); | |||||
| packet[9] = (byte)((_ssrc >> 16) & 0xFF); | |||||
| packet[10] = (byte)((_ssrc >> 8) & 0xFF); | |||||
| packet[11] = (byte)((_ssrc >> 0) & 0xFF); | |||||
| _sendQueue.Enqueue(packet); | |||||
| private void SendIsTalking(bool value) | |||||
| { | |||||
| var isTalking = new VoiceWebSocketCommands.IsTalking(); | |||||
| isTalking.Payload.IsSpeaking = value; | |||||
| isTalking.Payload.Delay = 0; | |||||
| QueueMessage(isTalking); | |||||
| } | } | ||||
| #endif | #endif | ||||
| @@ -17,6 +17,7 @@ namespace Discord | |||||
| private const int ReceiveChunkSize = 4096; | private const int ReceiveChunkSize = 4096; | ||||
| private const int SendChunkSize = 4096; | private const int SendChunkSize = 4096; | ||||
| protected readonly DiscordClient _client; | |||||
| protected volatile CancellationTokenSource _disconnectToken; | protected volatile CancellationTokenSource _disconnectToken; | ||||
| protected int _timeout, _heartbeatInterval; | protected int _timeout, _heartbeatInterval; | ||||
| protected readonly int _sendInterval; | protected readonly int _sendInterval; | ||||
| @@ -28,9 +29,10 @@ namespace Discord | |||||
| private DateTime _lastHeartbeat; | private DateTime _lastHeartbeat; | ||||
| private bool _isConnected; | private bool _isConnected; | ||||
| public DiscordWebSocket(int timeout, int interval) | |||||
| public DiscordWebSocket(DiscordClient client, int timeout, int interval) | |||||
| { | { | ||||
| _timeout = timeout; | |||||
| _client = client; | |||||
| _timeout = timeout; | |||||
| _sendInterval = interval; | _sendInterval = interval; | ||||
| _sendQueue = new ConcurrentQueue<byte[]>(); | _sendQueue = new ConcurrentQueue<byte[]>(); | ||||
| @@ -53,9 +53,6 @@ | |||||
| <Compile Include="..\Opus.Net\API.cs"> | <Compile Include="..\Opus.Net\API.cs"> | ||||
| <Link>API.cs</Link> | <Link>API.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Opus.Net\OpusDecoder.cs"> | |||||
| <Link>OpusDecoder.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Opus.Net\OpusEncoder.cs"> | <Compile Include="..\Opus.Net\OpusEncoder.cs"> | ||||
| <Link>OpusEncoder.cs</Link> | <Link>OpusEncoder.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -10,28 +10,28 @@ namespace Opus.Net | |||||
| internal class API | internal class API | ||||
| { | { | ||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out IntPtr error); | |||||
| public static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out Error error); | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern void opus_encoder_destroy(IntPtr encoder); | |||||
| public static extern void opus_encoder_destroy(IntPtr encoder); | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern int opus_encode(IntPtr st, byte[] pcm, int frame_size, IntPtr data, int max_data_bytes); | |||||
| public static extern int opus_encode(IntPtr st, byte[] pcm, int frame_size, IntPtr data, int max_data_bytes); | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||||
| internal static extern IntPtr opus_decoder_create(int Fs, int channels, out IntPtr error); | |||||
| /*[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||||
| public static extern IntPtr opus_decoder_create(int Fs, int channels, out Errors error); | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern void opus_decoder_destroy(IntPtr decoder); | |||||
| public static extern void opus_decoder_destroy(IntPtr decoder); | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern int opus_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec); | |||||
| public static extern int opus_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);*/ | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value); | |||||
| public static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value); | |||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, out int value); | |||||
| public static extern int opus_encoder_ctl(IntPtr st, Ctl request, out int value); | |||||
| } | } | ||||
| public enum Ctl : int | public enum Ctl : int | ||||
| @@ -45,23 +45,26 @@ namespace Opus.Net | |||||
| /// <summary> | /// <summary> | ||||
| /// Supported coding modes. | /// Supported coding modes. | ||||
| /// </summary> | /// </summary> | ||||
| public enum Application | |||||
| public enum Application : int | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Best for most VoIP/videoconference applications where listening quality and intelligibility matter most. | |||||
| /// Gives best quality at a given bitrate for voice signals. It enhances the input signal by high-pass filtering and emphasizing formants and harmonics. | |||||
| /// Optionally it includes in-band forward error correction to protect against packet loss. Use this mode for typical VoIP applications. | |||||
| /// Because of the enhancement, even at high bitrates the output may sound different from the input. | |||||
| /// </summary> | /// </summary> | ||||
| Voip = 2048, | Voip = 2048, | ||||
| /// <summary> | /// <summary> | ||||
| /// Best for broadcast/high-fidelity application where the decoded audio should be as close as possible to input. | |||||
| /// Gives best quality at a given bitrate for most non-voice signals like music. | |||||
| /// Use this mode for music and mixed (music/voice) content, broadcast, and applications requiring less than 15 ms of coding delay. | |||||
| /// </summary> | /// </summary> | ||||
| Audio = 2049, | Audio = 2049, | ||||
| /// <summary> | /// <summary> | ||||
| /// Only use when lowest-achievable latency is what matters most. Voice-optimized modes cannot be used. | |||||
| /// Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. | |||||
| /// </summary> | /// </summary> | ||||
| Restricted_LowLatency = 2051 | Restricted_LowLatency = 2051 | ||||
| } | } | ||||
| public enum Errors | |||||
| public enum Error : int | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// No error. | /// No error. | ||||
| @@ -1,133 +0,0 @@ | |||||
| using System; | |||||
| namespace Opus.Net | |||||
| { | |||||
| /// <summary> | |||||
| /// Opus audio decoder. | |||||
| /// </summary> | |||||
| public class OpusDecoder : IDisposable | |||||
| { | |||||
| /// <summary> | |||||
| /// Creates a new Opus decoder. | |||||
| /// </summary> | |||||
| /// <param name="outputSampleRate">Sample rate to decode at (Hz). This must be one of 8000, 12000, 16000, 24000, or 48000.</param> | |||||
| /// <param name="outputChannels">Number of channels to decode.</param> | |||||
| /// <returns>A new <c>OpusDecoder</c>.</returns> | |||||
| public static OpusDecoder Create(int outputSampleRate, int outputChannels) | |||||
| { | |||||
| if (outputSampleRate != 8000 && | |||||
| outputSampleRate != 12000 && | |||||
| outputSampleRate != 16000 && | |||||
| outputSampleRate != 24000 && | |||||
| outputSampleRate != 48000) | |||||
| throw new ArgumentOutOfRangeException("inputSamplingRate"); | |||||
| if (outputChannels != 1 && outputChannels != 2) | |||||
| throw new ArgumentOutOfRangeException("inputChannels"); | |||||
| IntPtr error; | |||||
| IntPtr decoder = API.opus_decoder_create(outputSampleRate, outputChannels, out error); | |||||
| if ((Errors)error != Errors.OK) | |||||
| { | |||||
| throw new Exception("Exception occured while creating decoder"); | |||||
| } | |||||
| return new OpusDecoder(decoder, outputSampleRate, outputChannels); | |||||
| } | |||||
| private IntPtr _decoder; | |||||
| private OpusDecoder(IntPtr decoder, int outputSamplingRate, int outputChannels) | |||||
| { | |||||
| _decoder = decoder; | |||||
| OutputSamplingRate = outputSamplingRate; | |||||
| OutputChannels = outputChannels; | |||||
| MaxDataBytes = 4000; | |||||
| } | |||||
| /// <summary> | |||||
| /// Produces PCM samples from Opus encoded data. | |||||
| /// </summary> | |||||
| /// <param name="inputOpusData">Opus encoded data to decode, null for dropped packet.</param> | |||||
| /// <param name="dataLength">Length of data to decode.</param> | |||||
| /// <param name="decodedLength">Set to the length of the decoded sample data.</param> | |||||
| /// <returns>PCM audio samples.</returns> | |||||
| public unsafe byte[] Decode(byte[] inputOpusData, int dataLength, out int decodedLength) | |||||
| { | |||||
| if (disposed) | |||||
| throw new ObjectDisposedException("OpusDecoder"); | |||||
| IntPtr decodedPtr; | |||||
| byte[] decoded = new byte[MaxDataBytes]; | |||||
| int frameCount = FrameCount(MaxDataBytes); | |||||
| int length = 0; | |||||
| fixed (byte* bdec = decoded) | |||||
| { | |||||
| decodedPtr = new IntPtr((void*)bdec); | |||||
| if (inputOpusData != null) | |||||
| length = API.opus_decode(_decoder, inputOpusData, dataLength, decodedPtr, frameCount, 0); | |||||
| else | |||||
| length = API.opus_decode(_decoder, null, 0, decodedPtr, frameCount, (ForwardErrorCorrection) ? 1 : 0); | |||||
| } | |||||
| decodedLength = length * 2; | |||||
| if (length < 0) | |||||
| throw new Exception("Decoding failed - " + ((Errors)length).ToString()); | |||||
| return decoded; | |||||
| } | |||||
| /// <summary> | |||||
| /// Determines the number of frames that can fit into a buffer of the given size. | |||||
| /// </summary> | |||||
| /// <param name="bufferSize"></param> | |||||
| /// <returns></returns> | |||||
| public int FrameCount(int bufferSize) | |||||
| { | |||||
| // seems like bitrate should be required | |||||
| int bitrate = 16; | |||||
| int bytesPerSample = (bitrate / 8) * OutputChannels; | |||||
| return bufferSize / bytesPerSample; | |||||
| } | |||||
| /// <summary> | |||||
| /// Gets the output sampling rate of the decoder. | |||||
| /// </summary> | |||||
| public int OutputSamplingRate { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the number of channels of the decoder. | |||||
| /// </summary> | |||||
| public int OutputChannels { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the size of memory allocated for decoding data. | |||||
| /// </summary> | |||||
| public int MaxDataBytes { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether forward error correction is enabled or not. | |||||
| /// </summary> | |||||
| public bool ForwardErrorCorrection { get; set; } | |||||
| ~OpusDecoder() | |||||
| { | |||||
| Dispose(); | |||||
| } | |||||
| private bool disposed; | |||||
| public void Dispose() | |||||
| { | |||||
| if (disposed) | |||||
| return; | |||||
| GC.SuppressFinalize(this); | |||||
| if (_decoder != IntPtr.Zero) | |||||
| { | |||||
| API.opus_decoder_destroy(_decoder); | |||||
| _decoder = IntPtr.Zero; | |||||
| } | |||||
| disposed = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -2,180 +2,90 @@ | |||||
| namespace Opus.Net | namespace Opus.Net | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Opus codec wrapper. | |||||
| /// </summary> | |||||
| /// <summary> Opus codec wrapper. </summary> | |||||
| public class OpusEncoder : IDisposable | public class OpusEncoder : IDisposable | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Creates a new Opus encoder. | |||||
| /// </summary> | |||||
| /// <param name="inputSamplingRate">Sampling rate of the input signal (Hz). This must be one of 8000, 12000, 16000, 24000, or 48000.</param> | |||||
| /// <param name="inputChannels">Number of channels (1 or 2) in input signal.</param> | |||||
| private readonly IntPtr _encoder; | |||||
| /// <summary> Gets the bit rate of the encoder. </summary> | |||||
| public const int BitRate = 16; | |||||
| /// <summary> Gets the input sampling rate of the encoder. </summary> | |||||
| public int InputSamplingRate { get; private set; } | |||||
| /// <summary> Gets the number of channels of the encoder. </summary> | |||||
| public int InputChannels { get; private set; } | |||||
| /// <summary> Gets the milliseconds per frame. </summary> | |||||
| public int FrameLength { get; private set; } | |||||
| /// <summary> Gets the number of samples per frame. </summary> | |||||
| public int SamplesPerFrame { get; private set; } | |||||
| /// <summary> Gets the bytes per sample. </summary> | |||||
| public int SampleSize { get; private set; } | |||||
| /// <summary> Gets the bytes per frame. </summary> | |||||
| public int FrameSize { get; private set; } | |||||
| /// <summary> Gets the coding mode of the encoder. </summary> | |||||
| public Application Application { get; private set; } | |||||
| /// <summary> Creates a new Opus encoder. </summary> | |||||
| /// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param> | |||||
| /// <param name="channels">Number of channels (1 or 2) in input signal.</param> | |||||
| /// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param> | |||||
| /// <param name="application">Coding mode.</param> | /// <param name="application">Coding mode.</param> | ||||
| /// <returns>A new <c>OpusEncoder</c></returns> | /// <returns>A new <c>OpusEncoder</c></returns> | ||||
| public static OpusEncoder Create(int inputSamplingRate, int inputChannels, Application application) | |||||
| public OpusEncoder(int samplingRate, int channels, int frameLength, Application application) | |||||
| { | { | ||||
| if (inputSamplingRate != 8000 && | |||||
| inputSamplingRate != 12000 && | |||||
| inputSamplingRate != 16000 && | |||||
| inputSamplingRate != 24000 && | |||||
| inputSamplingRate != 48000) | |||||
| if (samplingRate != 8000 && samplingRate != 12000 && | |||||
| samplingRate != 16000 && samplingRate != 24000 && | |||||
| samplingRate != 48000) | |||||
| throw new ArgumentOutOfRangeException("inputSamplingRate"); | throw new ArgumentOutOfRangeException("inputSamplingRate"); | ||||
| if (inputChannels != 1 && inputChannels != 2) | |||||
| if (channels != 1 && channels != 2) | |||||
| throw new ArgumentOutOfRangeException("inputChannels"); | throw new ArgumentOutOfRangeException("inputChannels"); | ||||
| IntPtr error; | |||||
| IntPtr encoder = API.opus_encoder_create(inputSamplingRate, inputChannels, (int)application, out error); | |||||
| if ((Errors)error != Errors.OK) | |||||
| { | |||||
| throw new Exception("Exception occured while creating encoder"); | |||||
| } | |||||
| return new OpusEncoder(encoder, inputSamplingRate, inputChannels, application); | |||||
| } | |||||
| InputSamplingRate = samplingRate; | |||||
| InputChannels = channels; | |||||
| Application = application; | |||||
| FrameLength = frameLength; | |||||
| SampleSize = (BitRate / 8) * channels; | |||||
| SamplesPerFrame = samplingRate / 1000 * FrameLength; | |||||
| FrameSize = SamplesPerFrame * SampleSize; | |||||
| private IntPtr _encoder; | |||||
| Error error; | |||||
| _encoder = API.opus_encoder_create(samplingRate, channels, (int)application, out error); | |||||
| if (error != Error.OK) | |||||
| throw new InvalidOperationException("Error occured while creating encoder: " + error.ToString()); | |||||
| private OpusEncoder(IntPtr encoder, int inputSamplingRate, int inputChannels, Application application) | |||||
| { | |||||
| _encoder = encoder; | |||||
| InputSamplingRate = inputSamplingRate; | |||||
| InputChannels = inputChannels; | |||||
| Application = application; | |||||
| MaxDataBytes = 4000; | |||||
| SetForwardErrorCorrection(true); | |||||
| } | } | ||||
| /// <summary> | |||||
| /// Produces Opus encoded audio from PCM samples. | |||||
| /// </summary> | |||||
| /// <param name="inputPcmSamples">PCM samples to encode.</param> | |||||
| /// <param name="sampleLength">How many bytes to encode.</param> | |||||
| /// <param name="encodedLength">Set to length of encoded audio.</param> | |||||
| /// <summary> Produces Opus encoded audio from PCM samples. </summary> | |||||
| /// <param name="pcmSamples">PCM samples to encode.</param> | |||||
| /// <param name="encodedLength">Length of encoded audio.</param> | |||||
| /// <returns>Opus encoded audio buffer.</returns> | /// <returns>Opus encoded audio buffer.</returns> | ||||
| public unsafe byte[] Encode(byte[] inputPcmSamples, int sampleLength, out int encodedLength) | |||||
| public unsafe int EncodeFrame(byte[] pcmSamples, byte[] outputBuffer) | |||||
| { | { | ||||
| if (disposed) | if (disposed) | ||||
| throw new ObjectDisposedException("OpusEncoder"); | throw new ObjectDisposedException("OpusEncoder"); | ||||
| int frames = FrameCount(inputPcmSamples); | |||||
| IntPtr encodedPtr; | IntPtr encodedPtr; | ||||
| byte[] encoded = new byte[MaxDataBytes]; | |||||
| int length = 0; | int length = 0; | ||||
| fixed (byte* benc = encoded) | |||||
| fixed (byte* bPtr = outputBuffer) | |||||
| { | { | ||||
| encodedPtr = new IntPtr((void*)benc); | |||||
| length = API.opus_encode(_encoder, inputPcmSamples, frames, encodedPtr, sampleLength); | |||||
| encodedPtr = new IntPtr((void*)bPtr); | |||||
| length = API.opus_encode(_encoder, pcmSamples, SamplesPerFrame, encodedPtr, outputBuffer.Length); | |||||
| } | } | ||||
| encodedLength = length; | |||||
| if (length < 0) | |||||
| throw new Exception("Encoding failed - " + ((Errors)length).ToString()); | |||||
| return encoded; | |||||
| } | |||||
| /// <summary> | |||||
| /// Determines the number of frames in the PCM samples. | |||||
| /// </summary> | |||||
| /// <param name="pcmSamples"></param> | |||||
| /// <returns></returns> | |||||
| public int FrameCount(byte[] pcmSamples) | |||||
| { | |||||
| // seems like bitrate should be required | |||||
| int bitrate = 16; | |||||
| int bytesPerSample = (bitrate / 8) * InputChannels; | |||||
| return pcmSamples.Length / bytesPerSample; | |||||
| } | |||||
| /// <summary> | |||||
| /// Helper method to determine how many bytes are required for encoding to work. | |||||
| /// </summary> | |||||
| /// <param name="frameCount">Target frame size.</param> | |||||
| /// <returns></returns> | |||||
| public int FrameByteCount(int frameCount) | |||||
| { | |||||
| int bitrate = 16; | |||||
| int bytesPerSample = (bitrate / 8) * InputChannels; | |||||
| return frameCount * bytesPerSample; | |||||
| } | |||||
| /// <summary> | |||||
| /// Gets the input sampling rate of the encoder. | |||||
| /// </summary> | |||||
| public int InputSamplingRate { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the number of channels of the encoder. | |||||
| /// </summary> | |||||
| public int InputChannels { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the coding mode of the encoder. | |||||
| /// </summary> | |||||
| public Application Application { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the size of memory allocated for reading encoded data. | |||||
| /// 4000 is recommended. | |||||
| /// </summary> | |||||
| public int MaxDataBytes { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the bitrate setting of the encoding. | |||||
| /// </summary> | |||||
| public int Bitrate | |||||
| { | |||||
| get | |||||
| { | |||||
| if (disposed) | |||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| int bitrate; | |||||
| var ret = API.opus_encoder_ctl(_encoder, Ctl.GetBitrateRequest, out bitrate); | |||||
| if (ret < 0) | |||||
| throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||||
| return bitrate; | |||||
| } | |||||
| set | |||||
| { | |||||
| if (disposed) | |||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| var ret = API.opus_encoder_ctl(_encoder, Ctl.SetBitrateRequest, value); | |||||
| if (ret < 0) | |||||
| throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||||
| } | |||||
| if (length < 0) | |||||
| throw new Exception("Encoding failed: " + ((Error)length).ToString()); | |||||
| return length; | |||||
| } | } | ||||
| /// <summary> | |||||
| /// Gets or sets whether Forward Error Correction is enabled. | |||||
| /// </summary> | |||||
| public bool ForwardErrorCorrection | |||||
| /// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | |||||
| public void SetForwardErrorCorrection(bool value) | |||||
| { | { | ||||
| get | |||||
| { | |||||
| if (_encoder == IntPtr.Zero) | |||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| int fec; | |||||
| int ret = API.opus_encoder_ctl(_encoder, Ctl.GetInbandFECRequest, out fec); | |||||
| if (ret < 0) | |||||
| throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||||
| return fec > 0; | |||||
| } | |||||
| set | |||||
| { | |||||
| if (_encoder == IntPtr.Zero) | |||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| var ret = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0); | |||||
| if (ret < 0) | |||||
| throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||||
| } | |||||
| } | |||||
| if (_encoder == IntPtr.Zero) | |||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| ~OpusEncoder() | |||||
| { | |||||
| Dispose(); | |||||
| var ret = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0); | |||||
| if (ret < 0) | |||||
| throw new Exception("Encoder error - " + ((Error)ret).ToString()); | |||||
| } | } | ||||
| private bool disposed; | private bool disposed; | ||||
| @@ -187,12 +97,13 @@ namespace Opus.Net | |||||
| GC.SuppressFinalize(this); | GC.SuppressFinalize(this); | ||||
| if (_encoder != IntPtr.Zero) | if (_encoder != IntPtr.Zero) | ||||
| { | |||||
| API.opus_encoder_destroy(_encoder); | API.opus_encoder_destroy(_encoder); | ||||
| _encoder = IntPtr.Zero; | |||||
| } | |||||
| disposed = true; | disposed = true; | ||||
| } | } | ||||
| ~OpusEncoder() | |||||
| { | |||||
| Dispose(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||