/// <summary>
        /// Encodes, encrypts, and sends the provided PCM data to the connected voice channel.
        /// </summary>
        /// <param name="pcmData">PCM data to encode, encrypt, and send.</param>
        /// <param name="blockSize">Millisecond length of the PCM data.</param>
        /// <param name="bitRate">Bitrate of the PCM data.</param>
        /// <returns>Task representing the sending operation.</returns>
        public async Task SendAsync(byte[] pcmData, int blockSize, int bitRate = 16)
        {
            if (!IsInitialized)
            {
                throw new InvalidOperationException("The connection is not initialized");
            }

            await PlaybackSemaphore.WaitAsync().ConfigureAwait(false);

            var rtp = Rtp.Encode(Sequence, Timestamp, SSRC);

            var dat = Opus.Encode(pcmData, 0, pcmData.Length, bitRate);

            dat = Sodium.Encode(dat, Rtp.MakeNonce(rtp), Key);
            dat = Rtp.Encode(rtp, dat);

            await SendSpeakingAsync(true).ConfigureAwait(false);

            // works:
            //await UdpClient.SendAsync(dat, dat.Length).ConfigureAwait(false);
            await UdpClient.SendNativelyAsync(dat, dat.Length).ConfigureAwait(false);

            Sequence++;
            Timestamp += 48 * (uint)blockSize;

            PlaybackSemaphore.Release();
        }
        /// <summary>
        /// Encodes, encrypts, and sends the provided PCM data to the connected voice channel.
        /// </summary>
        /// <param name="pcmData">PCM data to encode, encrypt, and send.</param>
        /// <param name="blockSize">Millisecond length of the PCM data.</param>
        /// <param name="bitRate">Bitrate of the PCM data.</param>
        /// <returns>Task representing the sending operation.</returns>
        public async Task SendAsync(byte[] pcmData, int blockSize, int bitRate = 16)
        {
            if (!IsInitialized)
            {
                throw new InvalidOperationException("The connection is not initialized");
            }

            await PlaybackSemaphore.WaitAsync().ConfigureAwait(false);

            var rtp = Rtp.Encode(Sequence, Timestamp, SSRC);

            var dat = Opus.Encode(pcmData, 0, pcmData.Length, bitRate);

            dat = Sodium.Encode(dat, Rtp.MakeNonce(rtp), Key);
            dat = Rtp.Encode(rtp, dat);

            if (SynchronizerTicks == 0)
            {
                SynchronizerTicks      = Stopwatch.GetTimestamp();
                SynchronizerResolution = (Stopwatch.Frequency * 0.02);
                TickResolution         = 10_000_000.0 / Stopwatch.Frequency;
                Discord.DebugLogger.LogMessage(LogLevel.Debug, "VoiceNext", $"Timer accuracy: {Stopwatch.Frequency.ToString("#,##0", CultureInfo.InvariantCulture)}/{SynchronizerResolution.ToString(CultureInfo.InvariantCulture)} (high resolution? {Stopwatch.IsHighResolution})", DateTime.Now);
            }
            else
            {
                // Provided by Laura#0090 (214796473689178133); this is Python, but adaptable:
                //
                // delay = max(0, self.delay + ((start_time + self.delay * loops) + - time.time()))
                //
                // self.delay
                //   sample size
                // start_time
                //   time since streaming started
                // loops
                //   number of samples sent
                // time.time()
                //   DateTime.Now

                var cts = Math.Max(Stopwatch.GetTimestamp() - SynchronizerTicks, 0);
                if (cts < SynchronizerResolution)
                {
                    await Task.Delay(TimeSpan.FromTicks((long)((SynchronizerResolution - cts) * TickResolution))).ConfigureAwait(false);
                }

                SynchronizerTicks += SynchronizerResolution;
            }

            await SendSpeakingAsync(true).ConfigureAwait(false);

            await UdpClient.SendAsync(dat, dat.Length).ConfigureAwait(false);

            Sequence++;
            Timestamp += 48 * (uint)blockSize;

            PlaybackSemaphore.Release();
        }
        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);
            }
        }