diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index a8644f97b..8bcd4b079 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -91,7 +91,7 @@ namespace Discord.API _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } - void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { diff --git a/src/Discord.Net/API/DiscordVoiceAPIClient.cs b/src/Discord.Net/API/DiscordVoiceAPIClient.cs index ecbd47778..2b278d903 100644 --- a/src/Discord.Net/API/DiscordVoiceAPIClient.cs +++ b/src/Discord.Net/API/DiscordVoiceAPIClient.cs @@ -83,7 +83,7 @@ namespace Discord.Audio _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } - void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs index 210cce347..cc357a910 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -14,6 +14,8 @@ namespace Discord.Audio { internal class AudioClient : IAudioClient, IDisposable { + public const int SampleRate = 48000; + public event Func Connected { add { _connectedEvent.Add(value); } @@ -57,7 +59,7 @@ namespace Discord.Audio private DiscordSocketClient Discord => Guild.Discord; /// Creates a new REST/WebSocket discord client. - internal AudioClient(CachedGuild guild, int id) + public AudioClient(CachedGuild guild, int id) { Guild = guild; @@ -171,6 +173,22 @@ namespace Discord.Audio await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } + public void Send(byte[] data, int count) + { + //TODO: Queue these? + ApiClient.SendAsync(data, count).ConfigureAwait(false); + } + + public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) + { + return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); + } + public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int channels = 2, + OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) + { + return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, SampleRate, bitrate, channels, application, bufferSize); + } + private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { #if BENCHMARK @@ -253,7 +271,6 @@ namespace Discord.Audio } #endif } - private async Task ProcessPacketAsync(byte[] packet) { if (!_connectTask.Task.IsCompleted) diff --git a/src/Discord.Net/Audio/IAudioClient.cs b/src/Discord.Net/Audio/IAudioClient.cs index 40a75d4b5..b225312dc 100644 --- a/src/Discord.Net/Audio/IAudioClient.cs +++ b/src/Discord.Net/Audio/IAudioClient.cs @@ -16,5 +16,9 @@ namespace Discord.Audio int Latency { get; } Task DisconnectAsync(); + + RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); + OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int channels = 2, + OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000); } } diff --git a/src/Discord.Net/Audio/Opus/OpusApplication.cs b/src/Discord.Net/Audio/Opus/OpusApplication.cs index cbaa894a5..d6a3ce0cf 100644 --- a/src/Discord.Net/Audio/Opus/OpusApplication.cs +++ b/src/Discord.Net/Audio/Opus/OpusApplication.cs @@ -1,6 +1,6 @@ -namespace Discord.Audio.Opus +namespace Discord.Audio { - internal enum OpusApplication : int + public enum OpusApplication : int { Voice = 2048, MusicOrMixed = 2049, diff --git a/src/Discord.Net/Audio/Opus/OpusConverter.cs b/src/Discord.Net/Audio/Opus/OpusConverter.cs index f430d07f7..732006990 100644 --- a/src/Discord.Net/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net/Audio/Opus/OpusConverter.cs @@ -1,6 +1,6 @@ using System; -namespace Discord.Audio.Opus +namespace Discord.Audio { internal abstract class OpusConverter : IDisposable { @@ -8,17 +8,27 @@ namespace Discord.Audio.Opus /// Gets the bit rate of this converter. public const int BitsPerSample = 16; + /// Gets the bytes per sample. + public const int SampleSize = (BitsPerSample / 8) * MaxChannels; + /// Gets the maximum amount of channels this encoder supports. + public const int MaxChannels = 2; + /// Gets the input sampling rate of this converter. public int SamplingRate { get; } + /// Gets the number of samples per second for this stream. + public int Channels { get; } - protected OpusConverter(int samplingRate) + protected OpusConverter(int samplingRate, int channels) { if (samplingRate != 8000 && samplingRate != 12000 && samplingRate != 16000 && samplingRate != 24000 && samplingRate != 48000) throw new ArgumentOutOfRangeException(nameof(samplingRate)); + if (channels != 1 && channels != 2) + throw new ArgumentOutOfRangeException(nameof(channels)); SamplingRate = samplingRate; + Channels = channels; } private bool disposedValue = false; // To detect redundant calls diff --git a/src/Discord.Net/Audio/Opus/Ctl.cs b/src/Discord.Net/Audio/Opus/OpusCtl.cs similarity index 72% rename from src/Discord.Net/Audio/Opus/Ctl.cs rename to src/Discord.Net/Audio/Opus/OpusCtl.cs index 5023782da..e71213ae6 100644 --- a/src/Discord.Net/Audio/Opus/Ctl.cs +++ b/src/Discord.Net/Audio/Opus/OpusCtl.cs @@ -1,6 +1,6 @@ -namespace Discord.Audio.Opus +namespace Discord.Audio { - internal enum Ctl : int + internal enum OpusCtl : int { SetBitrateRequest = 4002, GetBitrateRequest = 4003, diff --git a/src/Discord.Net/Audio/Opus/OpusDecoder.cs b/src/Discord.Net/Audio/Opus/OpusDecoder.cs index 2df7c2414..e3a3fa649 100644 --- a/src/Discord.Net/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net/Audio/Opus/OpusDecoder.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Discord.Audio.Opus +namespace Discord.Audio { internal unsafe class OpusDecoder : OpusConverter { @@ -10,13 +10,13 @@ namespace Discord.Audio.Opus [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] private static extern void DestroyDecoder(IntPtr decoder); [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] - private static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec); + private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec); - public OpusDecoder(int samplingRate) - : base(samplingRate) + public OpusDecoder(int samplingRate, int channels) + : base(samplingRate, channels) { OpusError error; - _ptr = CreateDecoder(samplingRate, 2, out error); + _ptr = CreateDecoder(samplingRate, channels, out error); if (error != OpusError.OK) throw new InvalidOperationException($"Error occured while creating decoder: {error}"); } @@ -25,11 +25,12 @@ namespace Discord.Audio.Opus /// PCM samples to decode. /// Offset of the frame in input. /// Buffer to store the decoded frame. - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output) + public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) - result = Decode(_ptr, inPtr + inputOffset, inputCount, output, inputCount, 0); + fixed (byte* outPtr = output) + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0); if (result < 0) throw new Exception(((OpusError)result).ToString()); diff --git a/src/Discord.Net/Audio/Opus/OpusEncoder.cs b/src/Discord.Net/Audio/Opus/OpusEncoder.cs index e17487f43..145447194 100644 --- a/src/Discord.Net/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net/Audio/Opus/OpusEncoder.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Discord.Audio.Opus +namespace Discord.Audio { internal unsafe class OpusEncoder : OpusConverter { @@ -10,49 +10,22 @@ namespace Discord.Audio.Opus [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] private static extern void DestroyEncoder(IntPtr encoder); [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] - private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes); + private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] - private static extern int EncoderCtl(IntPtr st, Ctl request, int value); - - /// Gets the bit rate in kbit/s. - public int? BitRate { get; } + private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); + /// Gets the coding mode of the encoder. public OpusApplication Application { get; } - /// Gets the number of channels of this converter. - public int InputChannels { get; } - /// Gets the milliseconds per frame. - public int FrameMilliseconds { get; } - - /// Gets the bytes per sample. - public int SampleSize => (BitsPerSample / 8) * InputChannels; - /// Gets the number of samples per frame. - public int SamplesPerFrame => SamplingRate / 1000 * FrameMilliseconds; - /// Gets the bytes per frame. - public int FrameSize => SamplesPerFrame * SampleSize; - public OpusEncoder(int samplingRate, int channels, int frameMillis, - int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed) - : base(samplingRate) + public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed) + : base(samplingRate, channels) { - if (channels != 1 && channels != 2) - throw new ArgumentOutOfRangeException(nameof(channels)); - if (bitrate != null && (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)) - throw new ArgumentOutOfRangeException(nameof(bitrate)); + Application = application; OpusError error; _ptr = CreateEncoder(samplingRate, channels, (int)application, out error); if (error != OpusError.OK) throw new InvalidOperationException($"Error occured while creating encoder: {error}"); - - - BitRate = bitrate; - Application = application; - InputChannels = channels; - FrameMilliseconds = frameMillis; - - SetForwardErrorCorrection(true); - if (bitrate != null) - SetBitrate(bitrate.Value); } @@ -61,11 +34,12 @@ namespace Discord.Audio.Opus /// Offset of the frame in pcmSamples. /// Buffer to store the encoded frame. /// Length of the frame contained in outputBuffer. - public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) + public unsafe int EncodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) - result = Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); + fixed (byte* outPtr = output) + result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset); if (result < 0) throw new Exception(((OpusError)result).ToString()); @@ -75,7 +49,7 @@ namespace Discord.Audio.Opus /// Gets or sets whether Forward Error Correction is enabled. public void SetForwardErrorCorrection(bool value) { - var result = EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0); + var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0); if (result < 0) throw new Exception(((OpusError)result).ToString()); } @@ -83,7 +57,10 @@ namespace Discord.Audio.Opus /// Gets or sets whether Forward Error Correction is enabled. public void SetBitrate(int value) { - var result = EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000); + if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate) + throw new ArgumentOutOfRangeException(nameof(value)); + + var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000); if (result < 0) throw new Exception(((OpusError)result).ToString()); } diff --git a/src/Discord.Net/Audio/Opus/OpusError.cs b/src/Discord.Net/Audio/Opus/OpusError.cs index 5bfb92d98..d29d8b9dd 100644 --- a/src/Discord.Net/Audio/Opus/OpusError.cs +++ b/src/Discord.Net/Audio/Opus/OpusError.cs @@ -1,4 +1,4 @@ -namespace Discord.Audio.Opus +namespace Discord.Audio { internal enum OpusError : int { diff --git a/src/Discord.Net/Audio/Sodium/SecretBox.cs b/src/Discord.Net/Audio/Sodium/SecretBox.cs index 727db2711..ba4bc2e62 100644 --- a/src/Discord.Net/Audio/Sodium/SecretBox.cs +++ b/src/Discord.Net/Audio/Sodium/SecretBox.cs @@ -1,23 +1,25 @@ using System.Runtime.InteropServices; -namespace Discord.Net.Audio.Sodium +namespace Discord.Audio { public unsafe static class SecretBox { [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] - private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); + private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] - private static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret); + private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); - public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + public static int Encrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) { + fixed (byte* inPtr = input) fixed (byte* outPtr = output) - return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret); + return SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); } - public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret) + public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) { fixed (byte* inPtr = input) - return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret); + fixed (byte* outPtr = output) + return SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); } } } diff --git a/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs new file mode 100644 index 000000000..c059955a8 --- /dev/null +++ b/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs @@ -0,0 +1,30 @@ +namespace Discord.Audio +{ + public class OpusDecodeStream : RTPReadStream + { + private readonly byte[] _buffer; + private readonly OpusDecoder _decoder; + + internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate, + int channels = OpusConverter.MaxChannels, int bufferSize = 4000) + : base(audioClient, secretKey) + { + _buffer = new byte[bufferSize]; + _decoder = new OpusDecoder(samplingRate, channels); + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0); + return base.Read(_buffer, 0, count); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _decoder.Dispose(); + } + } +} diff --git a/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs new file mode 100644 index 000000000..deb44f619 --- /dev/null +++ b/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs @@ -0,0 +1,34 @@ +namespace Discord.Audio +{ + public class OpusEncodeStream : RTPWriteStream + { + private readonly byte[] _buffer; + private readonly OpusEncoder _encoder; + + internal OpusEncodeStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int samplingRate, int? bitrate = null, + int channels = OpusConverter.MaxChannels, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) + : base(audioClient, secretKey, samplesPerFrame, ssrc) + { + _buffer = new byte[bufferSize]; + _encoder = new OpusEncoder(samplingRate, channels); + + _encoder.SetForwardErrorCorrection(true); + if (bitrate != null) + _encoder.SetBitrate(bitrate.Value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + count = _encoder.EncodeFrame(buffer, offset, count, _buffer, 0); + base.Write(_buffer, 0, count); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _encoder.Dispose(); + } + } +} diff --git a/src/Discord.Net/Audio/Streams/RTPReadStream.cs b/src/Discord.Net/Audio/Streams/RTPReadStream.cs new file mode 100644 index 000000000..4bf7f5e1b --- /dev/null +++ b/src/Discord.Net/Audio/Streams/RTPReadStream.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; +using System.IO; + +namespace Discord.Audio +{ + public class RTPReadStream : Stream + { + private readonly BlockingCollection _queuedData; //TODO: Replace with max-length ring buffer + private readonly AudioClient _audioClient; + private readonly byte[] _buffer, _nonce, _secretKey; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + internal RTPReadStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000) + { + _audioClient = audioClient; + _secretKey = secretKey; + _buffer = new byte[bufferSize]; + _queuedData = new BlockingCollection(100); + _nonce = new byte[24]; + } + + public override int Read(byte[] buffer, int offset, int count) + { + var queuedData = _queuedData.Take(); + Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count)); + return queuedData.Length; + } + public override void Write(byte[] buffer, int offset, int count) + { + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); + count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); + var newBuffer = new byte[count]; + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); + _queuedData.Add(newBuffer); + } + + public override void Flush() { throw new NotSupportedException(); } + + public override long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void SetLength(long value) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net/Audio/Streams/RTPWriteStream.cs new file mode 100644 index 000000000..f871ded0d --- /dev/null +++ b/src/Discord.Net/Audio/Streams/RTPWriteStream.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; + +namespace Discord.Audio +{ + public class RTPWriteStream : Stream + { + private readonly AudioClient _audioClient; + private readonly byte[] _buffer, _nonce, _secretKey; + private int _samplesPerFrame; + private uint _ssrc, _timestamp = 0; + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + + + internal RTPWriteStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000) + { + _audioClient = audioClient; + _secretKey = secretKey; + _samplesPerFrame = samplesPerFrame; + _ssrc = ssrc; + _nonce = new byte[24]; + _buffer = new byte[bufferSize]; + _buffer[0] = 0x80; + _buffer[1] = 0x78; + _buffer[8] = (byte)(_ssrc >> 24); + _buffer[9] = (byte)(_ssrc >> 16); + _buffer[10] = (byte)(_ssrc >> 8); + _buffer[11] = (byte)(_ssrc >> 0); + } + + public override void Write(byte[] buffer, int offset, int count) + { + unchecked + { + if (_buffer[3]++ == byte.MaxValue) + _buffer[4]++; + + _timestamp += (uint)_samplesPerFrame; + _buffer[4] = (byte)(_timestamp >> 24); + _buffer[5] = (byte)(_timestamp >> 16); + _buffer[6] = (byte)(_timestamp >> 8); + _buffer[7] = (byte)(_timestamp >> 0); + } + + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); + count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey); + _audioClient.Send(_buffer, count); + } + + public override void Flush() { } + + public override long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + } +}