Browse Source

Started porting audio code from 0.9

pull/108/head
RogueException 9 years ago
parent
commit
c1a0761279
11 changed files with 377 additions and 4 deletions
  1. +119
    -0
      src/Discord.Net.Audio/AudioClient.cs
  2. +9
    -0
      src/Discord.Net.Audio/AudioMode.cs
  3. +1
    -2
      src/Discord.Net.Audio/Discord.Net.Audio.xproj
  4. +23
    -0
      src/Discord.Net.Audio/LibSodium.cs
  5. +10
    -0
      src/Discord.Net.Audio/Opus/Ctl.cs
  6. +9
    -0
      src/Discord.Net.Audio/Opus/OpusApplication.cs
  7. +41
    -0
      src/Discord.Net.Audio/Opus/OpusConverter.cs
  8. +48
    -0
      src/Discord.Net.Audio/Opus/OpusDecoder.cs
  9. +100
    -0
      src/Discord.Net.Audio/Opus/OpusEncoder.cs
  10. +14
    -0
      src/Discord.Net.Audio/Opus/OpusError.cs
  11. +3
    -2
      src/Discord.Net.Audio/project.json

+ 119
- 0
src/Discord.Net.Audio/AudioClient.cs View File

@@ -0,0 +1,119 @@
using Discord.API;
using Discord.API.Voice;
using Discord.Net.Converters;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio
{
public class AudioClient
{
public const int MaxBitrate = 128;

private const string Mode = "xsalsa20_poly1305";

private readonly JsonSerializer _serializer;
private readonly IWebSocketClient _gatewayClient;
private readonly SemaphoreSlim _connectionLock;
private CancellationTokenSource _connectCancelToken;

public ConnectionState ConnectionState { get; private set; }

internal AudioClient(WebSocketProvider provider, JsonSerializer serializer = null)
{
_connectionLock = new SemaphoreSlim(1, 1);

_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
}

public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null)
{
byte[] bytes = null;
payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload };
if (payload != null)
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));
//TODO: Send
return Task.CompletedTask;
}

//Gateway
public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null)
{
await SendAsync(VoiceOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false);
}


public async Task ConnectAsync(string url)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync(url).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternalAsync(string url)
{
ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken = new CancellationTokenSource();
_gatewayClient.SetCancelToken(_connectCancelToken.Token);
await _gatewayClient.ConnectAsync(url).ConfigureAwait(false);

ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
await DisconnectInternalAsync().ConfigureAwait(false);
throw;
}
}

public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync()
{
if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); }
catch { }

await _gatewayClient.DisconnectAsync().ConfigureAwait(false);

ConnectionState = ConnectionState.Disconnected;
}

//Helpers
private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
private string SerializeJson(object value)
{
var sb = new StringBuilder(256);
using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture))
using (JsonWriter writer = new JsonTextWriter(text))
_serializer.Serialize(writer, value);
return sb.ToString();
}
private T DeserializeJson<T>(Stream jsonStream)
{
using (TextReader text = new StreamReader(jsonStream))
using (JsonReader reader = new JsonTextReader(text))
return _serializer.Deserialize<T>(reader);
}
}
}

+ 9
- 0
src/Discord.Net.Audio/AudioMode.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Audio
{
public enum AudioMode : byte
{
Outgoing = 1,
Incoming = 2,
Both = Outgoing | Incoming
}
}

+ 1
- 2
src/Discord.Net.Audio/Discord.Net.Audio.xproj View File

@@ -7,11 +7,10 @@
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>ddfcc44f-934e-478a-978c-69cdda2a1c5b</ProjectGuid>
<RootNamespace>Discord.Net.Audio</RootNamespace>
<RootNamespace>Discord.Audio</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>

<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>


+ 23
- 0
src/Discord.Net.Audio/LibSodium.cs View File

@@ -0,0 +1,23 @@
using System.Runtime.InteropServices;

namespace Discord.Net.Audio
{
public unsafe static class LibSodium
{
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)]
private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret);
[DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)]
private static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret);

public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
{
fixed (byte* outPtr = output)
return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret);
}
public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret)
{
fixed (byte* inPtr = input)
return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret);
}
}
}

+ 10
- 0
src/Discord.Net.Audio/Opus/Ctl.cs View File

@@ -0,0 +1,10 @@
namespace Discord.Audio.Opus
{
internal enum Ctl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
}

+ 9
- 0
src/Discord.Net.Audio/Opus/OpusApplication.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Audio.Opus
{
internal enum OpusApplication : int
{
Voice = 2048,
MusicOrMixed = 2049,
LowLatency = 2051
}
}

+ 41
- 0
src/Discord.Net.Audio/Opus/OpusConverter.cs View File

@@ -0,0 +1,41 @@
using System;

namespace Discord.Audio.Opus
{
internal abstract class OpusConverter : IDisposable
{
protected IntPtr _ptr;

/// <summary> Gets the bit rate of this converter. </summary>
public const int BitsPerSample = 16;
/// <summary> Gets the input sampling rate of this converter. </summary>
public int SamplingRate { get; }

protected OpusConverter(int samplingRate)
{
if (samplingRate != 8000 && samplingRate != 12000 &&
samplingRate != 16000 && samplingRate != 24000 &&
samplingRate != 48000)
throw new ArgumentOutOfRangeException(nameof(samplingRate));

SamplingRate = samplingRate;
}
private bool disposedValue = false; // To detect redundant calls

protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
disposedValue = true;
}
~OpusConverter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

+ 48
- 0
src/Discord.Net.Audio/Opus/OpusDecoder.cs View File

@@ -0,0 +1,48 @@
using System;
using System.Runtime.InteropServices;

namespace Discord.Audio.Opus
{
internal unsafe class OpusDecoder : OpusConverter
{
[DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error);
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyDecoder(IntPtr decoder);
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
private static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec);

public OpusDecoder(int samplingRate)
: base(samplingRate)
{
OpusError error;
_ptr = CreateDecoder(samplingRate, 2, out error);
if (error != OpusError.OK)
throw new InvalidOperationException($"Error occured while creating decoder: {error}");
}
/// <summary> Produces PCM samples from Opus-encoded audio. </summary>
/// <param name="input">PCM samples to decode.</param>
/// <param name="inputOffset">Offset of the frame in input.</param>
/// <param name="output">Buffer to store the decoded frame.</param>
public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output)
{
int result = 0;
fixed (byte* inPtr = input)
result = Decode(_ptr, inPtr + inputOffset, inputCount, output, inputCount, 0);

if (result < 0)
throw new Exception(((OpusError)result).ToString());
return result;
}

protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
DestroyDecoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

+ 100
- 0
src/Discord.Net.Audio/Opus/OpusEncoder.cs View File

@@ -0,0 +1,100 @@
using System;
using System.Runtime.InteropServices;

namespace Discord.Audio.Opus
{
internal unsafe class OpusEncoder : OpusConverter
{
[DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error);
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyEncoder(IntPtr encoder);
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes);
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
private static extern int EncoderCtl(IntPtr st, Ctl request, int value);

/// <summary> Gets the bit rate in kbit/s. </summary>
public int? BitRate { get; }
/// <summary> Gets the coding mode of the encoder. </summary>
public OpusApplication Application { get; }
/// <summary> Gets the number of channels of this converter. </summary>
public int InputChannels { get; }
/// <summary> Gets the milliseconds per frame. </summary>
public int FrameMilliseconds { get; }

/// <summary> Gets the bytes per sample. </summary>
public int SampleSize => (BitsPerSample / 8) * InputChannels;
/// <summary> Gets the number of samples per frame. </summary>
public int SamplesPerFrame => SamplingRate / 1000 * FrameMilliseconds;
/// <summary> Gets the bytes per frame. </summary>
public int FrameSize => SamplesPerFrame * SampleSize;

public OpusEncoder(int samplingRate, int channels, int frameMillis,
int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed)
: base(samplingRate)
{
if (channels != 1 && channels != 2)
throw new ArgumentOutOfRangeException(nameof(channels));
if (bitrate != null && (bitrate < 1 || bitrate > AudioClient.MaxBitrate))
throw new ArgumentOutOfRangeException(nameof(bitrate));

OpusError error;
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
if (error != OpusError.OK)
throw new InvalidOperationException($"Error occured while creating encoder: {error}");


BitRate = bitrate;
Application = application;
InputChannels = channels;
FrameMilliseconds = frameMillis;

SetForwardErrorCorrection(true);
if (bitrate != null)
SetBitrate(bitrate.Value);
}


/// <summary> Produces Opus encoded audio from PCM samples. </summary>
/// <param name="input">PCM samples to encode.</param>
/// <param name="inputOffset">Offset of the frame in pcmSamples.</param>
/// <param name="output">Buffer to store the encoded frame.</param>
/// <returns>Length of the frame contained in outputBuffer.</returns>
public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output)
{
int result = 0;
fixed (byte* inPtr = input)
result = Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length);

if (result < 0)
throw new Exception(((OpusError)result).ToString());
return result;
}

/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetForwardErrorCorrection(bool value)
{
var result = EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception(((OpusError)result).ToString());
}

/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetBitrate(int value)
{
var result = EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000);
if (result < 0)
throw new Exception(((OpusError)result).ToString());
}

protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
DestroyEncoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

+ 14
- 0
src/Discord.Net.Audio/Opus/OpusError.cs View File

@@ -0,0 +1,14 @@
namespace Discord.Audio.Opus
{
internal enum OpusError : int
{
OK = 0,
BadArg = -1,
BufferToSmall = -2,
InternalError = -3,
InvalidPacket = -4,
Unimplemented = -5,
InvalidState = -6,
AllocFail = -7
}
}

+ 3
- 2
src/Discord.Net.Audio/project.json View File

@@ -1,4 +1,4 @@
{
{
"version": "1.0.0-dev",
"description": "A Discord.Net extension adding audio support.",
"authors": [ "RogueException" ],
@@ -19,7 +19,8 @@
},

"dependencies": {
"Discord.Net": "1.0.0-dev"
"Discord.Net": "1.0.0-dev",
"System.Runtime.InteropServices": "4.1.0-rc2-24027"
},

"frameworks": {


Loading…
Cancel
Save