using Observatory.Framework; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using NetCoreAudio; using System.Threading; using System; using System.Diagnostics; namespace Observatory.Herald { class HeraldQueue { private Queue notifications; private bool processing; private string voice; private string style; private string rate; private byte volume; private SpeechRequestManager speechManager; private Player audioPlayer; private Action ErrorLogger; public HeraldQueue(SpeechRequestManager speechManager, Action errorLogger) { this.speechManager = speechManager; processing = false; notifications = new(); audioPlayer = new(); ErrorLogger = errorLogger; } internal void Enqueue(NotificationArgs notification, string selectedVoice, string selectedStyle = "", string selectedRate = "", int volume = 75) { voice = selectedVoice; style = selectedStyle; rate = selectedRate; // Ignore invalid values; assume default. volume = volume >= 0 && volume <= 100 ? volume : 75; // Volume is perceived logarithmically, convert to exponential curve // to make perceived volume more in line with value set. this.volume = ((byte)System.Math.Floor(System.Math.Pow(volume / 100.0, 2.0) * 100)); Debug.WriteLine("Attempting to de-dupe notification titles against '{0}': '{1}'", notification.Title.Trim().ToLower(), String.Join(',', notifications.Select(n => n.Title.Trim().ToLower()))); if (notifications.Where(n => n.Title.Trim().ToLower() == notification.Title.Trim().ToLower()).Any()) { // Suppress title. notification.Suppression |= NotificationSuppression.Title; } notifications.Enqueue(notification); if (!processing) { processing = true; ProcessQueueAsync(); } } private async void ProcessQueueAsync() { await Task.Factory.StartNew(ProcessQueue); } private void ProcessQueue() { Thread.Sleep(200); // Allow time for other notifications to arrive. NotificationArgs notification = null; try { while (notifications.Any()) { audioPlayer.SetVolume(volume).Wait(); notification = notifications.Dequeue(); Debug.WriteLine("Processing notification: {0} - {1}", notification.Title, notification.Detail); List> audioRequestTasks = new(); if (!notification.Suppression.HasFlag(NotificationSuppression.Title)) { audioRequestTasks.Add(string.IsNullOrWhiteSpace(notification.TitleSsml) ? RetrieveAudioToFile(notification.Title) : RetrieveAudioSsmlToFile(notification.TitleSsml)); } if (!notification.Suppression.HasFlag(NotificationSuppression.Detail)) { audioRequestTasks.Add(string.IsNullOrWhiteSpace(notification.DetailSsml) ? RetrieveAudioToFile(notification.Detail) : RetrieveAudioSsmlToFile(notification.DetailSsml)); } PlayAudioRequestsSequentially(audioRequestTasks); } } catch (Exception ex) { Debug.WriteLine($"Failed to fetch/play notification: {notification?.Title} - {notification?.Detail}"); ErrorLogger(ex, "while retrieving and playing audio for a notification"); } finally { processing = false; } } private async Task RetrieveAudioToFile(string text) { return await RetrieveAudioSsmlToFile($"{System.Security.SecurityElement.Escape(text)}"); } private async Task RetrieveAudioSsmlToFile(string ssml) { return await speechManager.GetAudioFileFromSsml(ssml, voice, style, rate); } private void PlayAudioRequestsSequentially(List> requestTasks) { foreach (var request in requestTasks) { string file = request.Result; try { Debug.WriteLine($"Playing audio file: {file}"); audioPlayer.Play(file).Wait(); } catch (Exception ex) { Debug.WriteLine($"Failed to play {file}: {ex.Message}"); ErrorLogger(ex, $"while playing: {file}"); } while (audioPlayer.Playing) Thread.Sleep(50); // Explicit stop to ensure device is ready for next file. // ...hopefully. audioPlayer.Stop(true).Wait(); } speechManager.CommitCache(); } } }