@@ -615,7 +615,7 @@ namespace Discord | |||
} | |||
/// <summary> Sends a PCM frame to the voice server. </summary> | |||
/// <param name="data">PCM frame to send. This must be an uncompressed 48Kz monochannel 20ms PCM frame. </param> | |||
/// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param> | |||
/// <param name="count">Number of bytes in this frame. </param> | |||
/// <remarks>Will block until</remarks> | |||
public void SendVoicePCM(byte[] data, int count) | |||
@@ -626,7 +626,7 @@ namespace Discord | |||
if (_isDebugMode) | |||
RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output."); | |||
#if !DNXCORE50 | |||
_voiceWebSocket.SendPCMFrame(data, count); | |||
_voiceWebSocket.SendPCMFrames(data, count); | |||
#endif | |||
} | |||
@@ -409,33 +409,70 @@ namespace Discord | |||
} | |||
} | |||
public void SendPCMFrame(byte[] data, int count) | |||
public void SendPCMFrames(byte[] data, int bytes) | |||
{ | |||
if (!_isReady) | |||
var cancelToken = _disconnectToken.Token; | |||
if (!_isReady || cancelToken == null) | |||
throw new InvalidOperationException("Not connected to a voice server."); | |||
if (bytes == 0) | |||
return; | |||
if (count != _encoder.FrameSize) | |||
throw new InvalidOperationException($"Invalid frame size. Got {count}, expected {_encoder.FrameSize}."); | |||
byte[] payload; | |||
lock (_encoder) | |||
int frameSize = _encoder.FrameSize; | |||
int frames = bytes / frameSize; | |||
int expectedBytes = frames * frameSize; | |||
int lastFrameSize = expectedBytes - bytes; | |||
//If this only consists of a partial frame and the buffer is too small to pad the end, make a new one | |||
if (data.Length < frameSize) | |||
{ | |||
int encodedLength = _encoder.EncodeFrame(data, _encodingBuffer); | |||
byte[] newData = new byte[frameSize]; | |||
Buffer.BlockCopy(data, 0, newData, 0, bytes); | |||
data = newData; | |||
} | |||
if (_mode == "xsalsa20_poly1305") | |||
byte[] payload; | |||
//Opus encoder requires packets be queued in the same order they were generated, so all of this must still be locked. | |||
lock (_encoder) | |||
{ | |||
for (int i = 0, pos = 0; i <= frames; i++, pos += frameSize) | |||
{ | |||
//TODO: Encode | |||
} | |||
if (i == frames) | |||
{ | |||
//If there are no partial frames, skip this step | |||
if (lastFrameSize == 0) | |||
break; | |||
payload = new byte[encodedLength]; | |||
Buffer.BlockCopy(_encodingBuffer, 0, payload, 0, encodedLength); | |||
} | |||
//Take the partial frame from the end of the buffer and put it at the start | |||
Buffer.BlockCopy(data, pos, data, 0, lastFrameSize); | |||
pos = 0; | |||
_sendQueueWait.Wait(_disconnectToken.Token); | |||
_sendQueue.Enqueue(payload); | |||
if (_sendQueue.Count >= _targetAudioBufferLength) | |||
_sendQueueWait.Reset(); | |||
_sendQueueEmptyWait.Reset(); | |||
} | |||
//Wipe the end of the buffer | |||
for (int j = lastFrameSize; j < frameSize; j++) | |||
data[j] = 0; | |||
} | |||
//Encode the frame | |||
int encodedLength = _encoder.EncodeFrame(data, pos, _encodingBuffer); | |||
//TODO: Handle Encryption | |||
if (_mode == "xsalsa20_poly1305") | |||
{ | |||
} | |||
//Copy result to the queue | |||
payload = new byte[encodedLength]; | |||
Buffer.BlockCopy(_encodingBuffer, 0, payload, 0, encodedLength); | |||
//Wait until the queue has a spot open | |||
_sendQueueWait.Wait(_disconnectToken.Token); | |||
_sendQueue.Enqueue(payload); | |||
if (_sendQueue.Count >= _targetAudioBufferLength) | |||
_sendQueueWait.Reset(); | |||
_sendQueueEmptyWait.Reset(); | |||
} | |||
} | |||
} | |||
public void ClearPCMFrames() | |||
{ | |||
_isClearing = true; | |||
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices; | |||
namespace Discord.Opus | |||
{ | |||
internal class API | |||
internal unsafe class API | |||
{ | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
public static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out Error error); | |||
@@ -12,7 +12,7 @@ namespace Discord.Opus | |||
public static extern void opus_encoder_destroy(IntPtr encoder); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
public static extern int opus_encode(IntPtr st, byte[] pcm, int frame_size, IntPtr data, int max_data_bytes); | |||
public static extern int opus_encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); | |||
/*[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
public static extern IntPtr opus_decoder_create(int Fs, int channels, out Errors error); | |||
@@ -35,9 +35,9 @@ namespace Discord.Opus | |||
if (samplingRate != 8000 && samplingRate != 12000 && | |||
samplingRate != 16000 && samplingRate != 24000 && | |||
samplingRate != 48000) | |||
throw new ArgumentOutOfRangeException("inputSamplingRate"); | |||
throw new ArgumentOutOfRangeException(nameof(samplingRate)); | |||
if (channels != 1 && channels != 2) | |||
throw new ArgumentOutOfRangeException("inputChannels"); | |||
throw new ArgumentOutOfRangeException(nameof(channels)); | |||
InputSamplingRate = samplingRate; | |||
InputChannels = channels; | |||
@@ -50,31 +50,29 @@ namespace Discord.Opus | |||
Error error; | |||
_encoder = API.opus_encoder_create(samplingRate, channels, (int)application, out error); | |||
if (error != Error.OK) | |||
throw new InvalidOperationException("Error occured while creating encoder: " + error.ToString()); | |||
throw new InvalidOperationException($"Error occured while creating encoder: {error}"); | |||
SetForwardErrorCorrection(true); | |||
} | |||
/// <summary> Produces Opus encoded audio from PCM samples. </summary> | |||
/// <param name="pcmSamples">PCM samples to encode.</param> | |||
/// <param name="encodedLength">Length of encoded audio.</param> | |||
/// <returns>Opus encoded audio buffer.</returns> | |||
public unsafe int EncodeFrame(byte[] pcmSamples, byte[] outputBuffer) | |||
/// <param name="offset">Offset of the frame in pcmSamples.</param> | |||
/// <param name="outputBuffer">Buffer to store the encoded frame.</param> | |||
/// <returns>Length of the frame contained in outputBuffer.</returns> | |||
public unsafe int EncodeFrame(byte[] pcmSamples, int offset, byte[] outputBuffer) | |||
{ | |||
if (disposed) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
IntPtr encodedPtr; | |||
int length = 0; | |||
fixed (byte* bPtr = outputBuffer) | |||
{ | |||
encodedPtr = new IntPtr((void*)bPtr); | |||
length = API.opus_encode(_encoder, pcmSamples, SamplesPerFrame, encodedPtr, outputBuffer.Length); | |||
} | |||
int result = 0; | |||
fixed (byte* inPtr = pcmSamples) | |||
fixed (byte* outPtr = outputBuffer) | |||
result = API.opus_encode(_encoder, inPtr + offset, SamplesPerFrame, outPtr, outputBuffer.Length); | |||
if (length < 0) | |||
throw new Exception("Encoding failed: " + ((Error)length).ToString()); | |||
return length; | |||
if (result < 0) | |||
throw new Exception("Encoding failed: " + ((Error)result).ToString()); | |||
return result; | |||
} | |||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | |||
@@ -83,9 +81,9 @@ namespace Discord.Opus | |||
if (_encoder == IntPtr.Zero) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
var ret = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0); | |||
if (ret < 0) | |||
throw new Exception("Encoder error - " + ((Error)ret).ToString()); | |||
var result = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0); | |||
if (result < 0) | |||
throw new Exception("Encoder error: " + ((Error)result).ToString()); | |||
} | |||
private bool disposed; | |||