Added RTP Read/Write and Opus Encode/Decode streams
This commit is contained in:
@@ -91,7 +91,7 @@ namespace Discord.API
|
|||||||
|
|
||||||
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
||||||
}
|
}
|
||||||
void Dispose(bool disposing)
|
private void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!_isDisposed)
|
if (!_isDisposed)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ namespace Discord.Audio
|
|||||||
|
|
||||||
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
||||||
}
|
}
|
||||||
void Dispose(bool disposing)
|
private void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!_isDisposed)
|
if (!_isDisposed)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace Discord.Audio
|
|||||||
{
|
{
|
||||||
internal class AudioClient : IAudioClient, IDisposable
|
internal class AudioClient : IAudioClient, IDisposable
|
||||||
{
|
{
|
||||||
|
public const int SampleRate = 48000;
|
||||||
|
|
||||||
public event Func<Task> Connected
|
public event Func<Task> Connected
|
||||||
{
|
{
|
||||||
add { _connectedEvent.Add(value); }
|
add { _connectedEvent.Add(value); }
|
||||||
@@ -57,7 +59,7 @@ namespace Discord.Audio
|
|||||||
private DiscordSocketClient Discord => Guild.Discord;
|
private DiscordSocketClient Discord => Guild.Discord;
|
||||||
|
|
||||||
/// <summary> Creates a new REST/WebSocket discord client. </summary>
|
/// <summary> Creates a new REST/WebSocket discord client. </summary>
|
||||||
internal AudioClient(CachedGuild guild, int id)
|
public AudioClient(CachedGuild guild, int id)
|
||||||
{
|
{
|
||||||
Guild = guild;
|
Guild = guild;
|
||||||
|
|
||||||
@@ -171,6 +173,22 @@ namespace Discord.Audio
|
|||||||
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
|
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)
|
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
|
||||||
{
|
{
|
||||||
#if BENCHMARK
|
#if BENCHMARK
|
||||||
@@ -253,7 +271,6 @@ namespace Discord.Audio
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessPacketAsync(byte[] packet)
|
private async Task ProcessPacketAsync(byte[] packet)
|
||||||
{
|
{
|
||||||
if (!_connectTask.Task.IsCompleted)
|
if (!_connectTask.Task.IsCompleted)
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ namespace Discord.Audio
|
|||||||
int Latency { get; }
|
int Latency { get; }
|
||||||
|
|
||||||
Task DisconnectAsync();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Discord.Audio.Opus
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
internal enum OpusApplication : int
|
public enum OpusApplication : int
|
||||||
{
|
{
|
||||||
Voice = 2048,
|
Voice = 2048,
|
||||||
MusicOrMixed = 2049,
|
MusicOrMixed = 2049,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Discord.Audio.Opus
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
internal abstract class OpusConverter : IDisposable
|
internal abstract class OpusConverter : IDisposable
|
||||||
{
|
{
|
||||||
@@ -8,17 +8,27 @@ namespace Discord.Audio.Opus
|
|||||||
|
|
||||||
/// <summary> Gets the bit rate of this converter. </summary>
|
/// <summary> Gets the bit rate of this converter. </summary>
|
||||||
public const int BitsPerSample = 16;
|
public const int BitsPerSample = 16;
|
||||||
|
/// <summary> Gets the bytes per sample. </summary>
|
||||||
|
public const int SampleSize = (BitsPerSample / 8) * MaxChannels;
|
||||||
|
/// <summary> Gets the maximum amount of channels this encoder supports. </summary>
|
||||||
|
public const int MaxChannels = 2;
|
||||||
|
|
||||||
/// <summary> Gets the input sampling rate of this converter. </summary>
|
/// <summary> Gets the input sampling rate of this converter. </summary>
|
||||||
public int SamplingRate { get; }
|
public int SamplingRate { get; }
|
||||||
|
/// <summary> Gets the number of samples per second for this stream. </summary>
|
||||||
|
public int Channels { get; }
|
||||||
|
|
||||||
protected OpusConverter(int samplingRate)
|
protected OpusConverter(int samplingRate, int channels)
|
||||||
{
|
{
|
||||||
if (samplingRate != 8000 && samplingRate != 12000 &&
|
if (samplingRate != 8000 && samplingRate != 12000 &&
|
||||||
samplingRate != 16000 && samplingRate != 24000 &&
|
samplingRate != 16000 && samplingRate != 24000 &&
|
||||||
samplingRate != 48000)
|
samplingRate != 48000)
|
||||||
throw new ArgumentOutOfRangeException(nameof(samplingRate));
|
throw new ArgumentOutOfRangeException(nameof(samplingRate));
|
||||||
|
if (channels != 1 && channels != 2)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(channels));
|
||||||
|
|
||||||
SamplingRate = samplingRate;
|
SamplingRate = samplingRate;
|
||||||
|
Channels = channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool disposedValue = false; // To detect redundant calls
|
private bool disposedValue = false; // To detect redundant calls
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Discord.Audio.Opus
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
internal enum Ctl : int
|
internal enum OpusCtl : int
|
||||||
{
|
{
|
||||||
SetBitrateRequest = 4002,
|
SetBitrateRequest = 4002,
|
||||||
GetBitrateRequest = 4003,
|
GetBitrateRequest = 4003,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Discord.Audio.Opus
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
internal unsafe class OpusDecoder : OpusConverter
|
internal unsafe class OpusDecoder : OpusConverter
|
||||||
{
|
{
|
||||||
@@ -10,13 +10,13 @@ namespace Discord.Audio.Opus
|
|||||||
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern void DestroyDecoder(IntPtr decoder);
|
private static extern void DestroyDecoder(IntPtr decoder);
|
||||||
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
|
[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)
|
public OpusDecoder(int samplingRate, int channels)
|
||||||
: base(samplingRate)
|
: base(samplingRate, channels)
|
||||||
{
|
{
|
||||||
OpusError error;
|
OpusError error;
|
||||||
_ptr = CreateDecoder(samplingRate, 2, out error);
|
_ptr = CreateDecoder(samplingRate, channels, out error);
|
||||||
if (error != OpusError.OK)
|
if (error != OpusError.OK)
|
||||||
throw new InvalidOperationException($"Error occured while creating decoder: {error}");
|
throw new InvalidOperationException($"Error occured while creating decoder: {error}");
|
||||||
}
|
}
|
||||||
@@ -25,11 +25,12 @@ namespace Discord.Audio.Opus
|
|||||||
/// <param name="input">PCM samples to decode.</param>
|
/// <param name="input">PCM samples to decode.</param>
|
||||||
/// <param name="inputOffset">Offset of the frame in input.</param>
|
/// <param name="inputOffset">Offset of the frame in input.</param>
|
||||||
/// <param name="output">Buffer to store the decoded frame.</param>
|
/// <param name="output">Buffer to store the decoded frame.</param>
|
||||||
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;
|
int result = 0;
|
||||||
fixed (byte* inPtr = input)
|
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)
|
if (result < 0)
|
||||||
throw new Exception(((OpusError)result).ToString());
|
throw new Exception(((OpusError)result).ToString());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Discord.Audio.Opus
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
internal unsafe class OpusEncoder : OpusConverter
|
internal unsafe class OpusEncoder : OpusConverter
|
||||||
{
|
{
|
||||||
@@ -10,49 +10,22 @@ namespace Discord.Audio.Opus
|
|||||||
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern void DestroyEncoder(IntPtr encoder);
|
private static extern void DestroyEncoder(IntPtr encoder);
|
||||||
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
|
[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)]
|
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern int EncoderCtl(IntPtr st, Ctl request, int value);
|
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value);
|
||||||
|
|
||||||
/// <summary> Gets the bit rate in kbit/s. </summary>
|
|
||||||
public int? BitRate { get; }
|
|
||||||
/// <summary> Gets the coding mode of the encoder. </summary>
|
/// <summary> Gets the coding mode of the encoder. </summary>
|
||||||
public OpusApplication Application { get; }
|
public OpusApplication Application { get; }
|
||||||
/// <summary> Gets the number of channels of this converter. </summary>
|
|
||||||
public int InputChannels { get; }
|
|
||||||
/// <summary> Gets the milliseconds per frame. </summary>
|
|
||||||
public int FrameMilliseconds { get; }
|
|
||||||
|
|
||||||
/// <summary> Gets the bytes per sample. </summary>
|
public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed)
|
||||||
public int SampleSize => (BitsPerSample / 8) * InputChannels;
|
: base(samplingRate, channels)
|
||||||
/// <summary> Gets the number of samples per frame. </summary>
|
|
||||||
public int SamplesPerFrame => SamplingRate / 1000 * FrameMilliseconds;
|
|
||||||
/// <summary> Gets the bytes per frame. </summary>
|
|
||||||
public int FrameSize => SamplesPerFrame * SampleSize;
|
|
||||||
|
|
||||||
public OpusEncoder(int samplingRate, int channels, int frameMillis,
|
|
||||||
int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed)
|
|
||||||
: base(samplingRate)
|
|
||||||
{
|
{
|
||||||
if (channels != 1 && channels != 2)
|
Application = application;
|
||||||
throw new ArgumentOutOfRangeException(nameof(channels));
|
|
||||||
if (bitrate != null && (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate))
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(bitrate));
|
|
||||||
|
|
||||||
OpusError error;
|
OpusError error;
|
||||||
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
|
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
|
||||||
if (error != OpusError.OK)
|
if (error != OpusError.OK)
|
||||||
throw new InvalidOperationException($"Error occured while creating encoder: {error}");
|
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
|
|||||||
/// <param name="inputOffset">Offset of the frame in pcmSamples.</param>
|
/// <param name="inputOffset">Offset of the frame in pcmSamples.</param>
|
||||||
/// <param name="output">Buffer to store the encoded frame.</param>
|
/// <param name="output">Buffer to store the encoded frame.</param>
|
||||||
/// <returns>Length of the frame contained in outputBuffer.</returns>
|
/// <returns>Length of the frame contained in outputBuffer.</returns>
|
||||||
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;
|
int result = 0;
|
||||||
fixed (byte* inPtr = input)
|
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)
|
if (result < 0)
|
||||||
throw new Exception(((OpusError)result).ToString());
|
throw new Exception(((OpusError)result).ToString());
|
||||||
@@ -75,7 +49,7 @@ namespace Discord.Audio.Opus
|
|||||||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
|
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
|
||||||
public void SetForwardErrorCorrection(bool value)
|
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)
|
if (result < 0)
|
||||||
throw new Exception(((OpusError)result).ToString());
|
throw new Exception(((OpusError)result).ToString());
|
||||||
}
|
}
|
||||||
@@ -83,7 +57,10 @@ namespace Discord.Audio.Opus
|
|||||||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
|
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
|
||||||
public void SetBitrate(int value)
|
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)
|
if (result < 0)
|
||||||
throw new Exception(((OpusError)result).ToString());
|
throw new Exception(((OpusError)result).ToString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Discord.Audio.Opus
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
internal enum OpusError : int
|
internal enum OpusError : int
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Discord.Net.Audio.Sodium
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
public unsafe static class SecretBox
|
public unsafe static class SecretBox
|
||||||
{
|
{
|
||||||
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)]
|
[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)]
|
[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* outPtr = output)
|
|
||||||
return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret);
|
|
||||||
}
|
|
||||||
public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret)
|
|
||||||
{
|
{
|
||||||
fixed (byte* inPtr = input)
|
fixed (byte* inPtr = input)
|
||||||
return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret);
|
fixed (byte* outPtr = output)
|
||||||
|
return SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret);
|
||||||
|
}
|
||||||
|
public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
|
||||||
|
{
|
||||||
|
fixed (byte* inPtr = input)
|
||||||
|
fixed (byte* outPtr = output)
|
||||||
|
return SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/Discord.Net/Audio/Streams/OpusDecodeStream.cs
Normal file
30
src/Discord.Net/Audio/Streams/OpusDecodeStream.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/Discord.Net/Audio/Streams/OpusEncodeStream.cs
Normal file
34
src/Discord.Net/Audio/Streams/OpusEncodeStream.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Discord.Net/Audio/Streams/RTPReadStream.cs
Normal file
53
src/Discord.Net/Audio/Streams/RTPReadStream.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
public class RTPReadStream : Stream
|
||||||
|
{
|
||||||
|
private readonly BlockingCollection<byte[]> _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<byte[]>(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(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Discord.Net/Audio/Streams/RTPWriteStream.cs
Normal file
66
src/Discord.Net/Audio/Streams/RTPWriteStream.cs
Normal file
@@ -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(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user