private async Task HandleDispatch(JObject jo)
        {
            var opc = (int)jo["op"];
            var opp = jo["d"] as JObject;

            switch (opc)
            {
            case 2:
                Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", "OP2 received", DateTime.Now);
                var vrp = opp.ToObject <VoiceReadyPayload>();
                SSRC = vrp.SSRC;
                ConnectionEndpoint = new ConnectionEndpoint {
                    Hostname = ConnectionEndpoint.Hostname, Port = vrp.Port
                };
                HeartbeatInterval = vrp.HeartbeatInterval;
                HeartbeatTask     = Task.Run(Heartbeat);
                await Stage1().ConfigureAwait(false);

                break;

            case 4:
                Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", "OP4 received", DateTime.Now);
                var vsd = opp.ToObject <VoiceSessionDescriptionPayload>();
                Key = vsd.SecretKey;
                await Stage2().ConfigureAwait(false);

                break;

            case 5:
                // Don't spam OP5
                //this.Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", "OP5 received", DateTime.Now);
                var spd = opp.ToObject <VoiceSpeakingPayload>();
                var spk = new UserSpeakingEventArgs(Discord)
                {
                    Speaking = spd.Speaking,
                    SSRC     = spd.SSRC.Value,
                    User     = Discord.InternalGetCachedUser(spd.UserId.Value)
                };
                if (!SSRCMap.ContainsKey(spk.SSRC))
                {
                    SSRCMap.AddOrUpdate(spk.SSRC, spk.User.Id, (k, v) => spk.User.Id);
                }
                await _userSpeaking.InvokeAsync(spk).ConfigureAwait(false);

                break;

            case 6:
                var dt   = DateTime.Now;
                var ping = (int)(dt - LastHeartbeat).TotalMilliseconds;
                Volatile.Write(ref _ping, ping);
                Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", $"Received voice heartbeat ACK, ping {ping.ToString("#,##0", CultureInfo.InvariantCulture)}ms", dt);
                LastHeartbeat = dt;
                break;

            case 8:
                // this sends a heartbeat interval that appears to be consistent with regular GW hello
                // however opcodes don't match (8 != 10)
                // so we suppress it so that users are not alerted
                // HELLO
                break;

            case 9:
                Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", "OP9 received", DateTime.Now);
                HeartbeatTask = Task.Run(Heartbeat);
                break;

            case 13:
                var ulpd = opp.ToObject <VoiceUserLeavePayload>();
                var usr  = await Discord.GetUserAsync(ulpd.UserId).ConfigureAwait(false);

                var ssrc = SSRCMap.FirstOrDefault(x => x.Value == ulpd.UserId);
                if (ssrc.Value != 0)
                {
                    SSRCMap.TryRemove(ssrc.Key, out _);
                }
                Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", $"User '{usr.Username}#{usr.Discriminator}' ({ulpd.UserId.ToString(CultureInfo.InvariantCulture)}) left voice chat in '{Channel.Guild.Name}' ({Channel.Guild.Id.ToString(CultureInfo.InvariantCulture)})", DateTime.Now);
                await _userLeft.InvokeAsync(new VoiceUserLeaveEventArgs(Discord) { User = usr }).ConfigureAwait(false);

                break;

            default:
                Discord.DebugLogger.LogMessage(LogLevel.Warning, "VoiceNext", $"Unknown opcode received: {opc.ToString(CultureInfo.InvariantCulture)}", DateTime.Now);
                break;
            }
        }
        private async Task VoiceReceiverTask()
        {
            var token  = ReceiverToken;
            var client = UdpClient;

            while (!token.IsCancellationRequested)
            {
                if (client.DataAvailable <= 0)
                {
                    continue;
                }

                byte[] data = null, header = null;
                ushort seq = 0;
                uint   ts = 0, ssrc = 0;
                try
                {
                    data = await client.ReceiveAsync().ConfigureAwait(false);

                    header = new byte[RtpCodec.SIZE_HEADER];
                    data   = Rtp.Decode(data, header);

                    var nonce = Rtp.MakeNonce(header);
                    data = Sodium.Decode(data, nonce, Key);

                    // following is thanks to code from Eris
                    // https://github.com/abalabahaha/eris/blob/master/lib/voice/VoiceConnection.js#L623
                    var doff = 0;
                    Rtp.Decode(header, out seq, out ts, out ssrc, out var has_ext);
                    if (has_ext)
                    {
                        if (data[0] == 0xBE && data[1] == 0xDE)
                        {
                            // RFC 5285, 4.2 One-Byte header
                            // http://www.rfcreader.com/#rfc5285_line186

                            var hlen = data[2] << 8 | data[3];
                            var i    = 4;
                            for (; i < hlen + 4; i++)
                            {
                                var b = data[i];
                                // This is unused(?)
                                //var id = (b >> 4) & 0x0F;
                                var len = (b & 0x0F) + 1;
                                i += len;
                            }
                            while (data[i] == 0)
                            {
                                i++;
                            }
                            doff = i;
                        }
                        // TODO: consider implementing RFC 5285, 4.3. Two-Byte Header
                    }

                    data = Opus.Decode(data, doff, data.Length - doff);
                }
                catch { continue; }

                // TODO: wait for ssrc map?
                DiscordUser user = null;
                if (SSRCMap.ContainsKey(ssrc))
                {
                    var id = SSRCMap[ssrc];
                    if (Guild != null)
                    {
                        user = Guild._members.FirstOrDefault(xm => xm.Id == id) ?? await Guild.GetMemberAsync(id).ConfigureAwait(false);
                    }

                    if (user == null)
                    {
                        user = Discord.InternalGetCachedUser(id);
                    }

                    if (user == null)
                    {
                        user = new DiscordUser {
                            Discord = Discord, Id = id
                        }
                    }
                    ;
                }

                await _voiceReceived.InvokeAsync(new VoiceReceiveEventArgs(Discord)
                {
                    SSRC        = ssrc,
                    Voice       = new ReadOnlyCollection <byte>(data),
                    VoiceLength = 20,
                    User        = user
                }).ConfigureAwait(false);
            }
        }