@@ -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) | |||||
: base(samplingRate) | |||||
public OpusDecoder(int samplingRate, int channels) | |||||
: 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); | |||||
/// <summary> Gets the bit rate in kbit/s. </summary> | |||||
public int? BitRate { get; } | |||||
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); | |||||
/// <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 int SampleSize => (BitsPerSample / 8) * InputChannels; | |||||
/// <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) | |||||
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; | 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* inPtr = input) | |||||
fixed (byte* outPtr = output) | 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) | 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); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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(); | |||||
} | |||||
} | |||||
} |
@@ -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(); | |||||
} | |||||
} | |||||
} |
@@ -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(); } | |||||
} | |||||
} |
@@ -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(); } | |||||
} | |||||
} |