diff --git a/Aerochat/Aerochat.csproj b/Aerochat/Aerochat.csproj index f1eaa67b..202b4f15 100644 --- a/Aerochat/Aerochat.csproj +++ b/Aerochat/Aerochat.csproj @@ -570,6 +570,7 @@ + diff --git a/Aerochat/Aerochat.sln b/Aerochat/Aerochat.sln new file mode 100644 index 00000000..a9c4a5ea --- /dev/null +++ b/Aerochat/Aerochat.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aerochat - Backup", "Aerochat - Backup.csproj", "{ED5B95D5-B0D5-62F8-71E6-4FC049C771E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aerochat", "Aerochat.csproj", "{2F7F88E9-35D7-18CE-8024-F96DADC79732}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ED5B95D5-B0D5-62F8-71E6-4FC049C771E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED5B95D5-B0D5-62F8-71E6-4FC049C771E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED5B95D5-B0D5-62F8-71E6-4FC049C771E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED5B95D5-B0D5-62F8-71E6-4FC049C771E8}.Release|Any CPU.Build.0 = Release|Any CPU + {2F7F88E9-35D7-18CE-8024-F96DADC79732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F7F88E9-35D7-18CE-8024-F96DADC79732}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F7F88E9-35D7-18CE-8024-F96DADC79732}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F7F88E9-35D7-18CE-8024-F96DADC79732}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C47F4002-1679-4B5E-83C6-32EFCEA09CDF} + EndGlobalSection +EndGlobal diff --git a/Aerochat/Settings/SettingsManager.cs b/Aerochat/Settings/SettingsManager.cs index 36924ad7..f1833345 100644 --- a/Aerochat/Settings/SettingsManager.cs +++ b/Aerochat/Settings/SettingsManager.cs @@ -1,6 +1,7 @@ using Aerochat.Enums; using Aerochat.ViewModels; using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -79,22 +80,28 @@ private static string SettingsFilePath public static void Save() { - // Serialize non-static properties - var properties = Instance.GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => prop.CanWrite && !prop.GetMethod.IsStatic) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(Instance)); + try + { + // Serialize non-static properties + var properties = Instance.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.CanWrite && !prop.GetMethod.IsStatic) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(Instance)); - // Ensure that the directory exists: - Directory.CreateDirectory(Path.GetDirectoryName(SettingsFilePath)!); + // Ensure that the directory exists: + Directory.CreateDirectory(Path.GetDirectoryName(SettingsFilePath)!); - var json = JsonSerializer.Serialize(properties, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(SettingsFilePath, json); + var json = JsonSerializer.Serialize(properties, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(SettingsFilePath, json); - // Call OnPropertyChanged for all properties - foreach (var property in properties.Keys) + // Call OnPropertyChanged for all properties + foreach (var property in properties.Keys) + { + Instance.InvokePropertyChanged(property); + } + } catch (Exception e) { - Instance.InvokePropertyChanged(property); + Debug.WriteLine(e); } } diff --git a/Aerochat/ViewModels/User.cs b/Aerochat/ViewModels/User.cs index 2173e8cd..fb2a7aca 100644 --- a/Aerochat/ViewModels/User.cs +++ b/Aerochat/ViewModels/User.cs @@ -20,6 +20,7 @@ public class UserViewModel : ViewModelBase private SceneViewModel? _scene; private string? _color = "#525252"; private string? _image; + private bool _isSpeaking = false; public required string Name { @@ -66,6 +67,12 @@ public string? Image set => SetProperty(ref _image, value); } + public bool IsSpeaking + { + get => _isSpeaking; + set => SetProperty(ref _isSpeaking, value); + } + public static UserViewModel FromUser(DiscordUser user) { return new UserViewModel diff --git a/Aerochat/Voice/VoiceManager.cs b/Aerochat/Voice/VoiceManager.cs index 126e3af2..2f3b1f74 100644 --- a/Aerochat/Voice/VoiceManager.cs +++ b/Aerochat/Voice/VoiceManager.cs @@ -16,6 +16,11 @@ public class VoiceManager : ViewModelBase { public static VoiceManager Instance = new(); private VoiceSocket? voiceSocket; + public VoiceSocket? VoiceSocket + { + get => voiceSocket; + } + public DiscordChannel? Channel => voiceSocket?.Channel; private ChannelViewModel? _channelVM; @@ -34,10 +39,10 @@ public async Task LeaveVoiceChannel() ChannelVM = null; } - public async Task JoinVoiceChannel(DiscordChannel channel) + public async Task JoinVoiceChannel(DiscordChannel channel, Action onStateChange) { await LeaveVoiceChannel(); - voiceSocket = new(Discord.Client); + voiceSocket = new(Discord.Client, onStateChange); await voiceSocket.ConnectAsync(channel); ChannelVM = ChannelViewModel.FromChannel(channel); } diff --git a/Aerochat/Windows/Chat.xaml b/Aerochat/Windows/Chat.xaml index 507bd006..dd752e35 100644 --- a/Aerochat/Windows/Chat.xaml +++ b/Aerochat/Windows/Chat.xaml @@ -474,12 +474,24 @@ - + + + + - + diff --git a/Aerochat/Windows/Chat.xaml.cs b/Aerochat/Windows/Chat.xaml.cs index dab8e422..3141559a 100644 --- a/Aerochat/Windows/Chat.xaml.cs +++ b/Aerochat/Windows/Chat.xaml.cs @@ -152,7 +152,8 @@ public async Task OnChannelChange(bool initial = false) // We cannot edit messages or upload images across channels, so close the respective UIs. // In the future, this design should possibly be reconsidered: the official Discord client // persists such state between channels, and it only applies to the currently-active channel. - await Dispatcher.BeginInvoke(() => { + await Dispatcher.BeginInvoke(() => + { ClearReplyTarget(); LeaveEditingMessage(); CloseAttachmentsEditor(); @@ -663,7 +664,8 @@ private void ProcessLastRead() if (!SettingsManager.Instance.LastReadMessages.TryGetValue(ChannelId, out var msgId)) { SettingsManager.Instance.LastReadMessages[ChannelId] = message.Id ?? 0; - } else + } + else { long prevTimestamp = ((long)(msgId >> 22)) + 1420070400000; DateTime prevLastMessageTime = DateTimeOffset.FromUnixTimeMilliseconds(prevTimestamp).DateTime; @@ -775,7 +777,8 @@ public Chat(ulong id, bool allowDefault = false, PresenceViewModel? initialPrese if (firstChannel is not null) { ChannelId = firstChannel.Id; - } else + } + else { UnavailableDialog(); return; @@ -854,7 +857,8 @@ private void Chat_KeyDown(object sender, KeyEventArgs e) PresentationSource.FromVisual(MessageTextBox), 0, e.Key - ) { RoutedEvent = Keyboard.KeyDownEvent }); + ) + { RoutedEvent = Keyboard.KeyDownEvent }); MessageTextBox.Focus(); } } @@ -1109,7 +1113,8 @@ private void RunGCRelease() private async void TypingUsers_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { List tempUsers = new(); - foreach (var user in TypingUsers.ToList()) { + foreach (var user in TypingUsers.ToList()) + { if (!Discord.Client.TryGetCachedUser(user.Id, out DiscordUser discordUser)) { // I believe this is fully safe since it'll only occur in a server context, @@ -1419,7 +1424,8 @@ private void Window_SizeChanged(object sender, SizeChangedEventArgs e) if (hiddenItems) { expandBtn.Visibility = Visibility.Visible; - } else + } + else { expandBtn.Visibility = Visibility.Collapsed; } @@ -1547,7 +1553,26 @@ private async void ItemClick(object sender, MouseButtonEventArgs e) } try { - await VoiceManager.Instance.JoinVoiceChannel(channel); + await VoiceManager.Instance.JoinVoiceChannel(channel, (e) => + { + var castedItem = (HomeListItemViewModel)item; + var channelID = castedItem.Id; + Dispatcher.InvokeAsync(() => + { + // FIXME: this code sucks + var category = ViewModel.Categories.ToList().Find(f => f.Items.Any(f => f.Id == channelID)); + if (category == null) return; + var channel = category.Items.ToList().Find(f => f.Id == channelID); + if (channel == null) return; + ulong userID = 0; + VoiceManager.Instance.VoiceSocket?.UserSSRCMap.TryGetValue(e.SSRC, out userID); + var user = castedItem.ConnectedUsers.FirstOrDefault(u => u.Id == userID, null!); + if (user == null) return; + var index = castedItem.ConnectedUsers.IndexOf(user); + if (index == -1) return; + channel.ConnectedUsers[index].IsSpeaking = e.Speaking; + }); + }); } catch (Exception ex) { @@ -1617,38 +1642,38 @@ private async void MessageParser_HyperlinkClicked(object sender, Controls.Hyperl switch (e.Type) { case Controls.HyperlinkType.WebLink: - { - string? uri = (string)e.AssociatedObject; + { + string? uri = (string)e.AssociatedObject; - if (uri is null) - return; + if (uri is null) + return; - OpenExternalUrl(uri); - break; - } + OpenExternalUrl(uri); + break; + } case Controls.HyperlinkType.Channel: - { - var channel = (DiscordChannel)e.AssociatedObject; - ChannelId = channel.Id; - foreach (var category in ViewModel.Categories) { - foreach (var item in category.Items) + var channel = (DiscordChannel)e.AssociatedObject; + ChannelId = channel.Id; + foreach (var category in ViewModel.Categories) { - if (item.Id == channel.Id) + foreach (var item in category.Items) { - item.IsSelected = true; - } - else - { - item.IsSelected = false; + if (item.Id == channel.Id) + { + item.IsSelected = true; + } + else + { + item.IsSelected = false; + } } } + await OnChannelChange().ConfigureAwait(false); + // find the item in the list + break; } - await OnChannelChange().ConfigureAwait(false); - // find the item in the list - break; - } } } @@ -1788,7 +1813,7 @@ private async void CanvasButton_Click(object sender, RoutedEventArgs e) } } - + private int _drawingHeight = 120; private int _writingHeight = 64; @@ -1900,7 +1925,7 @@ private void ShowColorMenu(object sender, MouseButtonEventArgs e) private string _lastValue = ""; Timer typingTimer = new(1000) - { + { AutoReset = false }; @@ -1930,7 +1955,8 @@ private void MessageTextBox_TextChanged(object sender, TextChangedEventArgs e) _isTyping = true; TypingTimer_Elapsed(null, null!); typingTimer.Start(); - }; + } + ; } private void OnMessageContextMenuOpening(object senderRaw, ContextMenuEventArgs e) diff --git a/Aerochat/Windows/Home.xaml.cs b/Aerochat/Windows/Home.xaml.cs index 5fdd1d92..5ae229ee 100644 --- a/Aerochat/Windows/Home.xaml.cs +++ b/Aerochat/Windows/Home.xaml.cs @@ -90,6 +90,10 @@ public Home() // Subscribe to changes in the DisplayAds property SettingsManager.Instance.PropertyChanged += OnSettingsChange; + + await Task.Delay(1000); + Chat chat = new(1287027740452716606); + chat.Show(); }); } diff --git a/Aerovoice/Aerovoice.csproj b/Aerovoice/Aerovoice.csproj index 3a30deae..099466b9 100644 --- a/Aerovoice/Aerovoice.csproj +++ b/Aerovoice/Aerovoice.csproj @@ -1,24 +1,41 @@  - - net8.0 - enable - enable - AnyCPU;x64 - + + net8.0 + enable + enable + AnyCPU;x64 + true + true + - - - - - - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/Aerovoice/Clients/VoiceSocket.cs b/Aerovoice/Clients/VoiceSocket.cs index 65ec2f01..8e57796e 100644 --- a/Aerovoice/Clients/VoiceSocket.cs +++ b/Aerovoice/Clients/VoiceSocket.cs @@ -24,42 +24,152 @@ using Aerovoice.Encoders; using Aerovoice.Timestamp; using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Runtime.InteropServices; +using static Vanara.PInvoke.Kernel32; namespace Aerovoice.Clients { + public class VoiceStateChanged + { + public uint SSRC; + public bool Speaking; + + public VoiceStateChanged(uint SSRC, bool Speaking) + { + this.SSRC = SSRC; + this.Speaking = Speaking; + } + } + + struct IPInfo + { + public string Address; + public ushort Port; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void VoiceUserCallback(uint ssrc, bool speaking); + + partial class VoiceSession : IDisposable + { + unsafe struct RawIPInfo + { + public fixed byte IP[64]; + public ushort Port; + } + + [LibraryImport("AerovoiceNative.dll")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private static partial IntPtr voice_session_new(uint ssrc, ulong channelId, [MarshalAs(UnmanagedType.LPUTF8Str)] string ip, ushort port, VoiceUserCallback onSpeaking); + [LibraryImport("AerovoiceNative.dll")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private static partial void voice_session_free(IntPtr sessionHandle); + [LibraryImport("AerovoiceNative.dll")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private static partial void voice_session_init_poll_thread(IntPtr sessionHandle); + [LibraryImport("AerovoiceNative.dll")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private static partial IntPtr voice_session_discover_ip(IntPtr sessionHandle); + [LibraryImport("AerovoiceNative.dll")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private unsafe static partial IntPtr voice_session_set_secret(IntPtr sessionHandle, byte* secret, uint secretLen); + [LibraryImport("AerovoiceNative.dll")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.LPUTF8Str)] + private unsafe static partial string voice_session_select_cryptor(IntPtr sessionHandle, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPUTF8Str, SizeParamIndex = 2)] string[] availableMethods, uint availableMethodsLen); + + private IntPtr _sessionHandle; + + public VoiceSession(uint ssrc, ulong channelId, string ip, ushort port, VoiceUserCallback onSpeaking) + { + AllocConsole(); + _sessionHandle = voice_session_new(ssrc, channelId, ip, port, onSpeaking); + if (_sessionHandle == IntPtr.Zero) + { + throw new Exception("Failed to create voice session."); + } + } + + public void BeginPolling() + { + voice_session_init_poll_thread(_sessionHandle); + } + + public IPInfo DiscoverIP() + { + unsafe + { + var result = *(RawIPInfo*)voice_session_discover_ip(_sessionHandle).ToPointer(); + byte[] managedIP = new byte[64]; + for (int i = 0; i < 64; i++) + { + managedIP[i] = result.IP[i]; + } + + string ip = Encoding.UTF8.GetString(managedIP).Trim((char)0); + ushort port = result.Port; + return new IPInfo { Address = ip, Port = port }; + } + } + + public void SetSecret(byte[] secret) + { + var len = (uint)secret.Length; + unsafe + { + fixed (byte* secretPtr = &secret[0]) + { + voice_session_set_secret(_sessionHandle, secretPtr, len); + } + } + } + + public string SelectCryptor(string[] availableCryptors) + { + return voice_session_select_cryptor(_sessionHandle, availableCryptors, (uint)availableCryptors.Length); + } + + public void Dispose() + { + FreeConsole(); + if (_sessionHandle != IntPtr.Zero) + { + voice_session_free(_sessionHandle); + _sessionHandle = IntPtr.Zero; + } + } + } + + public class VoiceSocket { private WebsocketClient _socket; - private UDPClient UdpClient; private DiscordClient _client; private JObject _ready; private bool _disposed = false; + private VoiceSession? _session; - - public IPlayer Player = new NAudioPlayer(); - public IDecoder Decoder = new OpusDotNetDecoder(); - public IEncoder Encoder = new ConcentusEncoder(); - public BaseRecorder Recorder = new NAudioRecorder(); - public string? ForceEncryptionName; - - - private BaseCrypt cryptor; - private byte[] _secretKey; + //private BaseCrypt cryptor; + private string _serverSdp; private int _sequence; private string _sessionId; private string _voiceToken; private Uri _endpoint; public DiscordChannel Channel { get; private set; } private bool _connected = false; - private List _availableEncryptionModes; private RTPTimestamp _timestamp = new(3840); private uint _ssrc = 0; + private VoiceUserCallback _cb; + private Dictionary _userSsrcMap = []; + public Dictionary UserSSRCMap { get { return _userSsrcMap; } } public bool Speaking { get; private set; } = false; System.Timers.Timer _timer; - public VoiceSocket(DiscordClient client) + Action _onStateChange; + + public VoiceSocket(DiscordClient client, Action onStateChange) { _client = client; // every 1/60 seconds, on another thread, incrementTimestamp using 3840 @@ -68,6 +178,34 @@ public VoiceSocket(DiscordClient client) _timer.AutoReset = true; _timer.Elapsed += (s, e) => _timestamp.Increment(3840); _timer.Start(); + _onStateChange = onStateChange; + _cb = new VoiceUserCallback(InternalVoiceCallback); + } + + private void InternalVoiceCallback(uint ssrc, bool speaking) + { + _onStateChange(new VoiceStateChanged(ssrc, speaking)); + if (ssrc != _ssrc || _socket == null) + { + return; + } + + Debug.WriteLine(speaking); + + Task.Run(async () => + { + var msg = new + { + op = 5, + d = new + { + speaking = speaking ? 1 : 0, + delay = 0, + ssrc + } + }; + await SendMessage(JObject.FromObject(msg)); + }); } public async Task SendMessage(JObject message) @@ -84,7 +222,7 @@ public byte[] ConstructPortScanPacket(uint ssrc, string ip, ushort port) byte[] packetLength = new byte[2]; BitConverter.GetBytes((ushort)70).CopyTo(packetLength, 0); - Array.Reverse(packetLength); + Array.Reverse(packetLength); byte[] ssrcBuf = new byte[4]; BitConverter.GetBytes(ssrc).CopyTo(ssrcBuf, 0); @@ -108,8 +246,12 @@ public byte[] ConstructPortScanPacket(uint ssrc, string ip, ushort port) return packet; } + private void OnSpeaking(uint ssrc, bool speaking) + { } + public async Task OnMessageReceived(JObject message) { + Debug.WriteLine(message); // if message["seq"] exists, set _sequence to it if (message["seq"] != null) { @@ -119,311 +261,92 @@ public async Task OnMessageReceived(JObject message) switch (op) { case 2: // ready - { - _ready = message["d"]!.Value()!; - var ip = _ready["ip"]!.Value()!; - var port = _ready["port"]!.Value(); - _ssrc = _ready["ssrc"]!.Value(); - var modes = _ready["modes"]!.ToArray().Select(x => x.Value()!); - _availableEncryptionModes = modes.ToList(); - Logger.Log($"Attempting to open UDP connection to {ip}:{port}."); - Logger.Log($"Your SSRC is {_ssrc}."); - UdpClient = new(ip, port); - UdpClient.MessageReceived += (s, e) => Task.Run(() => UdpClient_MessageReceived(s, e)); - - var discoveryPacket = ConstructPortScanPacket(_ssrc, ip, port); - - UdpClient.SendMessage(discoveryPacket); - break; - } - case 4: // session description - { - // secret_key is a number array - var secretKey = message["d"]!["secret_key"]!.Value()!.Select(x => (byte)x.Value()).ToArray(); - _secretKey = secretKey; - if (cryptor is null) { - cryptor = GetPreferredEncryption(); - } - break; - } - } - } - - private readonly SortedList _packetBuffer = new(); - private readonly object _bufferLock = new(); - private uint _lastPlayedTimestamp = 0; - private const int BUFFER_THRESHOLD = 5; - - private async Task UdpClient_MessageReceived(object? sender, byte[] e) - { - byte packetType = e[1]; - switch (packetType) - { - case 0x2: // ip discovery - { - var address = Encoding.UTF8.GetString(e, 8, 64).TrimEnd('\0'); - var port = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(e, 72)); - Logger.Log($"IP discovery was successful, your port is {port}."); - if (cryptor is null) - { - cryptor = GetPreferredEncryption(); - } - await SendMessage(JObject.FromObject(new - { - op = 1, - d = new + _ready = message["d"]!.Value()!; + var ip = _ready["ip"]!.Value()!; + var port = _ready["port"]!.Value(); + _ssrc = _ready["ssrc"]!.Value(); + _userSsrcMap.Add(_ssrc, _client.CurrentUser.Id); + var modes = _ready["modes"]!.ToArray().Select(x => x.Value()!); + Logger.Log($"Attempting to open UDP connection to {ip}:{port}."); + Logger.Log($"Your SSRC is {_ssrc}."); + _session = new VoiceSession(_ssrc, this.Channel.Id, ip, port, _cb); + var discovered = _session.DiscoverIP(); + Debug.WriteLine($"IP: {discovered.Address}"); + Debug.WriteLine($"Port: {discovered.Port}"); + var cryptor = _session.SelectCryptor([.. modes]); + if (cryptor == null) return; + var msg = new { - protocol = "udp", - data = new + op = 1, + d = new { - address, - port, - mode = cryptor.PName - }, - codecs = new[] - { - new + protocol = "udp", + data = new + { + address = discovered.Address, + port = discovered.Port, + mode = cryptor + }, + codecs = new[] { - name = "opus", - type = "audio", - priority = 1000, - payload_type = 120 + new + { + name = "opus", + type = "audio", + priority = 1000, + payload_type = 109, + } } } - } - })); - Recorder.Start(); - break; - } - case 0x78: - { - // TODO: thread manager where each user gets one thread - await Task.Run(() => - { - if (cryptor is null || _secretKey is null) return; + }; - var rtpTimestamp = BinaryPrimitives.ReadUInt32BigEndian(e.AsSpan(4)); - lock (_bufferLock) - { - _packetBuffer[rtpTimestamp] = e; - } - - TryProcessBufferedPackets(); - }); - break; - } - } - } - - private void TryProcessBufferedPackets() - { - lock (_bufferLock) - { - if (_packetBuffer.Count < BUFFER_THRESHOLD) - return; - - foreach (var key in _packetBuffer.Keys.ToList()) - { - if (key > _lastPlayedTimestamp) - { - byte[] packet = _packetBuffer[key]; - _packetBuffer.Remove(key); - _lastPlayedTimestamp = key; - - ProcessPacket(packet); - } - } - } - } - - private void ProcessPacket(byte[] e) - { - var ssrc = BinaryPrimitives.ReadUInt32BigEndian(e.AsSpan(8)); - - byte[] decryptedData = cryptor.Decrypt(e, _secretKey); - if (decryptedData.Length == 0) return; - // read ushort increment, 2 bytes in - ushort increment = BinaryPrimitives.ReadUInt16BigEndian(e.AsSpan(2)); - var decoded = Decoder.Decode(decryptedData, decryptedData.Length, out int decodedLength, ssrc, increment); - Player.AddSamples(decoded, decodedLength, ssrc); - } - - public BaseCrypt GetPreferredEncryption() - { - var decryptors = typeof(BaseCrypt).Assembly.GetTypes().Where(x => x.Namespace == "Aerovoice.Crypts" && x.IsSubclassOf(typeof(BaseCrypt)) && _availableEncryptionModes.Contains((string)x.GetProperty("Name")!.GetValue(null)!)); - var priority = new[] { "aead_aes256_gcm_rtpsize", "aead_xchacha20_poly1305_rtpsize" }; - BaseCrypt? decryptor = null; - if (ForceEncryptionName != null) - { - var forced = decryptors.FirstOrDefault(x => x.GetProperty("Name")!.GetValue(null)!.Equals(ForceEncryptionName)); - if (forced != null) - { - decryptor = (BaseCrypt)Activator.CreateInstance(forced)!; - } else - { - Logger.Log($"\"{ForceEncryptionName}\" is not supported, falling back to default."); - } - } - if (decryptor == null) - { - foreach (var p in priority) - { - var d = decryptors.FirstOrDefault(x => x.GetProperty("Name")!.GetValue(null)!.Equals(p)); - if (d != null && _availableEncryptionModes.Contains(p)) - { - decryptor = (BaseCrypt)Activator.CreateInstance(d)!; + await SendMessage(JObject.FromObject(msg)); break; } - } - } - - decryptor = decryptor ?? (BaseCrypt)Activator.CreateInstance(decryptors.FirstOrDefault(x => _availableEncryptionModes.Contains(x.GetProperty("Name")!.GetValue(null))!)!)!; - // log all encryption modes but make the preferred one bold - Logger.Log($"Encryption mode selected:"); - var names = decryptors.Select(x => (string)x.GetProperty("Name")!.GetValue(null)!); - // sort the modes such that the preferred one is first, then the supported ones, then the unsupported ones - // unsupported modes are modes where Name isn't in names - _availableEncryptionModes = _availableEncryptionModes.OrderBy(x => x != decryptor.PName).ThenBy(x => !names.Contains(x)).ThenBy(x => x).ToList(); - foreach (var mode in _availableEncryptionModes) - { - if (mode == decryptor.PName) - { - Console.ForegroundColor = ConsoleColor.Green; - } - else if (!names.Contains(mode)) - { - Console.ForegroundColor = ConsoleColor.DarkGray; - } - else - { - Console.ForegroundColor = ConsoleColor.White; - } - Console.WriteLine($"- {mode}"); - Console.ResetColor(); - } - return decryptor; - } - - public async Task ConnectAsync(DiscordChannel channel) - { - if (_disposed) throw new InvalidOperationException("This voice socket has been disposed!"); - Channel = channel; - await _client.UpdateVoiceStateAsync(Channel.Guild?.Id ?? Channel.Id, Channel.Id, false, false); - _client.VoiceStateUpdated += _client_VoiceStateUpdated; - _client.VoiceServerUpdated += _client_VoiceServerUpdated; - Recorder.DataAvailable += Recorder_DataAvailable; - } - - private short _udpSequence = (short)new Random().Next(0, short.MaxValue); - - private const int BufferDurationMs = 200; - private const int ChunkDurationMs = 20; - private const int SampleRate = 48000; // 48kHz - private const int BytesPerSample = 2; // 16-bit PCM - private const int Channels = 2; // Stereo - private const int BufferSizeBytes = (SampleRate * Channels * BytesPerSample * BufferDurationMs) / 1000; // 38400 bytes - private byte[] _circularBuffer = new byte[BufferSizeBytes]; - private int _bufferOffset = 0; - private bool _bufferFilled = false; - - private async void Recorder_DataAvailable(object? sender, byte[] e) - { - await Task.Run(() => - { - // Append incoming 20ms audio to the circular buffer - AddToCircularBuffer(e); - - // Check if the user is speaking using the circular buffer - var sampleIsSpeaking = IsSpeaking(_circularBuffer, _bufferFilled ? BufferSizeBytes : _bufferOffset); - - if (sampleIsSpeaking) - { - if (Speaking) + case 4: // session description { - _ = SendMessage(JObject.FromObject(new + //var serverSdp = message["d"]!["sdp"]!.Value()!; + //_serverSdp = serverSdp; + //_session.SetServerSdp(_serverSdp); + var secret = message["d"]!["secret_key"]!.Value()!.Select(x => (byte)x.Value()).ToArray(); + _session?.SetSecret(secret); + _session?.BeginPolling(); + var msg = new { op = 5, d = new { - speaking = 0, // not speaking + speaking = 0, delay = 0, ssrc = _ssrc } - })); + }; + await SendMessage(JObject.FromObject(msg)); + break; } - Speaking = false; - return; - } - - if (!Speaking) - { - _ = SendMessage(JObject.FromObject(new + case 5: // speaking { - op = 5, - d = new - { - speaking = 1 << 0, // VOICE - delay = 0, - ssrc = _ssrc - } - })); - Speaking = true; - } + var userId = ulong.Parse(message["d"]!["user_id"]!.Value()!); + var ssrc = message["d"]!["ssrc"]!.Value()!; - if (cryptor is null) return; - var opus = Encoder.Encode(e); - var header = new byte[12]; - header[0] = 0x80; // Version + Flags - header[1] = 0x78; // Payload Type - BinaryPrimitives.WriteInt16BigEndian(header.AsSpan(2), _udpSequence++); // Sequence - BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(4), _timestamp.GetCurrentTimestamp()); // Timestamp - BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(8), _ssrc); // SSRC - var packet = new byte[header.Length + opus.Length]; - Array.Copy(header, 0, packet, 0, header.Length); - Array.Copy(opus, 0, packet, header.Length, opus.Length); - var encrypted = cryptor.Encrypt(packet, _secretKey); - UdpClient.SendMessage(encrypted); - }); - } - - private void AddToCircularBuffer(byte[] data) - { - int dataLength = data.Length; + _userSsrcMap.Remove(ssrc); + _userSsrcMap.Add(ssrc, userId); - if (_bufferOffset + dataLength > BufferSizeBytes) - { - int overflow = _bufferOffset + dataLength - BufferSizeBytes; - Array.Copy(data, 0, _circularBuffer, _bufferOffset, dataLength - overflow); - Array.Copy(data, dataLength - overflow, _circularBuffer, 0, overflow); - _bufferOffset = overflow; - _bufferFilled = true; - } - else - { - Array.Copy(data, 0, _circularBuffer, _bufferOffset, dataLength); - _bufferOffset += dataLength; + break; + } } } - private bool IsSpeaking(byte[] buffer, int length) + public async Task ConnectAsync(DiscordChannel channel) { - int samples = length / BytesPerSample; // Convert byte length to number of samples - double sum = 0; - - for (int i = 0; i < length; i += 2) - { - short sample = (short)((buffer[i + 1] << 8) | buffer[i]); // Convert to 16-bit sample - sum += sample * sample; - } - - // Calculate RMS - double rms = Math.Sqrt(sum / samples); - - // Threshold for speaking detection - const double threshold = 300; // Adjust this value based on the microphone and environment - return rms < threshold; + if (_disposed) throw new InvalidOperationException("This voice socket has been disposed!"); + Channel = channel; + await _client.UpdateVoiceStateAsync(Channel.Guild?.Id ?? Channel.Id, Channel.Id, false, false); + _client.VoiceStateUpdated += _client_VoiceStateUpdated; + _client.VoiceServerUpdated += _client_VoiceServerUpdated; } + private async Task _client_VoiceStateUpdated(DiscordClient sender, DSharpPlus.EventArgs.VoiceStateUpdateEventArgs args) { _sessionId = args.SessionId; @@ -470,10 +393,10 @@ await SendMessage(JObject.FromObject(new op = 0, d = new { - server_id = Channel.Guild?.Id ?? Channel.Id, - user_id = _client.CurrentUser.Id, + server_id = Channel.Guild?.Id.ToString() ?? "", + user_id = _client.CurrentUser.Id.ToString(), session_id = _sessionId, - token = _voiceToken + token = _voiceToken, } })); } @@ -486,10 +409,8 @@ public async Task DisconnectAndDispose() _disposed = true; await _client.UpdateVoiceStateAsync(Channel.GuildId, null, false, false); _socket?.Dispose(); - UdpClient?.Dispose(); - Recorder?.Dispose(); _timer.Dispose(); - Encoder?.Dispose(); + _session?.Dispose(); } } } diff --git a/Aerovoice/Native/.gitignore b/Aerovoice/Native/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/Aerovoice/Native/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Aerovoice/Native/Cargo.lock b/Aerovoice/Native/Cargo.lock new file mode 100644 index 00000000..3cd1e65e --- /dev/null +++ b/Aerovoice/Native/Cargo.lock @@ -0,0 +1,1391 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aerovoice-native" +version = "0.1.0" +dependencies = [ + "audiopus", + "bytemuck", + "byteorder", + "bytes", + "chacha20poly1305", + "cpal", + "fixed-resample", + "hex", + "rand", + "rtrb", + "sodiumoxide", + "thiserror 2.0.12", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "audiopus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3743519567e9135cf6f9f1a509851cb0c8e4cb9d66feb286668afb1923bec458" +dependencies = [ + "audiopus_sys", +] + +[[package]] +name = "audiopus_sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927791de46f70facea982dbfaf19719a41ce6064443403be631a85de6a58fff9" +dependencies = [ + "log", + "pkg-config", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fast-interleave" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7e05e2b3c97d4516fa5c177133f3e4decf9c8318841e1545b260535311e3a5" + +[[package]] +name = "fixed-resample" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a267bc40fae9b208e2a67a99a8d794caf3a9fe1146d01c147f59bd96257a74" +dependencies = [ + "arrayvec", + "fast-interleave", + "ringbuf", + "rubato", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.9.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "realfft" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1" +dependencies = [ + "rustfft", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "rtrb" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" + +[[package]] +name = "rubato" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustfft" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f266ff9b0cfc79de11fd5af76a2bc672fe3ace10c96fa06456740fa70cb1ed49" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Aerovoice/Native/Cargo.toml b/Aerovoice/Native/Cargo.toml new file mode 100644 index 00000000..1d5998e8 --- /dev/null +++ b/Aerovoice/Native/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "aerovoice-native" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +audiopus = "0.2.0" +bytemuck = { version = "1.23.0", features = ["derive"] } +byteorder = "1.5.0" +bytes = "1.10.1" +chacha20poly1305 = "0.10.1" +cpal = "0.15.3" +fixed-resample = "0.8.0" +hex = "0.4.3" +rand = "0.9.1" +rtrb = "0.3.2" +sodiumoxide = "0.2.7" +thiserror = "2.0.12" diff --git a/Aerovoice/Native/src/char_ext.rs b/Aerovoice/Native/src/char_ext.rs new file mode 100644 index 00000000..19d30adb --- /dev/null +++ b/Aerovoice/Native/src/char_ext.rs @@ -0,0 +1,20 @@ +use std::ffi::c_char; + +pub trait CCharExt { + fn to_string_lossy(&self) -> Option; +} + +impl CCharExt for *const c_char { + fn to_string_lossy(&self) -> Option { + if self.is_null() { + return None; + } + let str = unsafe { + std::ffi::CStr::from_ptr(*self) + .to_string_lossy() + .into_owned() + }; + + Some(str) + } +} diff --git a/Aerovoice/Native/src/crypto/aead_xchacha20_poly1305_rtpsize.rs b/Aerovoice/Native/src/crypto/aead_xchacha20_poly1305_rtpsize.rs new file mode 100644 index 00000000..a94093be --- /dev/null +++ b/Aerovoice/Native/src/crypto/aead_xchacha20_poly1305_rtpsize.rs @@ -0,0 +1,56 @@ +use crate::rtp::{Decrypted, Encrypted, HeaderExtensionType, RtpPacket}; + +use super::Cryptor; +use chacha20poly1305::{ + KeyInit, XChaCha20Poly1305, + aead::{AeadMut, Payload}, +}; + +const NONCE_BYTES: usize = 4; + +#[derive(Default, Clone, Copy)] +pub struct AeadXChaCha20Poly1305RtpSize { + sequence: u32, +} + +impl Cryptor for AeadXChaCha20Poly1305RtpSize { + fn decrypt(&mut self, packet: &RtpPacket, key: &[u8]) -> Option> { + let mut cipher = XChaCha20Poly1305::new(key.into()); + let header = packet.header(HeaderExtensionType::Partial); + let blob = packet.encrypted_blob(); + if blob.len() < NONCE_BYTES { + return None; + } + + let ciphertext = &blob[..blob.len() - NONCE_BYTES]; + let nonce_bytes = &blob[blob.len() - NONCE_BYTES..]; + let mut nonce = [0; 24]; + nonce[..NONCE_BYTES].copy_from_slice(nonce_bytes); + let payload = Payload { + msg: ciphertext, + aad: header, + }; + + cipher.decrypt(&nonce.into(), payload).ok() + } + + fn encrypt(&mut self, packet: &RtpPacket, key: &[u8]) -> Option> { + self.sequence = self.sequence.wrapping_add(1); + let data = packet.data_to_encrypt(); + let mut cipher = XChaCha20Poly1305::new(key.into()); + let header = packet.header(HeaderExtensionType::Partial); + let mut nonce = [0; 24]; + nonce[..NONCE_BYTES].copy_from_slice(&self.sequence.to_be_bytes()); + let payload = Payload { + msg: data, + aad: header, + }; + let mut ciphertext = cipher.encrypt(&nonce.into(), payload).ok()?; + ciphertext.extend_from_slice(&nonce[..NONCE_BYTES]); + Some(ciphertext) + } + + fn name(&self) -> &'static str { + "aead_xchacha20_poly1305_rtpsize" + } +} diff --git a/Aerovoice/Native/src/crypto/mod.rs b/Aerovoice/Native/src/crypto/mod.rs new file mode 100644 index 00000000..543b7e22 --- /dev/null +++ b/Aerovoice/Native/src/crypto/mod.rs @@ -0,0 +1,28 @@ +mod aead_xchacha20_poly1305_rtpsize; + +pub use aead_xchacha20_poly1305_rtpsize::AeadXChaCha20Poly1305RtpSize; + +use crate::rtp::{Decrypted, Encrypted, RtpPacket}; + +const CRYPTOR_PRIORITY: &[&str] = &["aead_xchacha20_poly1305_rtpsize"]; + +pub fn encryption_by_priority(available_methods: &[String]) -> Option> { + for name in CRYPTOR_PRIORITY { + if available_methods.contains(&name.to_string()) { + return match *name { + "aead_xchacha20_poly1305_rtpsize" => { + Some(Box::new(AeadXChaCha20Poly1305RtpSize::default())) + } + _ => None, + }; + } + } + + None +} + +pub trait Cryptor: Send + Sync { + fn name(&self) -> &'static str; + fn encrypt(&mut self, packet: &RtpPacket, key: &[u8]) -> Option>; + fn decrypt(&mut self, packet: &RtpPacket, key: &[u8]) -> Option>; +} diff --git a/Aerovoice/Native/src/external.rs b/Aerovoice/Native/src/external.rs new file mode 100644 index 00000000..ab60f46c --- /dev/null +++ b/Aerovoice/Native/src/external.rs @@ -0,0 +1,116 @@ +use std::ffi::c_char; + +use crate::{ + char_ext::CCharExt, + crypto::encryption_by_priority, + session::{IPInfo, VoiceSession}, + snowflake::Snowflake, +}; + +#[unsafe(no_mangle)] +extern "C" fn voice_session_new( + ssrc: u32, + channel: u64, + ip: *const c_char, + port: u16, + on_speaking: extern "C" fn(u32, bool), +) -> *mut VoiceSession { + let channel = Snowflake::from_u64(channel); + + let Some(ip) = ip.to_string_lossy() else { + return std::ptr::null_mut(); + }; + + let session = VoiceSession::new(ssrc, channel, ip, port, on_speaking); + + Box::into_raw(Box::new(session)) +} + +// extern "C" fn on_speaking(ssrc: u32, speaking: bool) { +// println!("SSRC: {}, Speaking: {}", ssrc, speaking); +// } + +#[unsafe(no_mangle)] +extern "C" fn voice_session_init_poll_thread(session: *mut VoiceSession) { + if session.is_null() { + return; + } + + let session = unsafe { &mut *session }; + session.init_poll_thread(); +} + +#[unsafe(no_mangle)] +extern "C" fn voice_session_set_secret(session: *mut VoiceSession, secret: *const u8, len: u32) { + if session.is_null() { + return; + } + + let session = unsafe { &mut *session }; + let len = len as usize; + let secret = unsafe { std::slice::from_raw_parts(secret, len) }; + let secret = secret.to_vec(); + session.set_secret(secret); +} + +#[unsafe(no_mangle)] +extern "C" fn voice_session_select_cryptor( + session: *mut VoiceSession, + available_methods: *const *const c_char, + available_methods_len: u32, +) -> *mut c_char { + if session.is_null() { + return std::ptr::null_mut(); + } + + let session = unsafe { &mut *session }; + + let available_methods = + unsafe { std::slice::from_raw_parts(available_methods, available_methods_len as usize) }; + // convert to Vec + let available_methods = available_methods + .iter() + .map(|&s| unsafe { std::ffi::CStr::from_ptr(s) }) + .map(|s| s.to_string_lossy().to_string()) + .collect::>(); + + let Some(send_cryptor) = encryption_by_priority(&available_methods) else { + println!("!!! WARNING !!! No encryption method was found. VOICE CHAT WILL NOT WORK!"); + return std::ptr::null_mut(); + }; + + let Some(recv_cryptor) = encryption_by_priority(&available_methods) else { + println!("!!! WARNING !!! No encryption method was found. VOICE CHAT WILL NOT WORK!"); + return std::ptr::null_mut(); + }; + + let name = recv_cryptor.name().to_string(); + let name = std::ffi::CString::new(name).unwrap(); + + session.set_cryptor(send_cryptor, recv_cryptor); + + name.into_raw() +} + +#[unsafe(no_mangle)] +extern "C" fn voice_session_discover_ip(session: *mut VoiceSession) -> *const IPInfo { + if session.is_null() { + return std::ptr::null(); + } + + let session = unsafe { &mut *session }; + let ip_info = session.discover_ip(); + + ip_info as *const _ +} + +#[unsafe(no_mangle)] +extern "C" fn voice_session_free(session: *mut VoiceSession) { + if session.is_null() { + return; + } + + unsafe { + drop(Box::from_raw(session)); + } +} diff --git a/Aerovoice/Native/src/lib.rs b/Aerovoice/Native/src/lib.rs new file mode 100644 index 00000000..acd6c2ee --- /dev/null +++ b/Aerovoice/Native/src/lib.rs @@ -0,0 +1,24 @@ +use std::sync::LazyLock; + +use cpal::{Device, Host, traits::HostTrait as _}; + +mod char_ext; +mod crypto; +mod external; +mod rtp; +mod session; +mod snowflake; + +pub static DEFAULT_HOST: LazyLock = LazyLock::new(cpal::default_host); + +pub static OUTPUT_DEVICE: LazyLock = LazyLock::new(|| { + DEFAULT_HOST + .default_output_device() + .expect("failed to get default output device") +}); + +pub static INPUT_DEVICE: LazyLock = LazyLock::new(|| { + DEFAULT_HOST + .default_input_device() + .expect("failed to get default output device") +}); diff --git a/Aerovoice/Native/src/rtp.rs b/Aerovoice/Native/src/rtp.rs new file mode 100644 index 00000000..de28bf41 --- /dev/null +++ b/Aerovoice/Native/src/rtp.rs @@ -0,0 +1,316 @@ +use std::marker::PhantomData; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::crypto::Cryptor; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Encrypted; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Decrypted; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RtpPacket { + pub version_flags: u8, + pub payload_type: u8, + pub sequence: u16, + pub timestamp: u32, + pub ssrc: u32, + payload: Vec, + raw: Vec, + is_silence: Option, + _marker: PhantomData, +} + +#[derive(Debug, Error)] +pub enum PacketParseError { + #[error("Invalid packet length: {0}")] + PacketTooShort(usize), + #[error("Invalid header:: {0}")] + InvalidHeader(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +pub enum PacketDecryptError { + #[error("Decryption failed for cryptor {0}")] + DecryptionFailed(&'static str), +} + +#[derive(Debug, Error)] +pub enum PacketEncryptError { + #[error("Encryption failed for cryptor {0}")] + EncryptionFailed(&'static str), +} + +#[derive(Debug, Clone, Copy)] +pub enum HeaderExtensionType { + None, + Partial, + Full, +} + +#[derive(Debug, Error)] +pub enum PacketConstructError { + #[error("Failed to construct packet")] + PacketConstructFailed, +} + +impl RtpPacket { + pub fn header(&self, header_type: HeaderExtensionType) -> &[u8] { + &self.raw[..self.header_length(header_type)] + } + + pub fn extension_length(&self) -> usize { + if (self.version_flags & 0x10) == 0 { + return 0; + } + + let mut packet = &self.raw[14..]; + let extension_length = packet.read_u16::().unwrap_or(0) as usize; + + extension_length * 4 + } + + pub fn header_length(&self, header_type: HeaderExtensionType) -> usize { + const HEADER_LENGTH: usize = 12; + if !self.has_extension() { + return HEADER_LENGTH; + } + + match header_type { + HeaderExtensionType::None => HEADER_LENGTH, + HeaderExtensionType::Partial => HEADER_LENGTH + 4, + HeaderExtensionType::Full => HEADER_LENGTH + self.extension_length() + 4, + } + } + + pub fn has_extension(&self) -> bool { + (self.version_flags & 0x10) != 0 + } + + pub fn raw(&self) -> &[u8] { + &self.raw + } + + pub fn is_silent(&self) -> Option { + self.is_silence + } +} + +impl RtpPacket { + pub fn parse(packet: impl AsRef<[u8]>) -> Result { + let raw = packet.as_ref().to_vec(); + let mut packet = packet.as_ref(); + if packet.len() < 12 { + return Err(PacketParseError::PacketTooShort(packet.len())); + } + + let version_flags = packet.read_u8()?; + let payload_type = packet.read_u8()?; + let sequence = packet.read_u16::()?; + let timestamp = packet.read_u32::()?; + let ssrc = packet.read_u32::()?; + + Ok(RtpPacket:: { + version_flags, + payload_type, + sequence, + timestamp, + ssrc, + raw, + payload: vec![], + _marker: PhantomData, + is_silence: None, + }) + } + + pub fn decrypt( + self, + cryptor: &mut (impl Cryptor + ?Sized), + secret: &[u8], + ) -> Result, PacketDecryptError> { + let mut decrypted = cryptor + .decrypt(&self, secret) + .ok_or_else(|| PacketDecryptError::DecryptionFailed(cryptor.name()))?; + + if self.has_extension() { + decrypted.drain(..self.extension_length()); + } + + let header = self.header(HeaderExtensionType::Partial); + + let mut raw = Vec::with_capacity(header.len() + decrypted.len()); + raw.extend_from_slice(header); + raw.extend_from_slice(&decrypted); + + Ok(RtpPacket:: { + version_flags: self.version_flags, + payload_type: self.payload_type, + sequence: self.sequence, + timestamp: self.timestamp, + ssrc: self.ssrc, + payload: decrypted, + raw, + _marker: PhantomData, + is_silence: self.is_silence, + }) + } + + pub fn encrypted_blob(&self) -> &[u8] { + &self.raw[self.header_length(HeaderExtensionType::Partial)..] + } +} + +impl RtpPacket { + pub fn builder() -> RtpPacketBuilder { + RtpPacketBuilder::default() + } + + pub fn payload(&self) -> &[u8] { + &self.payload + } + + pub fn encrypt( + self, + cryptor: &mut (impl Cryptor + ?Sized), + secret: &[u8], + ) -> Result, PacketEncryptError> { + let encrypted_payload = cryptor + .encrypt(&self, secret) + .ok_or_else(|| PacketEncryptError::EncryptionFailed(cryptor.name()))?; + + let header = self.header(HeaderExtensionType::Partial); + let mut raw = Vec::with_capacity(header.len() + encrypted_payload.len()); + raw.extend_from_slice(header); + raw.extend_from_slice(&encrypted_payload); + + Ok(RtpPacket:: { + version_flags: self.version_flags, + payload_type: self.payload_type, + sequence: self.sequence, + timestamp: self.timestamp, + ssrc: self.ssrc, + is_silence: self.is_silence, + payload: encrypted_payload, + raw, + _marker: PhantomData, + }) + } + + pub fn data_to_encrypt(&self) -> &[u8] { + &self.raw[self.header_length(HeaderExtensionType::Partial)..] + } +} + +#[derive(Debug)] +pub struct RtpPacketBuilder { + version_flags: u8, + payload_type: u8, + sequence: u16, + timestamp: u32, + ssrc: u32, + payload: Vec, + is_silence: Option, +} + +impl Default for RtpPacketBuilder { + fn default() -> Self { + Self { + version_flags: 0x80, + payload_type: 0x78, + sequence: 0, + timestamp: 0, + ssrc: 0, + payload: vec![], + is_silence: None, + } + } +} + +impl RtpPacketBuilder { + pub fn silence(mut self, is_silence: bool) -> Self { + self.is_silence = Some(is_silence); + self + } + + pub fn sequence(mut self, sequence: u16) -> Self { + self.sequence = sequence; + self + } + + pub fn timestamp(mut self, timestamp: u32) -> Self { + self.timestamp = timestamp; + self + } + + pub fn ssrc(mut self, ssrc: u32) -> Self { + self.ssrc = ssrc; + self + } + + pub fn payload(mut self, payload: Vec) -> Self { + self.payload = payload; + self + } + + pub fn build(self) -> Result, PacketConstructError> { + let packet_size = 12 + self.payload.len(); + let mut raw = Vec::with_capacity(packet_size); + raw.write_u8(self.version_flags) + .map_err(|_| PacketConstructError::PacketConstructFailed)?; + raw.write_u8(self.payload_type) + .map_err(|_| PacketConstructError::PacketConstructFailed)?; + raw.write_u16::(self.sequence) + .map_err(|_| PacketConstructError::PacketConstructFailed)?; + raw.write_u32::(self.timestamp) + .map_err(|_| PacketConstructError::PacketConstructFailed)?; + raw.write_u32::(self.ssrc) + .map_err(|_| PacketConstructError::PacketConstructFailed)?; + raw.extend_from_slice(&self.payload); + + Ok(RtpPacket:: { + version_flags: self.version_flags, + payload_type: self.payload_type, + sequence: self.sequence, + timestamp: self.timestamp, + ssrc: self.ssrc, + payload: self.payload, + is_silence: self.is_silence, + raw, + _marker: PhantomData, + }) + } +} + +mod tests { + const SECRET: [u8; 32] = [127; 32]; + use crate::crypto::AeadXChaCha20Poly1305RtpSize; + + use super::*; + + #[test] + fn encrypt_decrypt() { + let packet = RtpPacket::builder() + .ssrc(250) + .sequence(24) + .timestamp(100) + .payload(vec![1, 2, 3, 4, 5, 6, 7, 8]) + .build() + .expect("failed to construct packet"); + + let mut cryptor = AeadXChaCha20Poly1305RtpSize::default(); + + let encrypted = packet + .clone() + .encrypt(&mut cryptor, &SECRET) + .expect("failed to encrypt packet"); + + let decrypted = encrypted + .decrypt(&mut cryptor, &SECRET) + .expect("failed to decrypt packet"); + + assert_eq!(packet, decrypted); + } +} diff --git a/Aerovoice/Native/src/session.rs b/Aerovoice/Native/src/session.rs new file mode 100644 index 00000000..93806fe2 --- /dev/null +++ b/Aerovoice/Native/src/session.rs @@ -0,0 +1,521 @@ +use crate::{ + INPUT_DEVICE, OUTPUT_DEVICE, + crypto::Cryptor, + rtp::{Encrypted, RtpPacket}, + snowflake::Snowflake, +}; +use audiopus::{ + Application, Channels, SampleRate, + coder::{Decoder, Encoder}, +}; +use byteorder::{BigEndian, WriteBytesExt}; + +use cpal::{ + InputCallbackInfo, OutputCallbackInfo, Stream, StreamConfig, + traits::{DeviceTrait, StreamTrait as _}, +}; +use rtrb::{Consumer, Producer, RingBuffer}; +use std::{ + collections::{HashMap, hash_map::Entry}, + net::UdpSocket, + sync::mpsc::{self, Receiver, Sender}, + thread::{self, JoinHandle}, + time::Instant, +}; + +struct RecorderState { + packet_tx: Sender>, + encoder: Encoder, + ssrc: u32, + cryptor: Box, + sequence: u16, + instant: Instant, + secret: Vec, + last_silences: [bool; 24], +} + +#[derive(Debug)] +#[repr(C)] +pub struct IPInfo { + ip: [u8; 64], + port: u16, +} + +macro_rules! fallible { + ($fallible:expr, $return:expr) => { + match $fallible { + Ok(val) => val, + Err(e) => { + println!("Error: {}", e); + return $return; + } + } + }; + + ($fallible:expr) => { + match $fallible { + Ok(val) => val, + Err(e) => { + println!("Error: {}", e); + continue; + } + } + }; +} + +macro_rules! nullable { + ($nullable:expr, $return:expr) => { + match $nullable { + Some(val) => val, + None => { + println!("Error: None value encountered"); + return $return; + } + } + }; + + ($nullable:expr) => { + match $nullable { + Some(val) => val, + None => { + println!("Error: None value encountered"); + continue; + } + } + }; +} + +struct AudioUser { + pcm_producer: Producer, + stream: Stream, + decoder: Decoder, + speaking: bool, + was_speaking: bool, + ssrc: u32, +} + +struct ThreadInitialiser { + cryptor: Box, + secret: Vec, + socket: UdpSocket, +} + +#[repr(C)] +pub struct VoiceSession { + channel: Snowflake, + secret: Option>, + ip: String, + port: u16, + discovered_ip: Option, + socket: Option, + ssrc: u32, + recv_cryptor: Option>, + send_cryptor: Option>, + poll_thread: JoinHandle<()>, + poll_thread_initialiser: Sender, + poll_thread_killer: Sender<()>, + on_speaking: extern "C" fn(u32, bool), + packet_tx: Option>>, + recording_stream: Option, +} + +impl VoiceSession { + pub fn new( + ssrc: u32, + channel: Snowflake, + ip: String, + port: u16, + on_speaking: extern "C" fn(u32, bool), + ) -> Self { + println!("connecting to voice session: {}:{}", ip, port); + let socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind UDP socket"); + socket + .set_nonblocking(true) + .expect("failed to set non-blocking mode"); + + socket + .connect(format!("{}:{}", ip, port)) + .expect("failed to connect to UDP socket"); + + let (init_tx, init_rx) = mpsc::channel(); + let (packet_tx, packet_rx) = mpsc::channel(); + let (killer_tx, killer_rx) = mpsc::channel(); + + let poll_thread = thread::spawn(move || { + // wait for the initialiser + let initializer: ThreadInitialiser = + init_rx.recv().expect("failed to receive initialiser"); + let cryptor = initializer.cryptor; + let secret = initializer.secret; + let socket = initializer.socket; + let users = HashMap::new(); + + Self::poll_thread( + socket, + cryptor, + secret, + users, + packet_rx, + killer_rx, + on_speaking, + ); + }); + + Self { + channel, + secret: None, + ip, + port, + discovered_ip: None, + socket: Some(socket), + ssrc, + recv_cryptor: None, + send_cryptor: None, + poll_thread, + poll_thread_initialiser: init_tx, + poll_thread_killer: killer_tx, + on_speaking, + packet_tx: Some(packet_tx), + recording_stream: None, + } + } + + fn record_audio(data: &[i16], _: &InputCallbackInfo, recorder_state: &mut RecorderState) { + recorder_state.sequence = recorder_state.sequence.wrapping_add(1); + let mut packet = vec![0; 2048]; + + let length = match recorder_state.encoder.encode(data, &mut packet) { + Ok(length) => length, + Err(e) => { + println!("Error encoding audio: {}", e); + return; + } + }; + + let packet = &packet[..length]; + + const CLOCK_RATE: u32 = 48000; + let elapsed_seconds = recorder_state.instant.elapsed().as_secs_f64(); + let timestamp = (elapsed_seconds * CLOCK_RATE as f64) as u32; + let is_silent = !is_not_silent(data); + + recorder_state.last_silences.rotate_right(1); + recorder_state.last_silences[0] = is_silent; + + let is_silent = recorder_state.last_silences.iter().all(|&x| x); + + let packet = match RtpPacket::builder() + .ssrc(recorder_state.ssrc) + .sequence(recorder_state.sequence) + .timestamp(timestamp) + .payload(packet.to_vec()) + .silence(is_silent) + .build() + { + Ok(packet) => packet, + Err(e) => { + println!("Error building RTP packet: {}", e); + return; + } + }; + + let Ok(packet) = packet.encrypt( + recorder_state.cryptor.as_mut(), + recorder_state.secret.as_slice(), + ) else { + println!("failed to encrypt packet"); + return; + }; + + if let Err(e) = recorder_state.packet_tx.send(packet) { + println!("Error sending packet: {}", e); + } + } + + pub fn init_poll_thread(&mut self) { + let socket = self.socket.take().expect("socket is not set"); + let recv_cryptor = self.recv_cryptor.take().expect("cryptor is not set"); + let send_cryptor = self.send_cryptor.take().expect("cryptor is not set"); + let secret = self.secret.take().expect("secret is not set"); + let packet_tx = self.packet_tx.take().expect("packet tx is not set"); + let ssrc = self.ssrc; + + let thread_initialiser = ThreadInitialiser { + cryptor: recv_cryptor, + secret: secret.clone(), + socket, + }; + + self.poll_thread_initialiser + .send(thread_initialiser) + .expect("failed to send initialiser to poll thread"); + + let config = StreamConfig { + channels: 2, + sample_rate: cpal::SampleRate(48000), + buffer_size: cpal::BufferSize::Fixed(960), + }; + + println!( + "creating input stream with {}", + INPUT_DEVICE.name().unwrap_or_default() + ); + + let mut recorder_state = RecorderState { + packet_tx, + encoder: Encoder::new(SampleRate::Hz48000, Channels::Stereo, Application::Voip) + .expect("failed to create encoder"), + ssrc, + cryptor: send_cryptor, + sequence: 0, + instant: Instant::now(), + secret, + last_silences: [true; 24], + }; + + let input = match INPUT_DEVICE.build_input_stream( + &config, + move |data, info| { + Self::record_audio(data, info, &mut recorder_state); + }, + |e| println!("An error occured in the recorder: {}", e), + None, + ) { + Ok(stream) => stream, + Err(e) => { + println!("Error creating input stream: {}", e); + return; + } + }; + + if let Err(e) = input.play() { + println!("Error playing input stream: {}", e) + } + + self.recording_stream = Some(input); + } + + fn poll_thread( + socket: UdpSocket, + mut cryptor: Box, + secret: Vec, + mut users: HashMap, + packet_rx: Receiver>, + killer_rx: Receiver<()>, + on_speaking: extern "C" fn(u32, bool), + ) { + let mut last_silent = true; + let mut faux_packets = vec![]; + + loop { + if killer_rx.try_recv().is_ok() { + println!("killing poll thread"); + break; + } + + while let Ok(packet) = packet_rx.try_recv() { + let is_silent = packet.is_silent().unwrap_or(true); + if last_silent != is_silent { + (on_speaking)(packet.ssrc, !is_silent); + } + + if !is_silent { + if let Err(e) = socket.send(packet.raw()) { + println!("Error sending packet: {}", e); + } + } + + last_silent = is_silent; + + faux_packets.push(packet); + } + + let mut buf = [0; 1024]; + + let len = match socket.recv(&mut buf) { + Ok(len) => len, + Err(e) => { + if e.kind() != std::io::ErrorKind::WouldBlock { + println!("Error receiving data: {}", e); + } + + continue; + } + }; + + let packet = &buf[..len]; + + if packet[0] == 129 && packet[1] == 201 { + continue; + } + + // let Ok(packet) = RtpPacket::parse(packet) else { + // println!("failed to parse packet"); + // continue; + // }; + + let Some(packet) = faux_packets.pop() else { + continue; + }; + + if let Entry::Vacant(e) = users.entry(packet.ssrc) { + let decoder = fallible!(Decoder::new(SampleRate::Hz48000, Channels::Stereo)); + let config = StreamConfig { + channels: 2, + sample_rate: cpal::SampleRate(48000), + buffer_size: cpal::BufferSize::Default, + }; + + // allow up to 1024 packets in the buffer + let (producer, mut consumer) = RingBuffer::new(960 * 2 * 2 * 1024); + + let stream = OUTPUT_DEVICE.build_output_stream( + &config, + move |data, info| { + Self::process_audio(&mut consumer, data, info); + }, + move |err| { + eprintln!("Error: {}", err); + }, + None, + ); + + let stream = fallible!(stream); + let user = AudioUser { + stream, + pcm_producer: producer, + decoder, + speaking: false, + was_speaking: false, + ssrc: packet.ssrc, + }; + + fallible!(user.stream.play()); + + println!("a new user was detected with ssrc {}", packet.ssrc); + e.insert(user); + } + + let user = nullable!(users.get_mut(&packet.ssrc)); + let mut output = vec![0; 960 * 2]; + + if let Ok(packet) = packet.decrypt(cryptor.as_mut(), secret.as_slice()) { + if user + .decoder + .decode(Some(packet.payload()), &mut output, false) + .is_ok() + { + for byte in &output { + if let Err(e) = user.pcm_producer.push(*byte) { + println!("Error pushing to PCM buffer: {}", e); + } + } + + user.speaking = true; + } else { + user.speaking = false; + } + } else { + println!("failed to decrypt packet"); + user.speaking = false; + } + + if user.was_speaking != user.speaking { + (on_speaking)(user.ssrc, user.speaking); + } + + user.was_speaking = user.speaking; + } + } + + pub fn set_cryptor(&mut self, send_cryptor: Box, recv_cryptor: Box) { + self.recv_cryptor = Some(recv_cryptor); + self.send_cryptor = Some(send_cryptor); + } + + pub fn process_audio(pcm_buffer: &mut Consumer, data: &mut [i16], _: &OutputCallbackInfo) { + for item in data.iter_mut() { + match pcm_buffer.pop() { + Ok(sample) => *item = sample, + Err(_) => *item = 0, + }; + } + } + + pub fn discover_ip(&mut self) -> &IPInfo { + if let Some(ref ip_info) = self.discovered_ip { + return ip_info; + } + + println!("attempting to discover IP..."); + let Some(ref socket) = self.socket else { + panic!("socket is not set"); + }; + + let mut ip_discovery = Vec::with_capacity(2 + 2 + 4 + 64 + 2); + ip_discovery.write_u16::(0x1).unwrap(); + ip_discovery.write_u16::(70).unwrap(); + ip_discovery.write_u32::(self.ssrc).unwrap(); + let ip = self.ip.as_bytes(); + let mut padded_ip = [0; 64]; + padded_ip[..ip.len()].copy_from_slice(ip); + ip_discovery.extend_from_slice(&padded_ip); + ip_discovery.write_u16::(self.port).unwrap(); + + println!("written IP discovery packet, blocking until response"); + + socket + .send(&ip_discovery) + .expect("failed to send IP discovery packet"); + let mut buf = [0; 2 + 2 + 4 + 64 + 2]; + + loop { + match socket.recv_from(&mut buf) { + Ok(_) => { + break; + } + Err(e) => { + if e.kind() != std::io::ErrorKind::WouldBlock { + panic!("error receiving data: {}", e); + } + } + } + } + + let mut ip = [0; 64]; + ip.copy_from_slice(&buf[8..8 + 64]); + + println!("IP discovered!"); + + let port = u16::from_be_bytes([buf[buf.len() - 2], buf[buf.len() - 1]]); + let ip_info = IPInfo { ip, port }; + self.discovered_ip = Some(ip_info); + self.discovered_ip.as_ref().unwrap() + } + + pub fn set_secret(&mut self, secret: Vec) { + self.secret = Some(secret); + } +} + +impl Drop for VoiceSession { + fn drop(&mut self) { + self.poll_thread_killer + .send(()) + .expect("failed to send kill signal to poll thread"); + } +} + +fn is_not_silent(data: &[i16]) -> bool { + const THRESHOLD: f32 = 0.03; + let sum_squares: f32 = data + .iter() + .map(|&sample| { + let normalized = sample as f32 / i16::MAX as f32; + normalized * normalized + }) + .sum(); + + let rms = (sum_squares / data.len() as f32).sqrt(); + rms > THRESHOLD +} diff --git a/Aerovoice/Native/src/snowflake.rs b/Aerovoice/Native/src/snowflake.rs new file mode 100644 index 00000000..3f3c883c --- /dev/null +++ b/Aerovoice/Native/src/snowflake.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Snowflake(u64); + +impl Snowflake { + pub fn from_u64(value: u64) -> Self { + Snowflake(value) + } +}