Browse Source

Initial outgoing audio support

tags/docs-0.9
Brandon Smith 9 years ago
parent
commit
74517d0ef3
10 changed files with 241 additions and 370 deletions
  1. +11
    -0
      src/Discord.Net/API/Models/VoiceWebSocketCommands.cs
  2. +10
    -0
      src/Discord.Net/API/Models/VoiceWebSocketEvents.cs
  3. +7
    -4
      src/Discord.Net/DiscordClient.cs
  4. +2
    -2
      src/Discord.Net/DiscordTextWebSocket.cs
  5. +127
    -60
      src/Discord.Net/DiscordVoiceSocket.cs
  6. +4
    -2
      src/Discord.Net/DiscordWebSocket.cs
  7. +0
    -3
      src/Opus.Net.Net45/Opus.Net.csproj
  8. +17
    -14
      src/Opus.Net/API.cs
  9. +0
    -133
      src/Opus.Net/OpusDecoder.cs
  10. +63
    -152
      src/Opus.Net/OpusEncoder.cs

+ 11
- 0
src/Discord.Net/API/Models/VoiceWebSocketCommands.cs View File

@@ -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;
}
}
}
}

+ 10
- 0
src/Discord.Net/API/Models/VoiceWebSocketEvents.cs View File

@@ -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;
}
}
}

+ 7
- 4
src/Discord.Net/DiscordClient.cs View File

@@ -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



+ 2
- 2
src/Discord.Net/DiscordTextWebSocket.cs View File

@@ -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);


+ 127
- 60
src/Discord.Net/DiscordVoiceSocket.cs View File

@@ -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



+ 4
- 2
src/Discord.Net/DiscordWebSocket.cs View File

@@ -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[]>();


+ 0
- 3
src/Opus.Net.Net45/Opus.Net.csproj View File

@@ -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>


+ 17
- 14
src/Opus.Net/API.cs View File

@@ -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.


+ 0
- 133
src/Opus.Net/OpusDecoder.cs View File

@@ -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;
}
}
}

+ 63
- 152
src/Opus.Net/OpusEncoder.cs View File

@@ -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();
}
}
}

Loading…
Cancel
Save