Initial outgoing audio support
This commit is contained in:
@@ -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 && !_isReady)
|
||||
{
|
||||
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)
|
||||
{
|
||||
while (_sendQueue.TryDequeue(out bytes))
|
||||
await _udp.SendAsync(bytes, bytes.Length);
|
||||
await Task.Delay(_sendInterval);
|
||||
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)]
|
||||
public static extern IntPtr opus_decoder_create(int Fs, int channels, out Errors error);
|
||||
|
||||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
|
||||
internal static extern IntPtr opus_decoder_create(int Fs, int channels, out IntPtr error);
|
||||
public static extern void opus_decoder_destroy(IntPtr decoder);
|
||||
|
||||
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
|
||||
internal static extern void opus_decoder_destroy(IntPtr decoder);
|
||||
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_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);
|
||||
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, 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);
|
||||
}
|
||||
|
||||
private IntPtr _encoder;
|
||||
|
||||
private OpusEncoder(IntPtr encoder, int inputSamplingRate, int inputChannels, Application application)
|
||||
{
|
||||
_encoder = encoder;
|
||||
InputSamplingRate = inputSamplingRate;
|
||||
InputChannels = inputChannels;
|
||||
InputSamplingRate = samplingRate;
|
||||
InputChannels = channels;
|
||||
Application = application;
|
||||
MaxDataBytes = 4000;
|
||||
FrameLength = frameLength;
|
||||
SampleSize = (BitRate / 8) * channels;
|
||||
SamplesPerFrame = samplingRate / 1000 * FrameLength;
|
||||
FrameSize = SamplesPerFrame * SampleSize;
|
||||
|
||||
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());
|
||||
|
||||
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;
|
||||
throw new Exception("Encoding failed: " + ((Error)length).ToString());
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of frames in the PCM samples.
|
||||
/// </summary>
|
||||
/// <param name="pcmSamples"></param>
|
||||
/// <returns></returns>
|
||||
public int FrameCount(byte[] pcmSamples)
|
||||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
|
||||
public void SetForwardErrorCorrection(bool value)
|
||||
{
|
||||
// seems like bitrate should be required
|
||||
int bitrate = 16;
|
||||
int bytesPerSample = (bitrate / 8) * InputChannels;
|
||||
return pcmSamples.Length / bytesPerSample;
|
||||
}
|
||||
if (_encoder == IntPtr.Zero)
|
||||
throw new ObjectDisposedException("OpusEncoder");
|
||||
|
||||
/// <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());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether Forward Error Correction is enabled.
|
||||
/// </summary>
|
||||
public bool ForwardErrorCorrection
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
~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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user