Browse Source

Initial commit of audio isolation

tags/docs-0.9
RogueException 9 years ago
parent
commit
1f3796ff96
19 changed files with 700 additions and 51 deletions
  1. +96
    -0
      src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj
  2. +18
    -0
      src/Discord.Net.Audio.Net5/Properties/AssemblyInfo.cs
  3. +95
    -0
      src/Discord.Net.Audio/API/Voice.cs
  4. +8
    -0
      src/Discord.Net.Audio/AudioExtensions.cs
  5. +199
    -0
      src/Discord.Net.Audio/AudioService.cs
  6. +57
    -0
      src/Discord.Net.Audio/AudioServiceConfig.cs
  7. +21
    -0
      src/Discord.Net.Audio/Discord.Net.Audio.xproj
  8. +143
    -0
      src/Discord.Net.Audio/DiscordAudioClient.cs
  9. +5
    -4
      src/Discord.Net.Audio/Net/VoiceWebSocket.Events.cs
  10. +30
    -36
      src/Discord.Net.Audio/Net/VoiceWebSocket.cs
  11. +0
    -0
      src/Discord.Net.Audio/Opus.cs
  12. +0
    -0
      src/Discord.Net.Audio/OpusDecoder.cs
  13. +0
    -0
      src/Discord.Net.Audio/OpusEncoder.cs
  14. +0
    -0
      src/Discord.Net.Audio/Sodium.cs
  15. +1
    -1
      src/Discord.Net.Audio/VoiceBuffer.cs
  16. +0
    -0
      src/Discord.Net.Audio/libsodium.dll
  17. +0
    -0
      src/Discord.Net.Audio/opus.dll
  18. +27
    -0
      src/Discord.Net.Audio/project.json
  19. +0
    -10
      src/Discord.Net/Audio/IDiscordVoiceBuffer.cs

+ 96
- 0
src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7BFEF748-B934-4621-9B11-6302E3A9F6B3}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Discord.Audio</RootNamespace>
<AssemblyName>Discord.Net.Audio</AssemblyName>
<FileAlignment>512</FileAlignment>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<UseMSBuildEngine>False</UseMSBuildEngine>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>TRACE;DEBUG;NET45</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>6</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;NET45</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>6</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Discord.Net.Audio\AudioExtensions.cs">
<Link>AudioExtensions.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\AudioService.cs">
<Link>AudioService.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\AudioServiceConfig.cs">
<Link>AudioServiceConfig.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\DiscordVoiceClient.cs">
<Link>DiscordVoiceClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\IDiscordVoiceClient.cs">
<Link>IDiscordVoiceClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\Opus.cs">
<Link>Opus.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\OpusDecoder.cs">
<Link>OpusDecoder.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\OpusEncoder.cs">
<Link>OpusEncoder.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\Sodium.cs">
<Link>Sodium.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\VoiceBuffer.cs">
<Link>VoiceBuffer.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\VoiceWebSocket.cs">
<Link>VoiceWebSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\VoiceWebSocket.Events.cs">
<Link>VoiceWebSocket.Events.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Commands.Net45\Discord.Net.Commands.csproj">
<Project>{1b5603b4-6f8f-4289-b945-7baae523d740}</Project>
<Name>Discord.Net.Commands</Name>
</ProjectReference>
<ProjectReference Include="..\Discord.Net.Net45\Discord.Net.csproj">
<Project>{8d71a857-879a-4a10-859e-5ff824ed6688}</Project>
<Name>Discord.Net</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

+ 18
- 0
src/Discord.Net.Audio.Net5/Properties/AssemblyInfo.cs View File

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

[assembly: AssemblyTitle("Discord.Net.Audio")]
[assembly: AssemblyDescription("A Discord.Net extension adding voice support.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("RogueException")]
[assembly: AssemblyProduct("Discord.Net.Modules")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]

[assembly: AssemblyVersion("0.8.1.0")]
[assembly: AssemblyFileVersion("0.8.1.0")]


+ 95
- 0
src/Discord.Net.Audio/API/Voice.cs View File

@@ -0,0 +1,95 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169

using Discord.API.Converters;
using Newtonsoft.Json;

namespace Discord.API
{
//Commands
internal sealed class VoiceLoginCommand : WebSocketMessage<VoiceLoginCommand.Data>
{
public VoiceLoginCommand() : base(0) { }
public class Data
{
[JsonProperty("server_id")]
[JsonConverter(typeof(LongStringConverter))]
public long ServerId;
[JsonProperty("user_id")]
[JsonConverter(typeof(LongStringConverter))]
public long UserId;
[JsonProperty("session_id")]
public string SessionId;
[JsonProperty("token")]
public string Token;
}
}
internal sealed class VoiceLogin2Command : WebSocketMessage<VoiceLogin2Command.Data>
{
public VoiceLogin2Command() : base(1) { }
public class Data
{
public class SocketInfo
{
[JsonProperty("address")]
public string Address;
[JsonProperty("port")]
public int Port;
[JsonProperty("mode")]
public string Mode = "xsalsa20_poly1305";
}
[JsonProperty("protocol")]
public string Protocol = "udp";
[JsonProperty("data")]
public SocketInfo SocketData = new SocketInfo();
}
}
internal sealed class VoiceKeepAliveCommand : WebSocketMessage<long>
{
public VoiceKeepAliveCommand() : base(3, EpochTime.GetMilliseconds()) { }
}
internal sealed class IsTalkingCommand : WebSocketMessage<IsTalkingCommand.Data>
{
public IsTalkingCommand() : base(5) { }
public class Data
{
[JsonProperty("delay")]
public int Delay;
[JsonProperty("speaking")]
public bool IsSpeaking;
}
}

//Events
public class VoiceReadyEvent
{
[JsonProperty("ssrc")]
public uint SSRC;
[JsonProperty("port")]
public ushort Port;
[JsonProperty("modes")]
public string[] Modes;
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval;
}

public class JoinServerEvent
{
[JsonProperty("secret_key")]
public byte[] SecretKey;
[JsonProperty("mode")]
public string Mode;
}

public class IsTalkingEvent
{
[JsonProperty("user_id")]
[JsonConverter(typeof(LongStringConverter))]
public long UserId;
[JsonProperty("ssrc")]
public uint SSRC;
[JsonProperty("speaking")]
public bool IsSpeaking;
}
}

+ 8
- 0
src/Discord.Net.Audio/AudioExtensions.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Audio
{
public static class AudioExtensions
{
public static AudioService Audio(this DiscordClient client, bool required = true)
=> client.GetService<AudioService>(required);
}
}

+ 199
- 0
src/Discord.Net.Audio/AudioService.cs View File

@@ -0,0 +1,199 @@
using Discord.Net.WebSockets;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Audio
{
public class VoiceDisconnectedEventArgs : DisconnectedEventArgs
{
public readonly long ServerId;

public VoiceDisconnectedEventArgs(long serverId, DisconnectedEventArgs e)
: base(e.WasUnexpected, e.Error)
{
ServerId = serverId;
}
}
public class UserIsSpeakingEventArgs : UserEventArgs
{
public readonly bool IsSpeaking;

public UserIsSpeakingEventArgs(User user, bool isSpeaking)
: base(user)
{
IsSpeaking = isSpeaking;
}
}
public class VoicePacketEventArgs : EventArgs
{
public readonly long UserId;
public readonly long ChannelId;
public readonly byte[] Buffer;
public readonly int Offset;
public readonly int Count;

public VoicePacketEventArgs(long userId, long channelId, byte[] buffer, int offset, int count)
{
UserId = userId;
ChannelId = channelId;
Buffer = buffer;
Offset = offset;
Count = count;
}
}

public class AudioService : IService
{
private DiscordAudioClient _defaultClient;
private ConcurrentDictionary<long, DiscordAudioClient> _voiceClients;
private ConcurrentDictionary<User, bool> _talkingUsers;
private int _nextClientId;

internal DiscordClient Client => _client;
private DiscordClient _client;

public AudioServiceConfig Config => _config;
private readonly AudioServiceConfig _config;

public event EventHandler Connected;
private void RaiseConnected()
{
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler<VoiceDisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(long serverId, DisconnectedEventArgs e)
{
if (Disconnected != null)
Disconnected(this, new VoiceDisconnectedEventArgs(serverId, e));
}
public event EventHandler<VoicePacketEventArgs> OnPacket;
internal void RaiseOnPacket(VoicePacketEventArgs e)
{
if (OnPacket != null)
OnPacket(this, e);
}
public event EventHandler<UserIsSpeakingEventArgs> UserIsSpeakingUpdated;
private void RaiseUserIsSpeakingUpdated(User user, bool isSpeaking)
{
if (UserIsSpeakingUpdated != null)
UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking));
}

public AudioService(AudioServiceConfig config)
{
_config = config;
_config.Lock();
}
public void Install(DiscordClient client)
{
_client = client;
if (Config.EnableMultiserver)
_voiceClients = new ConcurrentDictionary<long, DiscordAudioClient>();
else
{
var logger = Client.Log().CreateLogger("Voice");
var voiceSocket = new VoiceWebSocket(Client.Config, _config, logger);
_defaultClient = new DiscordAudioClient(this, 0, logger, _client.WebSocket, voiceSocket);
}
_talkingUsers = new ConcurrentDictionary<User, bool>();

client.Disconnected += async (s, e) =>
{
if (Config.EnableMultiserver)
{
var tasks = _voiceClients
.Select(x => x.Value.Disconnect())
.ToArray();
await Task.WhenAll(tasks).ConfigureAwait(false);
_voiceClients.Clear();
}
foreach (var member in _talkingUsers)
{
bool ignored;
if (_talkingUsers.TryRemove(member.Key, out ignored))
RaiseUserIsSpeakingUpdated(member.Key, false);
}
};
}

public DiscordAudioClient GetVoiceClient(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));

if (!Config.EnableMultiserver)
{
if (server.Id == _defaultClient.ServerId)
return _defaultClient;
else
return null;
}

DiscordAudioClient client;
if (_voiceClients.TryGetValue(server.Id, out client))
return client;
else
return null;
}
private Task<DiscordAudioClient> CreateVoiceClient(Server server)
{
if (!Config.EnableMultiserver)
{
_defaultClient.SetServerId(server.Id);
return Task.FromResult(_defaultClient);
}

var client = _voiceClients.GetOrAdd(server.Id, _ =>
{
int id = unchecked(++_nextClientId);
var logger = Client.Log().CreateLogger($"Voice #{id}");
DataWebSocket dataSocket = null;
var voiceSocket = new VoiceWebSocket(Client.Config, _config, logger);
var voiceClient = new DiscordAudioClient(this, id, logger, dataSocket, voiceSocket);
voiceClient.SetServerId(server.Id);

voiceSocket.OnPacket += (s, e) =>
{
RaiseOnPacket(e);
};
voiceSocket.IsSpeaking += (s, e) =>
{
var user = Client.GetUser(server, e.UserId);
RaiseUserIsSpeakingUpdated(user, e.IsSpeaking);
};

return voiceClient;
});
//await client.Connect(dataSocket.Host, _client.Token).ConfigureAwait(false);
return Task.FromResult(client);
}

public async Task<DiscordAudioClient> JoinVoiceServer(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
//CheckReady(true);

var client = await CreateVoiceClient(channel.Server).ConfigureAwait(false);
await client.Join(channel).ConfigureAwait(false);
return client;
}

public async Task LeaveVoiceServer(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
//CheckReady(true);

if (Config.EnableMultiserver)
{
//client.CheckReady();
DiscordAudioClient client;
if (_voiceClients.TryRemove(server.Id, out client))
await client.Disconnect().ConfigureAwait(false);
}
else
await _defaultClient.Disconnect().ConfigureAwait(false);
}
}
}

+ 57
- 0
src/Discord.Net.Audio/AudioServiceConfig.cs View File

@@ -0,0 +1,57 @@

using System;

namespace Discord.Audio
{
public enum AudioMode : byte
{
Outgoing = 1,
Incoming = 2,
Both = Outgoing | Incoming
}

public class AudioServiceConfig
{
/// <summary> Max time in milliseconds to wait for DiscordAudioClient to connect and initialize. </summary>
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } }
private int _connectionTimeout = 30000;

//Experimental Features
/// <summary> (Experimental) Enables the voice websocket and UDP client and specifies how it will be used. </summary>
public AudioMode Mode { get { return _mode; } set { SetValue(ref _mode, value); } }
private AudioMode _mode = AudioMode.Outgoing;

/// <summary> (Experimental) Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. </summary>
public bool EnableEncryption { get { return _enableEncryption; } set { SetValue(ref _enableEncryption, value); } }
private bool _enableEncryption = true;

/// <summary> (Experimental) Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). </summary>
public bool EnableMultiserver { get { return _enableMultiserver; } set { SetValue(ref _enableMultiserver, value); } }
private bool _enableMultiserver = false;

/// <summary> Gets or sets the max buffer length (in milliseconds) for outgoing voice packets. This value is the target maximum but is not guaranteed, the buffer will often go slightly above this value. </summary>
public int BufferLength { get { return _bufferLength; } set { SetValue(ref _bufferLength, value); } }
private int _bufferLength = 1000;

/// <summary> Gets or sets the bitrate used (in kbit/s, between 1 and 512 inclusively) for outgoing voice packets. A null value will use default Opus settings. </summary>
public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } }
private int? _bitrate = null;

//Lock
protected bool _isLocked;
internal void Lock() { _isLocked = true; }
protected void SetValue<T>(ref T storage, T value)
{
if (_isLocked)
throw new InvalidOperationException("Unable to modify a service's configuration after it has been created.");
storage = value;
}

public AudioServiceConfig Clone()
{
var config = MemberwiseClone() as AudioServiceConfig;
config._isLocked = false;
return config;
}
}
}

+ 21
- 0
src/Discord.Net.Audio/Discord.Net.Audio.xproj View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>dff7afe3-ca77-4109-bade-b4b49a4f6648</ProjectGuid>
<RootNamespace>Discord.Audio</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

+ 143
- 0
src/Discord.Net.Audio/DiscordAudioClient.cs View File

@@ -0,0 +1,143 @@
using Discord.Net.WebSockets;
using System;
using System.Threading.Tasks;

namespace Discord.Audio
{
public partial class DiscordAudioClient
{
private readonly int _id;
public int Id => _id;

private readonly AudioService _service;
private readonly DataWebSocket _dataSocket;
private readonly VoiceWebSocket _voiceSocket;
private readonly Logger _logger;

public long? ServerId => _voiceSocket.ServerId;
public long? ChannelId => _voiceSocket.ChannelId;

public DiscordAudioClient(AudioService service, int id, Logger logger, DataWebSocket dataSocket, VoiceWebSocket voiceSocket)
{
_service = service;
_id = id;
_logger = logger;
_dataSocket = dataSocket;
_voiceSocket = voiceSocket;

/*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected();
_voiceSocket.Disconnected += async (s, e) =>
{
_voiceSocket.CurrentServerId;
if (voiceServerId != null)
_dataSocket.SendLeaveVoice(voiceServerId.Value);
await _voiceSocket.Disconnect().ConfigureAwait(false);
RaiseVoiceDisconnected(socket.CurrentServerId.Value, e);
if (e.WasUnexpected)
await socket.Reconnect().ConfigureAwait(false);
};*/

/*_voiceSocket.IsSpeaking += (s, e) =>
{
if (_voiceSocket.State == WebSocketState.Connected)
{
var user = _users[e.UserId, socket.CurrentServerId];
bool value = e.IsSpeaking;
if (user.IsSpeaking != value)
{
user.IsSpeaking = value;
var channel = _channels[_voiceSocket.CurrentChannelId];
RaiseUserIsSpeaking(user, channel, value);
if (Config.TrackActivity)
user.UpdateActivity();
}
}
};*/

/*this.Connected += (s, e) =>
{
_voiceSocket.ParentCancelToken = _cancelToken;
};*/

_dataSocket.ReceivedEvent += async (s, e) =>
{
try
{
switch (e.Type)
{
case "VOICE_SERVER_UPDATE":
{
long serverId = IdConvert.ToLong(e.Payload.Value<string>("guild_id"));

if (serverId == ServerId)
{
var client = _service.Client;
string token = e.Payload.Value<string>("token");
_voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0];
await _voiceSocket.Login(client.CurrentUser.Id, _dataSocket.SessionId, token, client.CancelToken).ConfigureAwait(false);
}
}
break;
}
}
catch (Exception ex)
{
_dataSocket.Logger.Log(LogSeverity.Error, $"Error handling {e.Type} event", ex);
}
};
}
public Task Disconnect()
{
return _voiceSocket.Disconnect();
}

internal void SetServerId(long serverId)
{
_voiceSocket.ServerId = serverId;
}
public async Task Join(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
long? serverId = channel.Server?.Id;
if (serverId != ServerId)
throw new InvalidOperationException("Cannot join a channel on a different server than this voice client.");
//CheckReady(checkVoice: true);

await _voiceSocket.Disconnect().ConfigureAwait(false);
_voiceSocket.ChannelId = channel.Id;
_dataSocket.SendJoinVoice(channel.Server.Id, channel.Id);
await _voiceSocket.WaitForConnection(_service.Config.ConnectionTimeout).ConfigureAwait(false);
}

/// <summary> Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. </summary>
/// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param>
/// <param name="count">Number of bytes in this frame. </param>
public void Send(byte[] data, int count)
{
if (data == null) throw new ArgumentException(nameof(data));
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
//CheckReady(checkVoice: true);

if (count != 0)
_voiceSocket.SendPCMFrames(data, count);
}

/// <summary> Clears the PCM buffer. </summary>
public void Clear()
{
//CheckReady(checkVoice: true);

_voiceSocket.ClearPCMFrames();
}

/// <summary> Returns a task that completes once the voice output buffer is empty. </summary>
public Task Wait()
{
//CheckReady(checkVoice: true);

_voiceSocket.WaitForQueue();
return TaskHelper.CompletedTask;
}
}
}

src/Discord.Net/Net/WebSockets/VoiceWebSocket.Events.cs → src/Discord.Net.Audio/Net/VoiceWebSocket.Events.cs View File

@@ -1,4 +1,5 @@
using System;
using Discord.Audio;
using System;


namespace Discord.Net.WebSockets namespace Discord.Net.WebSockets
{ {
@@ -13,16 +14,16 @@ namespace Discord.Net.WebSockets
} }
} }


internal partial class VoiceWebSocket
public partial class VoiceWebSocket
{ {
public event EventHandler<IsTalkingEventArgs> IsSpeaking;
internal event EventHandler<IsTalkingEventArgs> IsSpeaking;
private void RaiseIsSpeaking(long userId, bool isSpeaking) private void RaiseIsSpeaking(long userId, bool isSpeaking)
{ {
if (IsSpeaking != null) if (IsSpeaking != null)
IsSpeaking(this, new IsTalkingEventArgs(userId, isSpeaking)); IsSpeaking(this, new IsTalkingEventArgs(userId, isSpeaking));
} }


public event EventHandler<VoicePacketEventArgs> OnPacket;
internal event EventHandler<VoicePacketEventArgs> OnPacket;
internal void RaiseOnPacket(long userId, long channelId, byte[] buffer, int offset, int count) internal void RaiseOnPacket(long userId, long channelId, byte[] buffer, int offset, int count)
{ {
if (OnPacket != null) if (OnPacket != null)

src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs → src/Discord.Net.Audio/Net/VoiceWebSocket.cs View File

@@ -15,7 +15,7 @@ using System.Threading.Tasks;


namespace Discord.Net.WebSockets namespace Discord.Net.WebSockets
{ {
internal partial class VoiceWebSocket : WebSocket
public partial class VoiceWebSocket : WebSocket
{ {
public enum OpCodes : byte public enum OpCodes : byte
{ {
@@ -33,8 +33,9 @@ namespace Discord.Net.WebSockets


//private readonly Random _rand; //private readonly Random _rand;
private readonly int _targetAudioBufferLength; private readonly int _targetAudioBufferLength;
private OpusEncoder _encoder;
private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders;
private readonly AudioServiceConfig _audioConfig;
private OpusEncoder _encoder;
private uint _ssrc; private uint _ssrc;
private ConcurrentDictionary<uint, long> _ssrcMapping; private ConcurrentDictionary<uint, long> _ssrcMapping;


@@ -49,31 +50,24 @@ namespace Discord.Net.WebSockets
private int _ping; private int _ping;
private Thread _sendThread, _receiveThread; private Thread _sendThread, _receiveThread;

public long? CurrentServerId => _serverId;
public long? CurrentChannelId => _channelId;
public VoiceBuffer OutputBuffer => _sendBuffer;
public long? ServerId { get { return _serverId; } internal set { _serverId = value; } }
public long? ChannelId { get { return _channelId; } internal set { _channelId = value; } }
public int Ping => _ping; public int Ping => _ping;
internal VoiceBuffer OutputBuffer => _sendBuffer;


public VoiceWebSocket(DiscordWSClient client)
: base(client)
public VoiceWebSocket(DiscordConfig config, AudioServiceConfig audioConfig, Logger logger)
: base(config, logger)
{ {
//_rand = new Random();
_audioConfig = audioConfig;
_decoders = new ConcurrentDictionary<uint, OpusDecoder>(); _decoders = new ConcurrentDictionary<uint, OpusDecoder>();
_targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames
_targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames
_encodingBuffer = new byte[MaxOpusSize]; _encodingBuffer = new byte[MaxOpusSize];
_ssrcMapping = new ConcurrentDictionary<uint, long>(); _ssrcMapping = new ConcurrentDictionary<uint, long>();
_encoder = new OpusEncoder(48000, 1, 20, client.Config.VoiceBitrate, Opus.Application.Audio);
_sendBuffer = new VoiceBuffer((int)Math.Ceiling(client.Config.VoiceBufferLength / (double)_encoder.FrameLength), _encoder.FrameSize);
}

public Task SetChannel(long serverId, long channelId)
{
_serverId = serverId;
_channelId = channelId;

return base.BeginConnect();
_encoder = new OpusEncoder(48000, 1, 20, _audioConfig.Bitrate, Opus.Application.Audio);
_sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize);
} }
public async Task Login(long userId, string sessionId, string token, CancellationToken cancelToken) public async Task Login(long userId, string sessionId, string token, CancellationToken cancelToken)
{ {
if ((WebSocketState)_state == WebSocketState.Connected) if ((WebSocketState)_state == WebSocketState.Connected)
@@ -94,7 +88,7 @@ namespace Discord.Net.WebSockets
try try
{ {
var cancelToken = ParentCancelToken.Value; var cancelToken = ParentCancelToken.Value;
await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false);
await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false);
while (!cancelToken.IsCancellationRequested) while (!cancelToken.IsCancellationRequested)
{ {
try try
@@ -107,9 +101,9 @@ namespace Discord.Net.WebSockets
catch (OperationCanceledException) { throw; } catch (OperationCanceledException) { throw; }
catch (Exception ex) catch (Exception ex)
{ {
RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}");
_logger.Log(LogSeverity.Error, "Reconnect failed", ex);
//Net is down? We can keep trying to reconnect until the user runs Disconnect() //Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false);
await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false);
} }
} }
} }
@@ -128,7 +122,7 @@ namespace Discord.Net.WebSockets
QueueMessage(msg); QueueMessage(msg);


List<Task> tasks = new List<Task>(); List<Task> tasks = new List<Task>();
if ((_client.Config.VoiceMode & DiscordVoiceMode.Outgoing) != 0)
if ((_audioConfig.Mode & AudioMode.Outgoing) != 0)
{ {
_sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken)));
_sendThread.IsBackground = true; _sendThread.IsBackground = true;
@@ -136,7 +130,7 @@ namespace Discord.Net.WebSockets
} }
//This thread is required to establish a connection even if we're outgoing only //This thread is required to establish a connection even if we're outgoing only
if ((_client.Config.VoiceMode & DiscordVoiceMode.Incoming) != 0)
if ((_audioConfig.Mode & AudioMode.Incoming) != 0)
{ {
_receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken)));
_receiveThread.IsBackground = true; _receiveThread.IsBackground = true;
@@ -161,7 +155,7 @@ namespace Discord.Net.WebSockets
return new Task[] { Task.WhenAll(tasks.ToArray()) }; return new Task[] { Task.WhenAll(tasks.ToArray()) };
} }
protected override Task Cleanup()
protected override Task Stop()
{ {
if (_sendThread != null) if (_sendThread != null)
_sendThread.Join(); _sendThread.Join();
@@ -186,7 +180,7 @@ namespace Discord.Net.WebSockets
} }
_udp = null; _udp = null;


return base.Cleanup();
return base.Stop();
} }
private void ReceiveVoiceAsync(CancellationToken cancelToken) private void ReceiveVoiceAsync(CancellationToken cancelToken)
@@ -198,7 +192,7 @@ namespace Discord.Net.WebSockets
int packetLength, resultOffset, resultLength; int packetLength, resultOffset, resultLength;
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);


if ((_client.Config.VoiceMode & DiscordVoiceMode.Incoming) != 0)
if ((_audioConfig.Mode & AudioMode.Incoming) != 0)
{ {
decodingBuffer = new byte[MaxOpusSize]; decodingBuffer = new byte[MaxOpusSize];
nonce = new byte[24]; nonce = new byte[24];
@@ -239,7 +233,7 @@ namespace Discord.Net.WebSockets
login2.Payload.SocketData.Mode = _encryptionMode; login2.Payload.SocketData.Mode = _encryptionMode;
login2.Payload.SocketData.Port = port; login2.Payload.SocketData.Port = port;
QueueMessage(login2); QueueMessage(login2);
if ((_client.Config.VoiceMode & DiscordVoiceMode.Incoming) == 0)
if ((_audioConfig.Mode & AudioMode.Incoming) == 0)
return; return;
} }
else else
@@ -404,7 +398,7 @@ namespace Discord.Net.WebSockets
} }
catch (SocketException ex) catch (SocketException ex)
{ {
RaiseOnLog(LogMessageSeverity.Error, "Failed to send UDP packet.", ex);
_logger.Log(LogSeverity.Error, "Failed to send UDP packet.", ex);
} }
hasFrame = false; hasFrame = false;
} }
@@ -454,12 +448,12 @@ namespace Discord.Net.WebSockets
{ {
if (_state != (int)WebSocketState.Connected) if (_state != (int)WebSocketState.Connected)
{ {
var payload = (msg.Payload as JToken).ToObject<VoiceReadyEvent>(_client.VoiceSocketSerializer);
var payload = (msg.Payload as JToken).ToObject<VoiceReadyEvent>(_serializer);
_heartbeatInterval = payload.HeartbeatInterval; _heartbeatInterval = payload.HeartbeatInterval;
_ssrc = payload.SSRC; _ssrc = payload.SSRC;
_endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port);
if (_client.Config.EnableVoiceEncryption)
if (_audioConfig.EnableEncryption)
{ {
if (payload.Modes.Contains(EncryptedMode)) if (payload.Modes.Contains(EncryptedMode))
{ {
@@ -497,7 +491,7 @@ namespace Discord.Net.WebSockets
break; break;
case OpCodes.SessionDescription: case OpCodes.SessionDescription:
{ {
var payload = (msg.Payload as JToken).ToObject<JoinServerEvent>(_client.VoiceSocketSerializer);
var payload = (msg.Payload as JToken).ToObject<JoinServerEvent>(_serializer);
_secretKey = payload.SecretKey; _secretKey = payload.SecretKey;
SendIsTalking(true); SendIsTalking(true);
EndConnect(); EndConnect();
@@ -505,13 +499,13 @@ namespace Discord.Net.WebSockets
break; break;
case OpCodes.Speaking: case OpCodes.Speaking:
{ {
var payload = (msg.Payload as JToken).ToObject<IsTalkingEvent>(_client.VoiceSocketSerializer);
var payload = (msg.Payload as JToken).ToObject<IsTalkingEvent>(_serializer);
RaiseIsSpeaking(payload.UserId, payload.IsSpeaking); RaiseIsSpeaking(payload.UserId, payload.IsSpeaking);
} }
break; break;
default: default:
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unknown Opcode: {opCode}");
if (_logger.Level >= LogSeverity.Warning)
_logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}");
break; break;
} }
} }

src/Discord.Net/Audio/Opus.cs → src/Discord.Net.Audio/Opus.cs View File


src/Discord.Net/Audio/OpusDecoder.cs → src/Discord.Net.Audio/OpusDecoder.cs View File


src/Discord.Net/Audio/OpusEncoder.cs → src/Discord.Net.Audio/OpusEncoder.cs View File


src/Discord.Net/Audio/Sodium.cs → src/Discord.Net.Audio/Sodium.cs View File


src/Discord.Net/Audio/VoiceBuffer.cs → src/Discord.Net.Audio/VoiceBuffer.cs View File

@@ -3,7 +3,7 @@ using System.Threading;


namespace Discord.Audio namespace Discord.Audio
{ {
internal class VoiceBuffer : IDiscordVoiceBuffer
internal class VoiceBuffer
{ {
private readonly int _frameSize, _frameCount, _bufferSize; private readonly int _frameSize, _frameCount, _bufferSize;
private readonly byte[] _buffer; private readonly byte[] _buffer;

src/Discord.Net/lib/libsodium.dll → src/Discord.Net.Audio/libsodium.dll View File


src/Discord.Net/lib/opus.dll → src/Discord.Net.Audio/opus.dll View File


+ 27
- 0
src/Discord.Net.Audio/project.json View File

@@ -0,0 +1,27 @@
{
"version": "0.9.0-alpha1",
"description": "A Discord.Net extension adding voice support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"compile": [ "**/*.cs", "../Discord.Net/Helpers/Shared/*.cs" ],
"contentFiles": [ "libsodium.dll", "opus.dll" ],

"compilationOptions": {
"warningsAsErrors": true,
"allowUnsafe": true
},

"dependencies": {
"Discord.Net": "0.9.0-alpha1"
},
"frameworks": {
"net45": { },
"dotnet5.4": { }
}
}

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

@@ -1,10 +0,0 @@
namespace Discord.Audio
{
public interface IDiscordVoiceBuffer
{
int FrameSize { get; }
int FrameCount { get; }
ushort ReadPos { get; }
ushort WritePos { get; }
}
}

Loading…
Cancel
Save