Started porting audio code from 0.9
This commit is contained in:
119
src/Discord.Net.Audio/AudioClient.cs
Normal file
119
src/Discord.Net.Audio/AudioClient.cs
Normal 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
src/Discord.Net.Audio/AudioMode.cs
Normal file
9
src/Discord.Net.Audio/AudioMode.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
public enum AudioMode : byte
|
||||||
|
{
|
||||||
|
Outgoing = 1,
|
||||||
|
Incoming = 2,
|
||||||
|
Both = Outgoing | Incoming
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,10 @@
|
|||||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
<PropertyGroup Label="Globals">
|
<PropertyGroup Label="Globals">
|
||||||
<ProjectGuid>ddfcc44f-934e-478a-978c-69cdda2a1c5b</ProjectGuid>
|
<ProjectGuid>ddfcc44f-934e-478a-978c-69cdda2a1c5b</ProjectGuid>
|
||||||
<RootNamespace>Discord.Net.Audio</RootNamespace>
|
<RootNamespace>Discord.Audio</RootNamespace>
|
||||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
23
src/Discord.Net.Audio/LibSodium.cs
Normal file
23
src/Discord.Net.Audio/LibSodium.cs
Normal 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
src/Discord.Net.Audio/Opus/Ctl.cs
Normal file
10
src/Discord.Net.Audio/Opus/Ctl.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Discord.Audio.Opus
|
||||||
|
{
|
||||||
|
internal enum Ctl : int
|
||||||
|
{
|
||||||
|
SetBitrateRequest = 4002,
|
||||||
|
GetBitrateRequest = 4003,
|
||||||
|
SetInbandFECRequest = 4012,
|
||||||
|
GetInbandFECRequest = 4013
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/Discord.Net.Audio/Opus/OpusApplication.cs
Normal file
9
src/Discord.Net.Audio/Opus/OpusApplication.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Audio.Opus
|
||||||
|
{
|
||||||
|
internal enum OpusApplication : int
|
||||||
|
{
|
||||||
|
Voice = 2048,
|
||||||
|
MusicOrMixed = 2049,
|
||||||
|
LowLatency = 2051
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/Discord.Net.Audio/Opus/OpusConverter.cs
Normal file
41
src/Discord.Net.Audio/Opus/OpusConverter.cs
Normal 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
src/Discord.Net.Audio/Opus/OpusDecoder.cs
Normal file
48
src/Discord.Net.Audio/Opus/OpusDecoder.cs
Normal 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
src/Discord.Net.Audio/Opus/OpusEncoder.cs
Normal file
100
src/Discord.Net.Audio/Opus/OpusEncoder.cs
Normal 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
src/Discord.Net.Audio/Opus/OpusError.cs
Normal file
14
src/Discord.Net.Audio/Opus/OpusError.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0-dev",
|
"version": "1.0.0-dev",
|
||||||
"description": "A Discord.Net extension adding audio support.",
|
"description": "A Discord.Net extension adding audio support.",
|
||||||
"authors": [ "RogueException" ],
|
"authors": [ "RogueException" ],
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Discord.Net": "1.0.0-dev"
|
"Discord.Net": "1.0.0-dev",
|
||||||
|
"System.Runtime.InteropServices": "4.1.0-rc2-24027"
|
||||||
},
|
},
|
||||||
|
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
|
|||||||
Reference in New Issue
Block a user