1
0
mirror of https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git synced 2024-11-07 22:18:44 +00:00
VoiceCraft-MCBE_Proximity_Chat/VoiceCraft.Maui/Services/VoipService.cs
2024-09-03 11:40:41 +10:00

333 lines
12 KiB
C#

using NAudio.Wave;
using System.Diagnostics;
using VoiceCraft.Core;
using VoiceCraft.Core.Audio;
using VoiceCraft.Maui.Models;
using VoiceCraft.Maui.VoiceCraft;
namespace VoiceCraft.Maui.Services
{
public class VoipService
{
public const int SampleRate = 48000;
public const int Channels = 1;
public const int FrameSizeMS = 20;
//State Variables
public string StatusMessage { get; private set; } = "Connecting...";
private string Username = "";
//VOIP and Audio handler variables
public VoiceCraftClient Client { get; private set; }
private int RecordDetection;
private IWaveIn? AudioRecorder;
private IWavePlayer? AudioPlayer;
private SoftLimiter? Normalizer;
private readonly SettingsModel Settings;
private readonly ServerModel Server;
#region Delegates
public delegate void Started();
public delegate void Stopped(string? reason = null);
public delegate void Deny(string? reason = null);
public delegate void StatusUpdated(string status);
public delegate void SpeakingStarted();
public delegate void SpeakingStopped();
public delegate void ChannelAdded(Channel channel);
public delegate void ChannelRemoved(Channel channel);
public delegate void ChannelJoined(Channel channel);
public delegate void ChannelLeft(Channel channel);
public delegate void ParticipantJoined(VoiceCraftParticipant participant);
public delegate void ParticipantLeft(VoiceCraftParticipant participant);
public delegate void ParticipantUpdated(VoiceCraftParticipant participant);
public delegate void ParticipantStartedSpeaking(VoiceCraftParticipant participant);
public delegate void ParticipantStoppedSpeaking(VoiceCraftParticipant participant);
#endregion
#region Events
public event Started? OnStarted;
public event Stopped? OnStopped;
public event Deny? OnDeny;
public event StatusUpdated? OnStatusUpdated;
public event SpeakingStarted? OnSpeakingStarted;
public event SpeakingStopped? OnSpeakingStopped;
public event ChannelAdded? OnChannelAdded;
public event ChannelRemoved? OnChannelRemoved;
public event ChannelJoined? OnChannelJoined;
public event ChannelLeft? OnChannelLeft;
public event ParticipantJoined? OnParticipantJoined;
public event ParticipantLeft? OnParticipantLeft;
public event ParticipantUpdated? OnParticipantUpdated;
public event ParticipantStartedSpeaking? OnParticipantStartedSpeaking;
public event ParticipantStoppedSpeaking? OnParticipantStoppedSpeaking;
#endregion
public VoipService(ServerModel server)
{
Settings = Database.Instance.Settings;
Server = server;
Client = new VoiceCraftClient(new WaveFormat(SampleRate, Channels), FrameSizeMS, Settings.ClientPort, Settings.JitterBufferSize)
{
LinearProximity = Settings.LinearVolume,
UseCustomProtocol = Settings.CustomClientProtocol,
DirectionalHearing = Settings.DirectionalAudioEnabled
};
Client.OnConnected += ClientConnected;
Client.OnDisconnected += ClientDisconnected;
Client.OnFailed += ClientFailed;
Client.OnDeny += ClientDeny;
Client.OnBinded += ClientBinded;
Client.OnUnbinded += ClientUnbinded;
Client.OnChannelAdded += ClientChannelAdded;
Client.OnChannelRemoved += ClientChannelRemoved;
Client.OnChannelJoined += ClientChannelJoined;
Client.OnChannelLeft += ClientChannelLeft;
Client.OnParticipantJoined += ClientParticipantJoined;
Client.OnParticipantLeft += ClientParticipantLeft;
Client.OnParticipantUpdated += ClientParticipantUpdated;
}
public async Task StartAsync(CancellationToken CT)
{
await Task.Run(async () =>
{
var audioManager = new AudioManager();
if (Settings.SoftLimiterEnabled)
{
Normalizer = new SoftLimiter(Client.AudioOutput);
Normalizer.Boost.CurrentValue = Settings.SoftLimiterGain;
AudioPlayer = audioManager.CreatePlayer(Normalizer);
}
else
{
AudioPlayer = audioManager.CreatePlayer(Client.AudioOutput);
}
AudioRecorder = audioManager.CreateRecorder(Client.AudioFormat, FrameSizeMS);
AudioRecorder.DataAvailable += DataAvailable;
AudioRecorder.RecordingStopped += RecordingStopped;
try
{
Client.Connect(Server.IP, (ushort)Server.Port, Server.Key, Settings.ClientSidedPositioning ? Core.PositioningTypes.ClientSided : Core.PositioningTypes.ServerSided);
await StartLogicLoop(CT);
if (AudioPlayer.PlaybackState != PlaybackState.Playing) AudioPlayer.Play();
}
catch (OperationCanceledException)
{ }
catch (Exception ex)
{
OnStopped?.Invoke(ex.Message);
}
finally
{
Client.OnConnected -= ClientConnected;
Client.OnDisconnected -= ClientDisconnected;
Client.OnDeny -= ClientDeny;
Client.OnBinded -= ClientBinded;
Client.OnUnbinded -= ClientUnbinded;
Client.OnChannelAdded -= ClientChannelAdded;
Client.OnChannelRemoved -= ClientChannelRemoved;
Client.OnChannelJoined -= ClientChannelJoined;
Client.OnChannelLeft -= ClientChannelLeft;
Client.OnParticipantJoined -= ClientParticipantJoined;
Client.OnParticipantLeft -= ClientParticipantLeft;
Client.OnParticipantUpdated -= ClientParticipantUpdated;
AudioRecorder.DataAvailable -= DataAvailable;
AudioRecorder.RecordingStopped -= RecordingStopped;
if (AudioPlayer.PlaybackState == PlaybackState.Playing)
AudioPlayer.Stop();
AudioRecorder.StopRecording();
AudioPlayer.Dispose();
AudioRecorder.Dispose();
Client.Disconnect();
Client.Dispose();
}
}, CT);
}
private async Task StartLogicLoop(CancellationToken CT)
{
OnStarted?.Invoke();
var talkingParticipants = new List<VoiceCraftParticipant>();
bool previousSpeakingState = false;
while (true)
{
await Task.Delay(200, CT);
try
{
var currentSpeakingState = Environment.TickCount - (long)RecordDetection < 500;
if (previousSpeakingState != currentSpeakingState)
{
if(currentSpeakingState)
OnSpeakingStarted?.Invoke();
else
OnSpeakingStopped?.Invoke();
previousSpeakingState = currentSpeakingState;
}
//Participant Talking Logic.
var oldPart = talkingParticipants.Where(x => Environment.TickCount64 - x.LastSpoke >= 500).ToArray();
foreach (var participant in oldPart)
{
talkingParticipants.Remove(participant);
OnParticipantStoppedSpeaking?.Invoke(participant);
}
var newPart = Client.Participants.Where(x => Environment.TickCount64 - x.Value.LastSpoke < 500);
foreach (var participant in newPart)
{
talkingParticipants.Add(participant.Value);
OnParticipantStartedSpeaking?.Invoke(participant.Value);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}
//Audio Events
private void DataAvailable(object? sender, WaveInEventArgs e)
{
if (Client.Muted || Client.Deafened)
return;
float max = 0;
// interpret as 16 bit audio
for (int index = 0; index < e.BytesRecorded; index += 2)
{
short sample = (short)((e.Buffer[index + 1] << 8) |
e.Buffer[index + 0]);
// to floating point
var sample32 = sample / 32768f;
// absolute value
if (sample32 < 0) sample32 = -sample32;
if (sample32 > max) max = sample32;
}
if (max >= Settings.MicrophoneDetectionPercentage)
{
RecordDetection = Environment.TickCount;
}
if (Environment.TickCount - (long)RecordDetection < 1000)
{
Client.SendAudio(e.Buffer, e.BytesRecorded);
}
}
private void RecordingStopped(object? sender, StoppedEventArgs e)
{
AudioRecorder?.StartRecording();
}
#region Event Methods
private void ClientConnected()
{
if(Client.PositioningType == PositioningTypes.ServerSided)
{
StatusMessage = $"Connected! Key - {Client.Key}\nWaiting for binding...";
}
else if(Settings.CustomClientProtocol)
{
StatusMessage = $"Connected!\nWaiting for client on port {Settings.ClientPort}";
}
else
{
StatusMessage = $"Connected!\nWaiting for MCWSS on port {Settings.ClientPort}";
}
OnStatusUpdated?.Invoke(StatusMessage);
}
private void ClientDisconnected(string? reason = null)
{
OnStopped?.Invoke(reason);
}
private void ClientFailed(Exception ex)
{
OnStopped?.Invoke(ex.Message);
}
private void ClientDeny(string? reason = null)
{
OnDeny?.Invoke(reason);
}
private void ClientBinded(string name)
{
Username = name ?? "<N.A.>";
StatusMessage = Client.PositioningType == PositioningTypes.ServerSided? $"Connected - Key: {Client.Key}\n{Username}" : $"Connected\n{Username}";
//Last step of verification. We start sending data and playing any received data.
try
{
AudioRecorder?.StartRecording();
AudioPlayer?.Play();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
} //Do nothing. This is just to make sure that the recorder and player is working.
OnStatusUpdated?.Invoke(StatusMessage);
}
private void ClientUnbinded()
{
if (Settings.CustomClientProtocol)
{
StatusMessage = $"Connected! Key\nDisconnected connection!";
}
else
{
StatusMessage = $"Connected! Key\nMCWSS Disconnected!";
}
OnStatusUpdated?.Invoke(StatusMessage);
}
private void ClientChannelAdded(Core.Channel channel)
{
OnChannelAdded?.Invoke(channel);
}
private void ClientChannelRemoved(Core.Channel channel)
{
OnChannelRemoved?.Invoke(channel);
}
private void ClientChannelJoined(Core.Channel channel)
{
OnChannelJoined?.Invoke(channel);
}
private void ClientChannelLeft(Core.Channel channel)
{
OnChannelLeft?.Invoke(channel);
}
private void ClientParticipantJoined(VoiceCraftParticipant participant)
{
OnParticipantJoined?.Invoke(participant);
}
private void ClientParticipantLeft(VoiceCraftParticipant participant)
{
OnParticipantLeft?.Invoke(participant);
}
private void ClientParticipantUpdated(VoiceCraftParticipant participant)
{
OnParticipantUpdated?.Invoke(participant);
}
#endregion
}
}