forked from EllieBotDevs/elliebot
207 lines
No EOL
6.4 KiB
C#
207 lines
No EOL
6.4 KiB
C#
using System;
|
|
using System.Buffers;
|
|
|
|
namespace Ayu.Discord.Voice
|
|
{
|
|
public sealed class VoiceClient : IDisposable
|
|
{
|
|
delegate int EncodeDelegate(Span<byte> input, byte[] output);
|
|
|
|
private readonly int sampleRate;
|
|
private readonly int bitRate;
|
|
private readonly int channels;
|
|
private readonly int frameDelay;
|
|
private readonly int bitDepth;
|
|
|
|
public LibOpusEncoder Encoder { get; }
|
|
private readonly ArrayPool<byte> _arrayPool;
|
|
|
|
public int BitDepth => bitDepth * 8;
|
|
public int Delay => frameDelay;
|
|
|
|
private int FrameSizePerChannel => Encoder.FrameSizePerChannel;
|
|
public int InputLength => FrameSizePerChannel * channels * bitDepth;
|
|
|
|
EncodeDelegate Encode;
|
|
|
|
// https://github.com/xiph/opus/issues/42 w
|
|
public VoiceClient(SampleRate sampleRate = SampleRate._48k,
|
|
Bitrate bitRate = Bitrate._192k,
|
|
Channels channels = Channels.Two,
|
|
FrameDelay frameDelay = FrameDelay.Delay20,
|
|
BitDepthEnum bitDepthEnum = BitDepthEnum.Float32)
|
|
{
|
|
this.frameDelay = (int)frameDelay;
|
|
this.sampleRate = (int)sampleRate;
|
|
this.bitRate = (int)bitRate;
|
|
this.channels = (int)channels;
|
|
this.bitDepth = (int)bitDepthEnum;
|
|
|
|
this.Encoder = new(this.sampleRate, this.channels, this.bitRate, this.frameDelay);
|
|
|
|
Encode = bitDepthEnum switch
|
|
{
|
|
BitDepthEnum.Float32 => Encoder.EncodeFloat,
|
|
BitDepthEnum.UInt16 => Encoder.Encode,
|
|
_ => throw new NotSupportedException(nameof(BitDepth))
|
|
};
|
|
|
|
if (bitDepthEnum == BitDepthEnum.Float32)
|
|
{
|
|
Encode = Encoder.EncodeFloat;
|
|
}
|
|
else
|
|
{
|
|
Encode = Encoder.Encode;
|
|
}
|
|
|
|
_arrayPool = ArrayPool<byte>.Shared;
|
|
}
|
|
|
|
public int SendPcmFrame(VoiceGateway gw, Span<byte> data, int offset, int count)
|
|
{
|
|
var secretKey = gw.SecretKey;
|
|
if (secretKey.Length == 0)
|
|
{
|
|
return (int)SendPcmError.SecretKeyUnavailable;
|
|
}
|
|
|
|
// encode using opus
|
|
var encodeOutput = _arrayPool.Rent(LibOpusEncoder.MaxData);
|
|
try
|
|
{
|
|
var encodeOutputLength = Encode(data, encodeOutput);
|
|
return SendOpusFrame(gw, encodeOutput, 0, encodeOutputLength);
|
|
}
|
|
finally
|
|
{
|
|
_arrayPool.Return(encodeOutput);
|
|
}
|
|
}
|
|
|
|
public int SendOpusFrame(VoiceGateway gw, byte[] data, int offset, int count)
|
|
{
|
|
var secretKey = gw.SecretKey;
|
|
if (secretKey is null)
|
|
{
|
|
return (int)SendPcmError.SecretKeyUnavailable;
|
|
}
|
|
|
|
// form RTP header
|
|
var headerLength = 1 // version + flags
|
|
+ 1 // payload type
|
|
+ 2 // sequence
|
|
+ 4 // timestamp
|
|
+ 4; // ssrc
|
|
|
|
var header = new byte[headerLength];
|
|
|
|
header[0] = 0x80; // version + flags
|
|
header[1] = 0x78; // payload type
|
|
|
|
// get byte values for header data
|
|
var seqBytes = BitConverter.GetBytes(gw.Sequence); // 2
|
|
var nonceBytes = BitConverter.GetBytes(gw.NonceSequence); // 2
|
|
var timestampBytes = BitConverter.GetBytes(gw.Timestamp); // 4
|
|
var ssrcBytes = BitConverter.GetBytes(gw.Ssrc); // 4
|
|
|
|
gw.Timestamp += (uint)FrameSizePerChannel;
|
|
gw.Sequence++;
|
|
gw.NonceSequence++;
|
|
|
|
if (BitConverter.IsLittleEndian)
|
|
{
|
|
Array.Reverse(seqBytes);
|
|
Array.Reverse(nonceBytes);
|
|
Array.Reverse(timestampBytes);
|
|
Array.Reverse(ssrcBytes);
|
|
}
|
|
|
|
// copy headers
|
|
Buffer.BlockCopy(seqBytes, 0, header, 2, 2);
|
|
Buffer.BlockCopy(timestampBytes, 0, header, 4, 4);
|
|
Buffer.BlockCopy(ssrcBytes, 0, header, 8, 4);
|
|
|
|
//// encryption part
|
|
//// create a byte array where to store the encrypted data
|
|
//// it has to be inputLength + crypto_secretbox_MACBYTES (constant with value 16)
|
|
var encryptedBytes = new byte[count + 16];
|
|
|
|
//// form nonce with header + 12 empty bytes
|
|
//var nonce = new byte[24];
|
|
//Buffer.BlockCopy(rtpHeader, 0, nonce, 0, rtpHeader.Length);
|
|
|
|
var nonce = new byte[4];
|
|
Buffer.BlockCopy(seqBytes, 0, nonce, 2, 2);
|
|
|
|
Sodium.Encrypt(data, 0, count, encryptedBytes, 0, nonce, secretKey);
|
|
|
|
var rtpDataLength = headerLength + encryptedBytes.Length + nonce.Length;
|
|
var rtpData = _arrayPool.Rent(rtpDataLength);
|
|
try
|
|
{
|
|
//copy headers
|
|
Buffer.BlockCopy(header, 0, rtpData, 0, header.Length);
|
|
//copy audio data
|
|
Buffer.BlockCopy(encryptedBytes, 0, rtpData, header.Length, encryptedBytes.Length);
|
|
Buffer.BlockCopy(nonce, 0, rtpData, rtpDataLength - 4, 4);
|
|
|
|
gw.SendRtpData(rtpData, rtpDataLength);
|
|
// FUTURE When there's a break in the sent data,
|
|
// the packet transmission shouldn't simply stop.
|
|
// Instead, send five frames of silence (0xF8, 0xFF, 0xFE)
|
|
// before stopping to avoid unintended Opus interpolation
|
|
// with subsequent transmissions.
|
|
|
|
return rtpDataLength;
|
|
}
|
|
finally
|
|
{
|
|
_arrayPool.Return(rtpData);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
=> Encoder.Dispose();
|
|
}
|
|
|
|
public enum SendPcmError
|
|
{
|
|
SecretKeyUnavailable = -1,
|
|
}
|
|
|
|
|
|
public enum FrameDelay
|
|
{
|
|
Delay5 = 5,
|
|
Delay10 = 10,
|
|
Delay20 = 20,
|
|
Delay40 = 40,
|
|
Delay60 = 60,
|
|
}
|
|
|
|
public enum BitDepthEnum
|
|
{
|
|
UInt16 = sizeof(UInt16),
|
|
Float32 = sizeof(float),
|
|
}
|
|
|
|
public enum SampleRate
|
|
{
|
|
_48k = 48_000,
|
|
}
|
|
|
|
public enum Bitrate
|
|
{
|
|
_64k = 64 * 1024,
|
|
_96k = 96 * 1024,
|
|
_128k = 128 * 1024,
|
|
_192k = 192 * 1024,
|
|
}
|
|
|
|
public enum Channels
|
|
{
|
|
One = 1,
|
|
Two = 2,
|
|
}
|
|
} |