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)' != ''" />
|
||||
<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
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",
|
||||
"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": {
|
||||
|
||||
Reference in New Issue
Block a user