diff --git a/Aerochat/Aerochat.csproj b/Aerochat/Aerochat.csproj index 09b96947..523c948f 100644 --- a/Aerochat/Aerochat.csproj +++ b/Aerochat/Aerochat.csproj @@ -1,4 +1,4 @@ - + Static @@ -613,27 +613,33 @@ - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/Aerochat/App.xaml.cs b/Aerochat/App.xaml.cs index 7cf99363..1eb3ab0c 100644 --- a/Aerochat/App.xaml.cs +++ b/Aerochat/App.xaml.cs @@ -1,4 +1,4 @@ -using Aerochat.Hoarder; +using Aerochat.Hoarder; using Aerochat.ViewModels; using Aerochat.Windows; using Aerochat.Settings; @@ -550,7 +550,7 @@ await Current.Dispatcher.InvokeAsync(() => }); noti.Show(); - mediaPlayer.Open(new Uri("Resources/Sounds/online.wav", UriKind.Relative)); + mediaPlayer.Open(Helpers.SoundHelper.GetSoundUri("online.wav")); }); }; @@ -627,13 +627,13 @@ await Current.Dispatcher.InvokeAsync(() => if (isNudge) { - if (SettingsManager.Instance.PlayNotificationSounds) //toggle sound - mediaPlayer.Open(new Uri("Resources/Sounds/nudge.wav", UriKind.Relative)); + if (SettingsManager.Instance.PlayNotificationSounds) + mediaPlayer.Open(Helpers.SoundHelper.GetSoundUri("nudge.wav")); } else { if (SettingsManager.Instance.PlayNotificationSounds) - mediaPlayer.Open(new Uri("Resources/Sounds/type.wav", UriKind.Relative)); + mediaPlayer.Open(Helpers.SoundHelper.GetSoundUri("type.wav")); } }); }; diff --git a/Aerochat/Helpers/SoundHelper.cs b/Aerochat/Helpers/SoundHelper.cs new file mode 100644 index 00000000..bb7e3b8d --- /dev/null +++ b/Aerochat/Helpers/SoundHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Windows.Media; + +namespace Aerochat.Helpers +{ + public static class SoundHelper + { + private static readonly string SoundBase = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "Sounds"); + + private static MediaPlayer _sfxPlayer; + + public static Uri GetSoundUri(string fileName) + { + return new Uri(Path.Combine(SoundBase, fileName), UriKind.Absolute); + } + + public static void PlaySound(string fileName) + { + if (_sfxPlayer == null) + { + _sfxPlayer = new MediaPlayer(); + _sfxPlayer.MediaOpened += (s, e) => _sfxPlayer.Play(); + } + _sfxPlayer.Open(GetSoundUri(fileName)); + } + } +} diff --git a/Aerochat/Resources/Sounds/joincall.wav b/Aerochat/Resources/Sounds/joincall.wav new file mode 100644 index 00000000..964bb460 Binary files /dev/null and b/Aerochat/Resources/Sounds/joincall.wav differ diff --git a/Aerochat/Resources/Sounds/leavecall.wav b/Aerochat/Resources/Sounds/leavecall.wav new file mode 100644 index 00000000..bf45d22a Binary files /dev/null and b/Aerochat/Resources/Sounds/leavecall.wav differ diff --git a/Aerochat/ViewModels/User.cs b/Aerochat/ViewModels/User.cs index 2173e8cd..74054d8b 100644 --- a/Aerochat/ViewModels/User.cs +++ b/Aerochat/ViewModels/User.cs @@ -1,4 +1,4 @@ -using DSharpPlus.Entities; +using DSharpPlus.Entities; using System; using System.Collections.Generic; using System.Linq; @@ -20,6 +20,13 @@ public class UserViewModel : ViewModelBase private SceneViewModel? _scene; private string? _color = "#525252"; private string? _image; + private bool _isSpeaking; + + public bool IsSpeaking + { + get => _isSpeaking; + set => SetProperty(ref _isSpeaking, value); + } public required string Name { diff --git a/Aerochat/Voice/VoiceManager.cs b/Aerochat/Voice/VoiceManager.cs index 862e5423..b9e28e48 100644 --- a/Aerochat/Voice/VoiceManager.cs +++ b/Aerochat/Voice/VoiceManager.cs @@ -1,14 +1,13 @@ -using Aerochat.Hoarder; +using Aerochat.Hoarder; using Aerochat.ViewModels; using Aerochat.Windows; using Aerovoice.Clients; using DSharpPlus.Entities; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; -using System.Windows; namespace Aerochat.Voice { @@ -25,10 +24,81 @@ public ChannelViewModel? ChannelVM set => SetProperty(ref _channelVM, value); } + public event EventHandler<(ulong UserId, bool IsSpeaking)> UserSpeakingChanged; + public event EventHandler ClientSpeakingChanged; + + private readonly ConcurrentDictionary _userVolumes = new(); + private readonly ConcurrentDictionary _userMuted = new(); + + public bool SelfMuted + { + get => voiceSocket?.SelfMuted ?? false; + set { if (voiceSocket != null) voiceSocket.SelfMuted = value; } + } + + public bool SelfDeafened + { + get => voiceSocket?.SelfDeafened ?? false; + set { if (voiceSocket != null) voiceSocket.SelfDeafened = value; } + } + + public float ClientTransmitVolume + { + get => voiceSocket?.Player.ClientTransmitVolume ?? 1.0f; + set { if (voiceSocket != null) voiceSocket.Player.ClientTransmitVolume = value; } + } + + public void SetUserVolume(ulong userId, float volume) + { + _userVolumes[userId] = volume; + ApplyVolumeForUser(userId); + } + + public float GetUserVolume(ulong userId) + { + return _userVolumes.TryGetValue(userId, out var vol) ? vol : 1.0f; + } + + public void SetUserMuted(ulong userId, bool muted) + { + _userMuted[userId] = muted; + ApplyMuteForUser(userId); + } + + public bool IsUserMuted(ulong userId) + { + return _userMuted.TryGetValue(userId, out var m) && m; + } + + private IEnumerable GetSsrcsForUser(ulong userId) + { + if (voiceSocket == null) return Array.Empty(); + return voiceSocket.SsrcToUserId + .Where(kvp => kvp.Value == userId) + .Select(kvp => kvp.Key); + } + + private void ApplyVolumeForUser(ulong userId) + { + if (voiceSocket == null) return; + float vol = _userVolumes.TryGetValue(userId, out var v) ? v : 1.0f; + foreach (var ssrc in GetSsrcsForUser(userId)) + voiceSocket.Player.SetSsrcVolume(ssrc, vol); + } + + private void ApplyMuteForUser(ulong userId) + { + if (voiceSocket == null) return; + bool muted = _userMuted.TryGetValue(userId, out var m) && m; + foreach (var ssrc in GetSsrcsForUser(userId)) + voiceSocket.Player.SetSsrcMuted(ssrc, muted); + } + public async Task LeaveVoiceChannel() { if (voiceSocket is null) return; + UnsubscribeEvents(); await voiceSocket.DisconnectAndDispose(); voiceSocket = null; ChannelVM = null; @@ -38,9 +108,40 @@ public async Task JoinVoiceChannel(DiscordChannel channel) { await LeaveVoiceChannel(); voiceSocket = new(Discord.Client); + SubscribeEvents(); await voiceSocket.ConnectAsync(channel); voiceSocket.Recorder.SetInputDevice(Settings.SettingsManager.Instance.InputDeviceIndex); ChannelVM = ChannelViewModel.FromChannel(channel); } + + private void SubscribeEvents() + { + if (voiceSocket == null) return; + voiceSocket.UserSpeakingChanged += OnUserSpeakingChanged; + voiceSocket.ClientSpeakingChanged += OnClientSpeakingChanged; + } + + private void UnsubscribeEvents() + { + if (voiceSocket == null) return; + voiceSocket.UserSpeakingChanged -= OnUserSpeakingChanged; + voiceSocket.ClientSpeakingChanged -= OnClientSpeakingChanged; + } + + private void OnUserSpeakingChanged(object? sender, (ulong UserId, bool IsSpeaking) e) + { + if (voiceSocket == null) return; + if (e.IsSpeaking) + { + ApplyMuteForUser(e.UserId); + ApplyVolumeForUser(e.UserId); + } + UserSpeakingChanged?.Invoke(this, e); + } + + private void OnClientSpeakingChanged(object? sender, bool isSpeaking) + { + ClientSpeakingChanged?.Invoke(this, isSpeaking); + } } } diff --git a/Aerochat/Windows/Chat.xaml b/Aerochat/Windows/Chat.xaml index 467667f2..cb701585 100644 --- a/Aerochat/Windows/Chat.xaml +++ b/Aerochat/Windows/Chat.xaml @@ -1,4 +1,4 @@ - - - - - + + + + + + + + + diff --git a/Aerochat/Windows/Chat.xaml.cs b/Aerochat/Windows/Chat.xaml.cs index cf887495..89364c60 100644 --- a/Aerochat/Windows/Chat.xaml.cs +++ b/Aerochat/Windows/Chat.xaml.cs @@ -1,4 +1,4 @@ -using Aerochat.Helpers; +using Aerochat.Helpers; using Aerochat.Hoarder; using Aerochat.Services; using Aerochat.Settings; @@ -141,6 +141,8 @@ public Chat(ulong id, bool allowDefault = false, PresenceViewModel? initialPrese _chatService.ChannelUpdated += OnChannelUpdated; _chatService.PresenceUpdated += OnPresenceUpdated; _chatService.VoiceStateUpdated += OnVoiceStateUpdated; + VoiceManager.Instance.UserSpeakingChanged += OnUserSpeakingChanged; + VoiceManager.Instance.ClientSpeakingChanged += OnClientSpeakingChanged; ViewModel.UndoEnabled = false; ViewModel.RedoEnabled = false; @@ -651,6 +653,8 @@ protected override void OnClosing(CancelEventArgs e) } catch (Exception) { } base.OnClosing(e); + VoiceManager.Instance.UserSpeakingChanged -= OnUserSpeakingChanged; + VoiceManager.Instance.ClientSpeakingChanged -= OnClientSpeakingChanged; _chatService.TypingStarted -= OnType; _chatService.MessageCreated -= OnMessageCreation; // dispose of the chat @@ -729,14 +733,14 @@ await Dispatcher.BeginInvoke(() => if (isNudge) { - chatSoundPlayer.Open(new Uri("Resources/Sounds/nudge.wav", UriKind.Relative)); + chatSoundPlayer.Open(SoundHelper.GetSoundUri("nudge.wav")); } else { if (IsActive && message.Author?.Id != _chatService.GetCurrentUser().Result.Id) { if (SettingsManager.Instance.NotifyChat || message.MessageEntity.MentionedUsers.Contains(_chatService.GetCurrentUser().Result)) // IDK - chatSoundPlayer.Open(new Uri("Resources/Sounds/type.wav", UriKind.Relative)); + chatSoundPlayer.Open(SoundHelper.GetSoundUri("type.wav")); } } }); @@ -971,8 +975,66 @@ private void RefreshAerochatVersionLinkVisibility() private async Task OnVoiceStateUpdated(DiscordClient sender, DSharpPlus.EventArgs.VoiceStateUpdateEventArgs args) { - if (args.Guild.Id != Channel.Guild?.Id) return; - Dispatcher.BeginInvoke(RefreshChannelList); + var currentUser = await _chatService.GetCurrentUser(); + bool isMe = args.User.Id == currentUser.Id; + + ulong? beforeId = args.Before?.Channel?.Id; + ulong? afterId = args.After?.Channel?.Id; + bool channelChanged = beforeId != afterId; + + if (channelChanged) + { + bool joined = afterId != null; + bool left = beforeId != null; + + if (isMe) + { + if (joined) + Dispatcher.BeginInvoke(() => chatSoundPlayer.Open(SoundHelper.GetSoundUri("joincall.wav"))); + else if (left) + Dispatcher.BeginInvoke(() => chatSoundPlayer.Open(SoundHelper.GetSoundUri("leavecall.wav"))); + } + else + { + var myChannel = VoiceManager.Instance.Channel; + if (myChannel != null) + { + if (joined && afterId == myChannel.Id) + Dispatcher.BeginInvoke(() => chatSoundPlayer.Open(SoundHelper.GetSoundUri("joincall.wav"))); + else if (left && beforeId == myChannel.Id) + Dispatcher.BeginInvoke(() => chatSoundPlayer.Open(SoundHelper.GetSoundUri("leavecall.wav"))); + } + } + } + + if (args.Guild.Id == Channel.Guild?.Id) + Dispatcher.BeginInvoke(RefreshChannelList); + } + + private void OnUserSpeakingChanged(object? sender, (ulong UserId, bool IsSpeaking) e) + { + Dispatcher.BeginInvoke(() => UpdateSpeakingState(e.UserId, e.IsSpeaking)); + } + + private void OnClientSpeakingChanged(object? sender, bool isSpeaking) + { + var currentUser = _chatService.GetCurrentUser().Result; + Dispatcher.BeginInvoke(() => UpdateSpeakingState(currentUser.Id, isSpeaking)); + } + + private void UpdateSpeakingState(ulong userId, bool isSpeaking) + { + foreach (var cat in ViewModel.Categories) + { + foreach (var item in cat.Items) + { + foreach (var user in item.ConnectedUsers) + { + if (user.Id == userId) + user.IsSpeaking = isSpeaking; + } + } + } } private async Task OnPresenceUpdated(DiscordClient sender, DSharpPlus.EventArgs.PresenceUpdateEventArgs args) @@ -1826,6 +1888,75 @@ private async void LeaveCallButton_PreviewMouseLeftButtonUp(object sender, Mouse await VoiceManager.Instance.LeaveVoiceChannel(); } + private void VoiceUserContextMenu_Open(object sender, MouseButtonEventArgs e) + { + var border = sender as FrameworkElement; + if (border?.DataContext is not UserViewModel user) return; + + var currentUser = _chatService.GetCurrentUser().Result; + bool isMe = user.Id == currentUser.Id; + + var menu = new ContextMenu(); + + var profileItem = new MenuItem { Header = "Profile" }; + menu.Items.Add(profileItem); + menu.Items.Add(new Separator()); + + bool isMuted = isMe ? VoiceManager.Instance.SelfMuted : VoiceManager.Instance.IsUserMuted(user.Id); + var muteItem = new MenuItem { Header = isMuted ? "Unmute" : "Mute", IsCheckable = false }; + muteItem.Click += (s, _) => + { + if (isMe) + VoiceManager.Instance.SelfMuted = !VoiceManager.Instance.SelfMuted; + else + VoiceManager.Instance.SetUserMuted(user.Id, !VoiceManager.Instance.IsUserMuted(user.Id)); + }; + menu.Items.Add(muteItem); + + if (isMe) + { + bool isDeafened = VoiceManager.Instance.SelfDeafened; + var deafenItem = new MenuItem { Header = isDeafened ? "Undeafen" : "Deafen", IsCheckable = false }; + deafenItem.Click += (s, _) => + { + VoiceManager.Instance.SelfDeafened = !VoiceManager.Instance.SelfDeafened; + }; + menu.Items.Add(deafenItem); + } + + menu.Items.Add(new Separator()); + + var volumeMenu = new MenuItem { Header = "Volume" }; + float currentVol = isMe + ? VoiceManager.Instance.ClientTransmitVolume + : VoiceManager.Instance.GetUserVolume(user.Id); + + int[] volumeSteps = { 0, 25, 50, 75, 100, 125, 150, 175, 200 }; + foreach (int pct in volumeSteps) + { + float vol = pct / 100f; + var volItem = new MenuItem + { + Header = $"{pct}%", + IsCheckable = true, + IsChecked = Math.Abs(currentVol - vol) < 0.01f + }; + volItem.Click += (s, _) => + { + if (isMe) + VoiceManager.Instance.ClientTransmitVolume = vol; + else + VoiceManager.Instance.SetUserVolume(user.Id, vol); + }; + volumeMenu.Items.Add(volItem); + } + menu.Items.Add(volumeMenu); + + menu.PlacementTarget = border; + menu.IsOpen = true; + e.Handled = true; + } + private string GetMessageBoxText() // returns full text & converts all emoticon images to Unicode emojis { StringBuilder sb = new StringBuilder(); diff --git a/Aerovoice/Clients/VoiceSocket.cs b/Aerovoice/Clients/VoiceSocket.cs index 65ec2f01..1ebd2629 100644 --- a/Aerovoice/Clients/VoiceSocket.cs +++ b/Aerovoice/Clients/VoiceSocket.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -35,14 +35,12 @@ public class VoiceSocket private JObject _ready; private bool _disposed = false; - - public IPlayer Player = new NAudioPlayer(); + public NAudioPlayer 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 int _sequence; @@ -56,18 +54,52 @@ public class VoiceSocket private uint _ssrc = 0; public bool Speaking { get; private set; } = false; + public bool SelfMuted { get; set; } = false; + public bool SelfDeafened { get; set; } = false; + + public readonly ConcurrentDictionary SsrcToUserId = new(); + private readonly ConcurrentDictionary _lastSpeakingTime = new(); + private const double SpeakingTimeoutMs = 300; + private const double IncomingRmsThreshold = 300; + + public event EventHandler<(ulong UserId, bool IsSpeaking)> UserSpeakingChanged; + public event EventHandler ClientSpeakingChanged; + + public ulong GetUserIdFromSsrc(uint ssrc) + { + return SsrcToUserId.TryGetValue(ssrc, out var userId) ? userId : 0; + } System.Timers.Timer _timer; + System.Timers.Timer _speakingDecayTimer; public VoiceSocket(DiscordClient client) { _client = client; - // every 1/60 seconds, on another thread, incrementTimestamp using 3840 _timer = new(); _timer.Interval = 16.666666666666668; _timer.AutoReset = true; _timer.Elapsed += (s, e) => _timestamp.Increment(3840); _timer.Start(); + + _speakingDecayTimer = new(); + _speakingDecayTimer.Interval = 150; + _speakingDecayTimer.AutoReset = true; + _speakingDecayTimer.Elapsed += SpeakingDecayTimer_Elapsed; + _speakingDecayTimer.Start(); + } + + private void SpeakingDecayTimer_Elapsed(object? sender, ElapsedEventArgs e) + { + var now = DateTime.UtcNow; + foreach (var kvp in _lastSpeakingTime) + { + if ((now - kvp.Value).TotalMilliseconds > SpeakingTimeoutMs) + { + if (_lastSpeakingTime.TryRemove(kvp.Key, out _)) + UserSpeakingChanged?.Invoke(this, (kvp.Key, false)); + } + } } public async Task SendMessage(JObject message) @@ -138,7 +170,6 @@ public async Task OnMessageReceived(JObject message) } 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) @@ -147,6 +178,14 @@ public async Task OnMessageReceived(JObject message) } break; } + case 5: // speaking + { + var d = message["d"]!; + var userId = d["user_id"]!.Value(); + var ssrc = d["ssrc"]!.Value(); + SsrcToUserId[ssrc] = userId; + break; + } } } @@ -239,16 +278,45 @@ private void TryProcessBufferedPackets() private void ProcessPacket(byte[] e) { + if (SelfDeafened) return; + var ssrc = BinaryPrimitives.ReadUInt32BigEndian(e.AsSpan(8)); + if (Player.IsSsrcMuted(ssrc)) return; + 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); + + var userId = GetUserIdFromSsrc(ssrc); + if (userId != 0) + { + double rms = ComputeRms(decoded, decodedLength); + if (rms >= IncomingRmsThreshold) + { + bool wasNew = !_lastSpeakingTime.ContainsKey(userId); + _lastSpeakingTime[userId] = DateTime.UtcNow; + if (wasNew) + UserSpeakingChanged?.Invoke(this, (userId, true)); + } + } + Player.AddSamples(decoded, decodedLength, ssrc); } + private static double ComputeRms(byte[] pcm, int length) + { + int samples = length / 2; + double sum = 0; + for (int i = 0; i < length - 1; i += 2) + { + short sample = (short)((pcm[i + 1] << 8) | pcm[i]); + sum += sample * sample; + } + return Math.Sqrt(sum / samples); + } + 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)!)); @@ -331,13 +399,11 @@ 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 (sampleIsSpeaking || SelfMuted) { if (Speaking) { @@ -346,13 +412,14 @@ await Task.Run(() => op = 5, d = new { - speaking = 0, // not speaking + speaking = 0, delay = 0, ssrc = _ssrc } })); + Speaking = false; + ClientSpeakingChanged?.Invoke(this, false); } - Speaking = false; return; } @@ -363,22 +430,29 @@ await Task.Run(() => op = 5, d = new { - speaking = 1 << 0, // VOICE + speaking = 1 << 0, delay = 0, ssrc = _ssrc } })); Speaking = true; + ClientSpeakingChanged?.Invoke(this, true); } if (cryptor is null) return; - var opus = Encoder.Encode(e); + + byte[] toEncode = e; + float vol = Player.ClientTransmitVolume; + if (Math.Abs(vol - 1.0f) > 0.01f) + toEncode = ScalePcm(e, vol); + + var opus = Encoder.Encode(toEncode); 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 + header[0] = 0x80; + header[1] = 0x78; + BinaryPrimitives.WriteInt16BigEndian(header.AsSpan(2), _udpSequence++); + BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(4), _timestamp.GetCurrentTimestamp()); + BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(8), _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); @@ -387,6 +461,20 @@ await Task.Run(() => }); } + private static byte[] ScalePcm(byte[] pcm, float volume) + { + var result = new byte[pcm.Length]; + for (int i = 0; i < pcm.Length - 1; i += 2) + { + short sample = (short)((pcm[i + 1] << 8) | pcm[i]); + int scaled = (int)(sample * volume); + scaled = Math.Clamp(scaled, short.MinValue, short.MaxValue); + result[i] = (byte)(scaled & 0xFF); + result[i + 1] = (byte)((scaled >> 8) & 0xFF); + } + return result; + } + private void AddToCircularBuffer(byte[] data) { int dataLength = data.Length; @@ -480,7 +568,6 @@ await SendMessage(JObject.FromObject(new public async Task DisconnectAndDispose() { - // if the socket isn't connected, return if (!_connected) return; _connected = false; _disposed = true; @@ -489,6 +576,7 @@ public async Task DisconnectAndDispose() UdpClient?.Dispose(); Recorder?.Dispose(); _timer.Dispose(); + _speakingDecayTimer.Dispose(); Encoder?.Dispose(); } } diff --git a/Aerovoice/Players/NAudioPlayer.cs b/Aerovoice/Players/NAudioPlayer.cs index 111524db..40530d6c 100644 --- a/Aerovoice/Players/NAudioPlayer.cs +++ b/Aerovoice/Players/NAudioPlayer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using NAudio.Wave; using NAudio.Wave.SampleProviders; @@ -8,10 +8,14 @@ namespace Aerovoice.Players public class NAudioPlayer : IPlayer { private readonly ConcurrentDictionary _waveProviders = new(); + private readonly ConcurrentDictionary _volumeProviders = new(); + private readonly ConcurrentDictionary _mutedSsrcs = new(); private readonly MixingSampleProvider _mixer; private readonly WaveOutEvent _waveOut; private readonly WaveFormat _format = new(48000, 16, 2); + public float ClientTransmitVolume { get; set; } = 1.0f; + public NAudioPlayer() { _mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(48000, 2)); @@ -21,6 +25,27 @@ public NAudioPlayer() _waveOut.Play(); } + public void SetSsrcVolume(uint ssrc, float volume) + { + if (_volumeProviders.TryGetValue(ssrc, out var vp)) + vp.Volume = volume; + } + + public void SetSsrcMuted(uint ssrc, bool muted) + { + _mutedSsrcs[ssrc] = muted; + } + + public bool IsSsrcMuted(uint ssrc) + { + return _mutedSsrcs.TryGetValue(ssrc, out var muted) && muted; + } + + public float GetSsrcVolume(uint ssrc) + { + return _volumeProviders.TryGetValue(ssrc, out var vp) ? vp.Volume : 1.0f; + } + public void AddSamples(byte[] pcmData, int pcmLength, uint ssrc) { if (!_waveProviders.TryGetValue(ssrc, out var waveProvider)) @@ -30,11 +55,13 @@ public void AddSamples(byte[] pcmData, int pcmLength, uint ssrc) DiscardOnBufferOverflow = true }; var sampleProvider = waveProvider.ToSampleProvider(); + var volumeProvider = new VolumeSampleProvider(sampleProvider); _waveProviders.TryAdd(ssrc, waveProvider); - _mixer.AddMixerInput(sampleProvider); + _volumeProviders.TryAdd(ssrc, volumeProvider); + _mixer.AddMixerInput(volumeProvider); } _waveProviders[ssrc].AddSamples(pcmData, 0, pcmLength); } } -} \ No newline at end of file +}