| @@ -615,7 +615,7 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> Sends a PCM frame to the voice server. </summary> | /// <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> | /// <param name="count">Number of bytes in this frame. </param> | ||||
| /// <remarks>Will block until</remarks> | /// <remarks>Will block until</remarks> | ||||
| public void SendVoicePCM(byte[] data, int count) | public void SendVoicePCM(byte[] data, int count) | ||||
| @@ -626,7 +626,7 @@ namespace Discord | |||||
| if (_isDebugMode) | if (_isDebugMode) | ||||
| RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output."); | RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output."); | ||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| _voiceWebSocket.SendPCMFrame(data, count); | |||||
| _voiceWebSocket.SendPCMFrames(data, count); | |||||
| #endif | #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; | 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() | public void ClearPCMFrames() | ||||
| { | { | ||||
| _isClearing = true; | _isClearing = true; | ||||
| @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; | |||||
| namespace Discord.Opus | namespace Discord.Opus | ||||
| { | { | ||||
| internal class API | |||||
| internal unsafe class API | |||||
| { | { | ||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out Error error); | 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); | public static extern void opus_encoder_destroy(IntPtr encoder); | ||||
| [DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | [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)] | /*[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern IntPtr opus_decoder_create(int Fs, int channels, out Errors error); | 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 && | if (samplingRate != 8000 && samplingRate != 12000 && | ||||
| samplingRate != 16000 && samplingRate != 24000 && | samplingRate != 16000 && samplingRate != 24000 && | ||||
| samplingRate != 48000) | samplingRate != 48000) | ||||
| throw new ArgumentOutOfRangeException("inputSamplingRate"); | |||||
| throw new ArgumentOutOfRangeException(nameof(samplingRate)); | |||||
| if (channels != 1 && channels != 2) | if (channels != 1 && channels != 2) | ||||
| throw new ArgumentOutOfRangeException("inputChannels"); | |||||
| throw new ArgumentOutOfRangeException(nameof(channels)); | |||||
| InputSamplingRate = samplingRate; | InputSamplingRate = samplingRate; | ||||
| InputChannels = channels; | InputChannels = channels; | ||||
| @@ -50,31 +50,29 @@ namespace Discord.Opus | |||||
| Error error; | Error error; | ||||
| _encoder = API.opus_encoder_create(samplingRate, channels, (int)application, out error); | _encoder = API.opus_encoder_create(samplingRate, channels, (int)application, out error); | ||||
| if (error != Error.OK) | 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); | SetForwardErrorCorrection(true); | ||||
| } | } | ||||
| /// <summary> Produces Opus encoded audio from PCM samples. </summary> | /// <summary> Produces Opus encoded audio from PCM samples. </summary> | ||||
| /// <param name="pcmSamples">PCM samples to encode.</param> | /// <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) | if (disposed) | ||||
| throw new ObjectDisposedException("OpusEncoder"); | 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> | /// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | ||||
| @@ -83,9 +81,9 @@ namespace Discord.Opus | |||||
| if (_encoder == IntPtr.Zero) | if (_encoder == IntPtr.Zero) | ||||
| throw new ObjectDisposedException("OpusEncoder"); | 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; | private bool disposed; | ||||