@@ -70,5 +70,16 @@ namespace Discord.API.Models | |||
public SocketInfo SocketData = new SocketInfo(); | |||
} | |||
} | |||
public sealed class IsTalking : WebSocketMessage<IsTalking.Data> | |||
{ | |||
public IsTalking() : base(5) { } | |||
public class Data | |||
{ | |||
[JsonProperty(PropertyName = "delay")] | |||
public int Delay; | |||
[JsonProperty(PropertyName = "speaking")] | |||
public bool IsSpeaking; | |||
} | |||
} | |||
} | |||
} |
@@ -27,5 +27,15 @@ namespace Discord.API.Models | |||
[JsonProperty(PropertyName = "mode")] | |||
public string Mode; | |||
} | |||
public sealed class IsTalking | |||
{ | |||
[JsonProperty(PropertyName = "user_id")] | |||
public string UserId; | |||
[JsonProperty(PropertyName = "ssrc")] | |||
public uint SSRC; | |||
[JsonProperty(PropertyName = "speaking")] | |||
public bool IsSpeaking; | |||
} | |||
} | |||
} |
@@ -287,7 +287,7 @@ namespace Discord | |||
user => { } | |||
); | |||
_webSocket = new DiscordTextWebSocket(_config.ConnectionTimeout, _config.WebSocketInterval); | |||
_webSocket = new DiscordTextWebSocket(this, _config.ConnectionTimeout, _config.WebSocketInterval); | |||
_webSocket.Connected += (s, e) => RaiseConnected(); | |||
_webSocket.Disconnected += async (s, e) => | |||
{ | |||
@@ -312,7 +312,7 @@ namespace Discord | |||
}; | |||
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | |||
_voiceWebSocket = new DiscordVoiceSocket(_config.VoiceConnectionTimeout, _config.WebSocketInterval); | |||
_voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval); | |||
_voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||
_voiceWebSocket.Disconnected += (s, e) => | |||
{ | |||
@@ -1321,9 +1321,12 @@ namespace Discord | |||
} | |||
#if !DNXCORE50 | |||
public void SendVoiceWAV(byte[] buffer, int count) | |||
/// <summary> Sends a PCM frame to the voice server. </summary> | |||
/// <param name="data">PCM frame to send.</param> | |||
/// <param name="count">Number of bytes in this frame. </param> | |||
public void SendVoicePCM(byte[] data, int count) | |||
{ | |||
_voiceWebSocket.SendWAV(buffer, count); | |||
_voiceWebSocket.SendPCMFrame(data, count); | |||
} | |||
#endif | |||
@@ -13,8 +13,8 @@ namespace Discord | |||
{ | |||
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | |||
public DiscordTextWebSocket(int timeout, int interval) | |||
: base(timeout, interval) | |||
public DiscordTextWebSocket(DiscordClient client, int timeout, int interval) | |||
: base(client, timeout, interval) | |||
{ | |||
_connectWaitOnLogin = new ManualResetEventSlim(false); | |||
_connectWaitOnLogin2 = new ManualResetEventSlim(false); | |||
@@ -1,16 +1,16 @@ | |||
using Discord.API.Models; | |||
using Discord.Helpers; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Net.Sockets; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||
using System.Text; | |||
using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||
#if !DNXCORE50 | |||
using Opus.Net; | |||
#endif | |||
@@ -19,50 +19,63 @@ namespace Discord | |||
{ | |||
internal sealed partial class DiscordVoiceSocket : DiscordWebSocket | |||
{ | |||
private struct Packet | |||
{ | |||
public byte[] Data; | |||
public int Count; | |||
public Packet(byte[] data, int count) | |||
{ | |||
Data = data; | |||
Count = count; | |||
} | |||
} | |||
private ManualResetEventSlim _connectWaitOnLogin; | |||
private UdpClient _udp; | |||
private ConcurrentQueue<byte[]> _sendQueue; | |||
private string _myIp; | |||
private IPEndPoint _endpoint; | |||
private byte[] _secretKey; | |||
private string _mode; | |||
private bool _isFirst; | |||
private ushort _sequence; | |||
private uint _ssrc; | |||
private long _startTicks; | |||
private readonly Random _rand = new Random(); | |||
#if !DNXCORE50 | |||
private OpusEncoder _encoder; | |||
private Queue<Packet> _sendQueue; | |||
private UdpClient _udp; | |||
private IPEndPoint _endpoint; | |||
private bool _isReady; | |||
private byte[] _secretKey; | |||
private string _myIp; | |||
private ushort _sequence; | |||
private string _mode; | |||
#endif | |||
public DiscordVoiceSocket(int timeout, int interval) | |||
: base(timeout, interval) | |||
public DiscordVoiceSocket(DiscordClient client, int timeout, int interval) | |||
: base(client, timeout, interval) | |||
{ | |||
_connectWaitOnLogin = new ManualResetEventSlim(false); | |||
_sendQueue = new ConcurrentQueue<byte[]>(); | |||
#if !DNXCORE50 | |||
_encoder = OpusEncoder.Create(24000, 1, Application.Voip); | |||
_sendQueue = new Queue<Packet>(); | |||
_encoder = new OpusEncoder(48000, 1, 20, Application.Audio); | |||
#endif | |||
} | |||
#if !DNXCORE50 | |||
protected override void OnConnect() | |||
{ | |||
_udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
_udp.AllowNatTraversal(true); | |||
_isFirst = true; | |||
} | |||
protected override void OnDisconnect() | |||
{ | |||
_udp = null; | |||
} | |||
#endif | |||
protected override Task[] CreateTasks(CancellationToken cancelToken) | |||
{ | |||
return new Task[] | |||
{ | |||
#if !DNXCORE50 | |||
Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | |||
Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | |||
#endif | |||
Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | |||
}.Concat(base.CreateTasks(cancelToken)).ToArray(); | |||
} | |||
@@ -73,8 +86,6 @@ namespace Discord | |||
_connectWaitOnLogin.Reset(); | |||
_sequence = 0; | |||
VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | |||
msg.Payload.ServerId = serverId; | |||
msg.Payload.SessionId = sessionId; | |||
@@ -95,10 +106,10 @@ namespace Discord | |||
SetConnected(); | |||
} | |||
#if !DNXCORE50 | |||
private async Task ReceiveAsync() | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
try | |||
{ | |||
while (!cancelToken.IsCancellationRequested) | |||
@@ -115,17 +126,69 @@ namespace Discord | |||
var cancelToken = _disconnectToken.Token; | |||
try | |||
{ | |||
byte[] bytes; | |||
while (!cancelToken.IsCancellationRequested) | |||
while (!cancelToken.IsCancellationRequested && !_isReady) | |||
{ | |||
while (_sendQueue.TryDequeue(out bytes)) | |||
await _udp.SendAsync(bytes, bytes.Length); | |||
lock (_sendQueue) | |||
{ | |||
while (_sendQueue.Count > 0) | |||
{ | |||
var packet = _sendQueue.Dequeue(); | |||
_udp.Send(packet.Data, packet.Count); | |||
} | |||
} | |||
await Task.Delay(_sendInterval); | |||
} | |||
if (cancelToken.IsCancellationRequested) | |||
return; | |||
uint timestamp = 0; | |||
double nextTicks = 0.0; | |||
double ticksPerFrame = Stopwatch.Frequency / 1000.0 * _encoder.FrameLength; | |||
uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; | |||
Stopwatch sw = Stopwatch.StartNew(); | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
byte[] rtpPacket = new byte[4012]; | |||
rtpPacket[0] = 0x80; //Flags; | |||
rtpPacket[1] = 0x78; //Payload Type | |||
rtpPacket[8] = (byte)((_ssrc >> 24) & 0xFF); | |||
rtpPacket[9] = (byte)((_ssrc >> 16) & 0xFF); | |||
rtpPacket[10] = (byte)((_ssrc >> 8) & 0xFF); | |||
rtpPacket[11] = (byte)((_ssrc >> 0) & 0xFF); | |||
if (sw.ElapsedTicks > nextTicks) | |||
{ | |||
lock (_sendQueue) | |||
{ | |||
while (sw.ElapsedTicks > nextTicks) | |||
{ | |||
if (_sendQueue.Count > 0) | |||
{ | |||
var packet = _sendQueue.Dequeue(); | |||
ushort sequence = unchecked(_sequence++); | |||
rtpPacket[2] = (byte)((sequence >> 8) & 0xFF); | |||
rtpPacket[3] = (byte)((sequence >> 0) & 0xFF); | |||
rtpPacket[4] = (byte)((timestamp >> 24) & 0xFF); | |||
rtpPacket[5] = (byte)((timestamp >> 16) & 0xFF); | |||
rtpPacket[6] = (byte)((timestamp >> 8) & 0xFF); | |||
rtpPacket[7] = (byte)((timestamp >> 0) & 0xFF); | |||
Buffer.BlockCopy(packet.Data, 0, rtpPacket, 12, packet.Count); | |||
_udp.Send(rtpPacket, packet.Count + 12); | |||
} | |||
timestamp = unchecked(timestamp + samplesPerFrame); | |||
nextTicks += ticksPerFrame; | |||
} | |||
} | |||
} | |||
/*else | |||
await Task.Delay(1);*/ | |||
} | |||
} | |||
catch { } | |||
finally { _disconnectToken.Cancel(); } | |||
} | |||
#endif | |||
private async Task WatcherAsync() | |||
{ | |||
try | |||
@@ -133,14 +196,16 @@ namespace Discord | |||
await Task.Delay(-1, _disconnectToken.Token); | |||
} | |||
catch (TaskCanceledException) { } | |||
#if DNXCORE50 | |||
finally { _udp.Dispose(); } | |||
#else | |||
#if !DNXCORE50 | |||
finally { _udp.Close(); } | |||
#endif | |||
} | |||
#if DNXCORE50 | |||
protected override Task ProcessMessage(string json) | |||
#else | |||
protected override async Task ProcessMessage(string json) | |||
#endif | |||
{ | |||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
switch (msg.Operation) | |||
@@ -149,18 +214,17 @@ namespace Discord | |||
{ | |||
var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>(); | |||
_heartbeatInterval = payload.HeartbeatInterval; | |||
_ssrc = payload.SSRC; | |||
#if !DNXCORE50 | |||
_endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host)).FirstOrDefault(), payload.Port); | |||
//_mode = payload.Modes.LastOrDefault(); | |||
_mode = "plain"; | |||
_udp.Connect(_endpoint); | |||
lock(_rand) | |||
{ | |||
_sequence = (ushort)_rand.Next(0, ushort.MaxValue); | |||
_startTicks = DateTime.UtcNow.Ticks - _rand.Next(); | |||
} | |||
_ssrc = payload.SSRC; | |||
_sendQueue.Enqueue(new byte[70] { | |||
lock (_rand) | |||
_sequence = (ushort)_rand.Next(0, ushort.MaxValue); | |||
_isReady = false; | |||
_sendQueue.Enqueue(new Packet(new byte[70] { | |||
(byte)((_ssrc >> 24) & 0xFF), | |||
(byte)((_ssrc >> 16) & 0xFF), | |||
(byte)((_ssrc >> 8) & 0xFF), | |||
@@ -170,30 +234,40 @@ namespace Discord | |||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0 | |||
}); | |||
}, 70)); | |||
#else | |||
_connectWaitOnLogin.Set(); | |||
#endif | |||
} | |||
break; | |||
#if !DNXCORE50 | |||
case 4: //SESSION_DESCRIPTION | |||
{ | |||
var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>(); | |||
_secretKey = payload.SecretKey; | |||
SendIsTalking(true); | |||
_connectWaitOnLogin.Set(); | |||
} | |||
break; | |||
#endif | |||
default: | |||
RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||
break; | |||
} | |||
#if DNXCORE50 | |||
return Task.CompletedTask; | |||
#endif | |||
} | |||
#if !DNXCORE50 | |||
private void ProcessUdpMessage(UdpReceiveResult msg) | |||
{ | |||
if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint)) | |||
{ | |||
byte[] buffer = msg.Buffer; | |||
int length = msg.Buffer.Length; | |||
if (_isFirst) | |||
if (!_isReady) | |||
{ | |||
_isFirst = false; | |||
_isReady = true; | |||
if (length != 70) | |||
throw new Exception($"Unexpected message length. Expected 70, got {length}."); | |||
@@ -256,36 +330,29 @@ namespace Discord | |||
} | |||
} | |||
#if !DNXCORE50 | |||
public void SendWAV(byte[] buffer, int count) | |||
public void SendPCMFrame(byte[] data, int count) | |||
{ | |||
int encodedLength; | |||
byte[] payload = _encoder.Encode(buffer, count, out encodedLength); | |||
if (count != _encoder.FrameSize) | |||
throw new InvalidOperationException($"Invalid frame size. Got {count}, expected {_encoder.FrameSize}."); | |||
byte[] payload = new byte[4000]; | |||
int encodedLength = _encoder.EncodeFrame(data, payload); | |||
if (_mode == "xsalsa20_poly1305") | |||
{ | |||
//TODO: Encode | |||
} | |||
lock (_sendQueue) | |||
_sendQueue.Enqueue(new Packet(payload, encodedLength)); | |||
} | |||
byte[] packet = new byte[12 + encodedLength]; | |||
Buffer.BlockCopy(payload, 0, packet, 12, encodedLength); | |||
ushort sequence = _sequence++; | |||
long timestamp = (DateTime.UtcNow.Ticks - _startTicks) >> 2; //200ns resolution | |||
packet[0] = 0x80; //Flags; | |||
packet[1] = 0x78; //Payload Type | |||
packet[2] = (byte)((sequence >> 8) & 0xFF); | |||
packet[3] = (byte)((sequence >> 0) & 0xFF); | |||
packet[4] = (byte)((timestamp >> 24) & 0xFF); | |||
packet[5] = (byte)((timestamp >> 16) & 0xFF); | |||
packet[6] = (byte)((timestamp >> 8) & 0xFF); | |||
packet[7] = (byte)((timestamp >> 0) & 0xFF); | |||
packet[8] = (byte)((_ssrc >> 24) & 0xFF); | |||
packet[9] = (byte)((_ssrc >> 16) & 0xFF); | |||
packet[10] = (byte)((_ssrc >> 8) & 0xFF); | |||
packet[11] = (byte)((_ssrc >> 0) & 0xFF); | |||
_sendQueue.Enqueue(packet); | |||
private void SendIsTalking(bool value) | |||
{ | |||
var isTalking = new VoiceWebSocketCommands.IsTalking(); | |||
isTalking.Payload.IsSpeaking = value; | |||
isTalking.Payload.Delay = 0; | |||
QueueMessage(isTalking); | |||
} | |||
#endif | |||
@@ -17,6 +17,7 @@ namespace Discord | |||
private const int ReceiveChunkSize = 4096; | |||
private const int SendChunkSize = 4096; | |||
protected readonly DiscordClient _client; | |||
protected volatile CancellationTokenSource _disconnectToken; | |||
protected int _timeout, _heartbeatInterval; | |||
protected readonly int _sendInterval; | |||
@@ -28,9 +29,10 @@ namespace Discord | |||
private DateTime _lastHeartbeat; | |||
private bool _isConnected; | |||
public DiscordWebSocket(int timeout, int interval) | |||
public DiscordWebSocket(DiscordClient client, int timeout, int interval) | |||
{ | |||
_timeout = timeout; | |||
_client = client; | |||
_timeout = timeout; | |||
_sendInterval = interval; | |||
_sendQueue = new ConcurrentQueue<byte[]>(); | |||
@@ -53,9 +53,6 @@ | |||
<Compile Include="..\Opus.Net\API.cs"> | |||
<Link>API.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Opus.Net\OpusDecoder.cs"> | |||
<Link>OpusDecoder.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Opus.Net\OpusEncoder.cs"> | |||
<Link>OpusEncoder.cs</Link> | |||
</Compile> | |||
@@ -10,28 +10,28 @@ namespace Opus.Net | |||
internal class API | |||
{ | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out IntPtr error); | |||
public static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out Error error); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern void opus_encoder_destroy(IntPtr encoder); | |||
public static extern void opus_encoder_destroy(IntPtr encoder); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal 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, IntPtr data, int max_data_bytes); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern IntPtr opus_decoder_create(int Fs, int channels, out IntPtr error); | |||
/*[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
public static extern IntPtr opus_decoder_create(int Fs, int channels, out Errors error); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern void opus_decoder_destroy(IntPtr decoder); | |||
public static extern void opus_decoder_destroy(IntPtr decoder); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern int opus_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec); | |||
public static extern int opus_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);*/ | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value); | |||
public static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value); | |||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)] | |||
internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, out int value); | |||
public static extern int opus_encoder_ctl(IntPtr st, Ctl request, out int value); | |||
} | |||
public enum Ctl : int | |||
@@ -45,23 +45,26 @@ namespace Opus.Net | |||
/// <summary> | |||
/// Supported coding modes. | |||
/// </summary> | |||
public enum Application | |||
public enum Application : int | |||
{ | |||
/// <summary> | |||
/// Best for most VoIP/videoconference applications where listening quality and intelligibility matter most. | |||
/// Gives best quality at a given bitrate for voice signals. It enhances the input signal by high-pass filtering and emphasizing formants and harmonics. | |||
/// Optionally it includes in-band forward error correction to protect against packet loss. Use this mode for typical VoIP applications. | |||
/// Because of the enhancement, even at high bitrates the output may sound different from the input. | |||
/// </summary> | |||
Voip = 2048, | |||
/// <summary> | |||
/// Best for broadcast/high-fidelity application where the decoded audio should be as close as possible to input. | |||
/// Gives best quality at a given bitrate for most non-voice signals like music. | |||
/// Use this mode for music and mixed (music/voice) content, broadcast, and applications requiring less than 15 ms of coding delay. | |||
/// </summary> | |||
Audio = 2049, | |||
/// <summary> | |||
/// Only use when lowest-achievable latency is what matters most. Voice-optimized modes cannot be used. | |||
/// Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. | |||
/// </summary> | |||
Restricted_LowLatency = 2051 | |||
} | |||
public enum Errors | |||
public enum Error : int | |||
{ | |||
/// <summary> | |||
/// No error. | |||
@@ -1,133 +0,0 @@ | |||
using System; | |||
namespace Opus.Net | |||
{ | |||
/// <summary> | |||
/// Opus audio decoder. | |||
/// </summary> | |||
public class OpusDecoder : IDisposable | |||
{ | |||
/// <summary> | |||
/// Creates a new Opus decoder. | |||
/// </summary> | |||
/// <param name="outputSampleRate">Sample rate to decode at (Hz). This must be one of 8000, 12000, 16000, 24000, or 48000.</param> | |||
/// <param name="outputChannels">Number of channels to decode.</param> | |||
/// <returns>A new <c>OpusDecoder</c>.</returns> | |||
public static OpusDecoder Create(int outputSampleRate, int outputChannels) | |||
{ | |||
if (outputSampleRate != 8000 && | |||
outputSampleRate != 12000 && | |||
outputSampleRate != 16000 && | |||
outputSampleRate != 24000 && | |||
outputSampleRate != 48000) | |||
throw new ArgumentOutOfRangeException("inputSamplingRate"); | |||
if (outputChannels != 1 && outputChannels != 2) | |||
throw new ArgumentOutOfRangeException("inputChannels"); | |||
IntPtr error; | |||
IntPtr decoder = API.opus_decoder_create(outputSampleRate, outputChannels, out error); | |||
if ((Errors)error != Errors.OK) | |||
{ | |||
throw new Exception("Exception occured while creating decoder"); | |||
} | |||
return new OpusDecoder(decoder, outputSampleRate, outputChannels); | |||
} | |||
private IntPtr _decoder; | |||
private OpusDecoder(IntPtr decoder, int outputSamplingRate, int outputChannels) | |||
{ | |||
_decoder = decoder; | |||
OutputSamplingRate = outputSamplingRate; | |||
OutputChannels = outputChannels; | |||
MaxDataBytes = 4000; | |||
} | |||
/// <summary> | |||
/// Produces PCM samples from Opus encoded data. | |||
/// </summary> | |||
/// <param name="inputOpusData">Opus encoded data to decode, null for dropped packet.</param> | |||
/// <param name="dataLength">Length of data to decode.</param> | |||
/// <param name="decodedLength">Set to the length of the decoded sample data.</param> | |||
/// <returns>PCM audio samples.</returns> | |||
public unsafe byte[] Decode(byte[] inputOpusData, int dataLength, out int decodedLength) | |||
{ | |||
if (disposed) | |||
throw new ObjectDisposedException("OpusDecoder"); | |||
IntPtr decodedPtr; | |||
byte[] decoded = new byte[MaxDataBytes]; | |||
int frameCount = FrameCount(MaxDataBytes); | |||
int length = 0; | |||
fixed (byte* bdec = decoded) | |||
{ | |||
decodedPtr = new IntPtr((void*)bdec); | |||
if (inputOpusData != null) | |||
length = API.opus_decode(_decoder, inputOpusData, dataLength, decodedPtr, frameCount, 0); | |||
else | |||
length = API.opus_decode(_decoder, null, 0, decodedPtr, frameCount, (ForwardErrorCorrection) ? 1 : 0); | |||
} | |||
decodedLength = length * 2; | |||
if (length < 0) | |||
throw new Exception("Decoding failed - " + ((Errors)length).ToString()); | |||
return decoded; | |||
} | |||
/// <summary> | |||
/// Determines the number of frames that can fit into a buffer of the given size. | |||
/// </summary> | |||
/// <param name="bufferSize"></param> | |||
/// <returns></returns> | |||
public int FrameCount(int bufferSize) | |||
{ | |||
// seems like bitrate should be required | |||
int bitrate = 16; | |||
int bytesPerSample = (bitrate / 8) * OutputChannels; | |||
return bufferSize / bytesPerSample; | |||
} | |||
/// <summary> | |||
/// Gets the output sampling rate of the decoder. | |||
/// </summary> | |||
public int OutputSamplingRate { get; private set; } | |||
/// <summary> | |||
/// Gets the number of channels of the decoder. | |||
/// </summary> | |||
public int OutputChannels { get; private set; } | |||
/// <summary> | |||
/// Gets or sets the size of memory allocated for decoding data. | |||
/// </summary> | |||
public int MaxDataBytes { get; set; } | |||
/// <summary> | |||
/// Gets or sets whether forward error correction is enabled or not. | |||
/// </summary> | |||
public bool ForwardErrorCorrection { get; set; } | |||
~OpusDecoder() | |||
{ | |||
Dispose(); | |||
} | |||
private bool disposed; | |||
public void Dispose() | |||
{ | |||
if (disposed) | |||
return; | |||
GC.SuppressFinalize(this); | |||
if (_decoder != IntPtr.Zero) | |||
{ | |||
API.opus_decoder_destroy(_decoder); | |||
_decoder = IntPtr.Zero; | |||
} | |||
disposed = true; | |||
} | |||
} | |||
} |
@@ -2,180 +2,90 @@ | |||
namespace Opus.Net | |||
{ | |||
/// <summary> | |||
/// Opus codec wrapper. | |||
/// </summary> | |||
/// <summary> Opus codec wrapper. </summary> | |||
public class OpusEncoder : IDisposable | |||
{ | |||
/// <summary> | |||
/// Creates a new Opus encoder. | |||
/// </summary> | |||
/// <param name="inputSamplingRate">Sampling rate of the input signal (Hz). This must be one of 8000, 12000, 16000, 24000, or 48000.</param> | |||
/// <param name="inputChannels">Number of channels (1 or 2) in input signal.</param> | |||
private readonly IntPtr _encoder; | |||
/// <summary> Gets the bit rate of the encoder. </summary> | |||
public const int BitRate = 16; | |||
/// <summary> Gets the input sampling rate of the encoder. </summary> | |||
public int InputSamplingRate { get; private set; } | |||
/// <summary> Gets the number of channels of the encoder. </summary> | |||
public int InputChannels { get; private set; } | |||
/// <summary> Gets the milliseconds per frame. </summary> | |||
public int FrameLength { get; private set; } | |||
/// <summary> Gets the number of samples per frame. </summary> | |||
public int SamplesPerFrame { get; private set; } | |||
/// <summary> Gets the bytes per sample. </summary> | |||
public int SampleSize { get; private set; } | |||
/// <summary> Gets the bytes per frame. </summary> | |||
public int FrameSize { get; private set; } | |||
/// <summary> Gets the coding mode of the encoder. </summary> | |||
public Application Application { get; private set; } | |||
/// <summary> Creates a new Opus encoder. </summary> | |||
/// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param> | |||
/// <param name="channels">Number of channels (1 or 2) in input signal.</param> | |||
/// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param> | |||
/// <param name="application">Coding mode.</param> | |||
/// <returns>A new <c>OpusEncoder</c></returns> | |||
public static OpusEncoder Create(int inputSamplingRate, int inputChannels, Application application) | |||
public OpusEncoder(int samplingRate, int channels, int frameLength, Application application) | |||
{ | |||
if (inputSamplingRate != 8000 && | |||
inputSamplingRate != 12000 && | |||
inputSamplingRate != 16000 && | |||
inputSamplingRate != 24000 && | |||
inputSamplingRate != 48000) | |||
if (samplingRate != 8000 && samplingRate != 12000 && | |||
samplingRate != 16000 && samplingRate != 24000 && | |||
samplingRate != 48000) | |||
throw new ArgumentOutOfRangeException("inputSamplingRate"); | |||
if (inputChannels != 1 && inputChannels != 2) | |||
if (channels != 1 && channels != 2) | |||
throw new ArgumentOutOfRangeException("inputChannels"); | |||
IntPtr error; | |||
IntPtr encoder = API.opus_encoder_create(inputSamplingRate, inputChannels, (int)application, out error); | |||
if ((Errors)error != Errors.OK) | |||
{ | |||
throw new Exception("Exception occured while creating encoder"); | |||
} | |||
return new OpusEncoder(encoder, inputSamplingRate, inputChannels, application); | |||
} | |||
InputSamplingRate = samplingRate; | |||
InputChannels = channels; | |||
Application = application; | |||
FrameLength = frameLength; | |||
SampleSize = (BitRate / 8) * channels; | |||
SamplesPerFrame = samplingRate / 1000 * FrameLength; | |||
FrameSize = SamplesPerFrame * SampleSize; | |||
private IntPtr _encoder; | |||
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()); | |||
private OpusEncoder(IntPtr encoder, int inputSamplingRate, int inputChannels, Application application) | |||
{ | |||
_encoder = encoder; | |||
InputSamplingRate = inputSamplingRate; | |||
InputChannels = inputChannels; | |||
Application = application; | |||
MaxDataBytes = 4000; | |||
SetForwardErrorCorrection(true); | |||
} | |||
/// <summary> | |||
/// Produces Opus encoded audio from PCM samples. | |||
/// </summary> | |||
/// <param name="inputPcmSamples">PCM samples to encode.</param> | |||
/// <param name="sampleLength">How many bytes to encode.</param> | |||
/// <param name="encodedLength">Set to length of encoded audio.</param> | |||
/// <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 byte[] Encode(byte[] inputPcmSamples, int sampleLength, out int encodedLength) | |||
public unsafe int EncodeFrame(byte[] pcmSamples, byte[] outputBuffer) | |||
{ | |||
if (disposed) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
int frames = FrameCount(inputPcmSamples); | |||
IntPtr encodedPtr; | |||
byte[] encoded = new byte[MaxDataBytes]; | |||
int length = 0; | |||
fixed (byte* benc = encoded) | |||
fixed (byte* bPtr = outputBuffer) | |||
{ | |||
encodedPtr = new IntPtr((void*)benc); | |||
length = API.opus_encode(_encoder, inputPcmSamples, frames, encodedPtr, sampleLength); | |||
encodedPtr = new IntPtr((void*)bPtr); | |||
length = API.opus_encode(_encoder, pcmSamples, SamplesPerFrame, encodedPtr, outputBuffer.Length); | |||
} | |||
encodedLength = length; | |||
if (length < 0) | |||
throw new Exception("Encoding failed - " + ((Errors)length).ToString()); | |||
return encoded; | |||
} | |||
/// <summary> | |||
/// Determines the number of frames in the PCM samples. | |||
/// </summary> | |||
/// <param name="pcmSamples"></param> | |||
/// <returns></returns> | |||
public int FrameCount(byte[] pcmSamples) | |||
{ | |||
// seems like bitrate should be required | |||
int bitrate = 16; | |||
int bytesPerSample = (bitrate / 8) * InputChannels; | |||
return pcmSamples.Length / bytesPerSample; | |||
} | |||
/// <summary> | |||
/// Helper method to determine how many bytes are required for encoding to work. | |||
/// </summary> | |||
/// <param name="frameCount">Target frame size.</param> | |||
/// <returns></returns> | |||
public int FrameByteCount(int frameCount) | |||
{ | |||
int bitrate = 16; | |||
int bytesPerSample = (bitrate / 8) * InputChannels; | |||
return frameCount * bytesPerSample; | |||
} | |||
/// <summary> | |||
/// Gets the input sampling rate of the encoder. | |||
/// </summary> | |||
public int InputSamplingRate { get; private set; } | |||
/// <summary> | |||
/// Gets the number of channels of the encoder. | |||
/// </summary> | |||
public int InputChannels { get; private set; } | |||
/// <summary> | |||
/// Gets the coding mode of the encoder. | |||
/// </summary> | |||
public Application Application { get; private set; } | |||
/// <summary> | |||
/// Gets or sets the size of memory allocated for reading encoded data. | |||
/// 4000 is recommended. | |||
/// </summary> | |||
public int MaxDataBytes { get; set; } | |||
/// <summary> | |||
/// Gets or sets the bitrate setting of the encoding. | |||
/// </summary> | |||
public int Bitrate | |||
{ | |||
get | |||
{ | |||
if (disposed) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
int bitrate; | |||
var ret = API.opus_encoder_ctl(_encoder, Ctl.GetBitrateRequest, out bitrate); | |||
if (ret < 0) | |||
throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||
return bitrate; | |||
} | |||
set | |||
{ | |||
if (disposed) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
var ret = API.opus_encoder_ctl(_encoder, Ctl.SetBitrateRequest, value); | |||
if (ret < 0) | |||
throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||
} | |||
if (length < 0) | |||
throw new Exception("Encoding failed: " + ((Error)length).ToString()); | |||
return length; | |||
} | |||
/// <summary> | |||
/// Gets or sets whether Forward Error Correction is enabled. | |||
/// </summary> | |||
public bool ForwardErrorCorrection | |||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | |||
public void SetForwardErrorCorrection(bool value) | |||
{ | |||
get | |||
{ | |||
if (_encoder == IntPtr.Zero) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
int fec; | |||
int ret = API.opus_encoder_ctl(_encoder, Ctl.GetInbandFECRequest, out fec); | |||
if (ret < 0) | |||
throw new Exception("Encoder error - " + ((Errors)ret).ToString()); | |||
return fec > 0; | |||
} | |||
set | |||
{ | |||
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 - " + ((Errors)ret).ToString()); | |||
} | |||
} | |||
if (_encoder == IntPtr.Zero) | |||
throw new ObjectDisposedException("OpusEncoder"); | |||
~OpusEncoder() | |||
{ | |||
Dispose(); | |||
var ret = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0); | |||
if (ret < 0) | |||
throw new Exception("Encoder error - " + ((Error)ret).ToString()); | |||
} | |||
private bool disposed; | |||
@@ -187,12 +97,13 @@ namespace Opus.Net | |||
GC.SuppressFinalize(this); | |||
if (_encoder != IntPtr.Zero) | |||
{ | |||
API.opus_encoder_destroy(_encoder); | |||
_encoder = IntPtr.Zero; | |||
} | |||
disposed = true; | |||
} | |||
~OpusEncoder() | |||
{ | |||
Dispose(); | |||
} | |||
} | |||
} |