mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-07-02 17:03:41 -04:00
Export version fixes (#83)
* Add file association for .eop, prompt for install dir * Handle .eop or .aip file passed as arg. * VS2022 version bump * Filter neutron stars and black holes from fast spinning criteria. * Adjustments for new "high value" check * Refactor herald cache * Fix element order and namespaces for voice moods. * Add explicit .Stop() between audio player calls. * Use nullsafe member access instead of skipping * Don't queue up a title that's already queued. * Improve body ordinal handling for explorer speech titles. * Escape strings being inserted into xml * Handle flip-flopping JSON type * Converter for flip-flopping property type * Use the converter * Escape characters *before* we wrap it in xml. * Give Eahlstan his clear button. :D * Exclude all stars from fast rotation check. * Close outstanding popup notifications on exit. * TO DONE * [Herald] Suppress duplicate notification titles for spoken notifications If you have notifications from multiple plugins producing notifications with the same title in quick succession (ie. "Body A 1 e" from both Explorer and BioInsights), the title on successive notifications will not be spoken again to save the breath of our friendly Azure speakers. * Doc update * Remove unintended member hiding * Fix export errors when exporting BioInsights data, cleanup Discovered a couple issues with exporting BioInsights data resulting from using two different types of objects in the data grid; improved error handling as well. Also cleaned up some old-style read all code. * Add read-all on launch setting * Updated framework xml * Improve high-value body description text Co-authored-by: Fred Kuipers <mr.fredk@gmail.com>
This commit is contained in:
@ -43,6 +43,15 @@ namespace Observatory.Herald
|
||||
// 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)
|
||||
@ -59,7 +68,7 @@ namespace Observatory.Herald
|
||||
|
||||
private void ProcessQueue()
|
||||
{
|
||||
|
||||
Thread.Sleep(200); // Allow time for other notifications to arrive.
|
||||
NotificationArgs notification = null;
|
||||
try
|
||||
{
|
||||
@ -69,24 +78,20 @@ namespace Observatory.Herald
|
||||
notification = notifications.Dequeue();
|
||||
Debug.WriteLine("Processing notification: {0} - {1}", notification.Title, notification.Detail);
|
||||
|
||||
Task<string>[] audioRequestTasks = new Task<string>[2];
|
||||
List<Task<string>> audioRequestTasks = new();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(notification.TitleSsml))
|
||||
if (!notification.Suppression.HasFlag(NotificationSuppression.Title))
|
||||
{
|
||||
audioRequestTasks[0] = RetrieveAudioToFile(notification.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioRequestTasks[0] = RetrieveAudioSsmlToFile(notification.TitleSsml);
|
||||
audioRequestTasks.Add(string.IsNullOrWhiteSpace(notification.TitleSsml)
|
||||
? RetrieveAudioToFile(notification.Title)
|
||||
: RetrieveAudioSsmlToFile(notification.TitleSsml));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(notification.DetailSsml))
|
||||
if (!notification.Suppression.HasFlag(NotificationSuppression.Detail))
|
||||
{
|
||||
audioRequestTasks[1] = RetrieveAudioToFile(notification.Detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioRequestTasks[1] = RetrieveAudioSsmlToFile(notification.DetailSsml);
|
||||
audioRequestTasks.Add(string.IsNullOrWhiteSpace(notification.DetailSsml)
|
||||
? RetrieveAudioToFile(notification.Detail)
|
||||
: RetrieveAudioSsmlToFile(notification.DetailSsml));
|
||||
}
|
||||
|
||||
PlayAudioRequestsSequentially(audioRequestTasks);
|
||||
@ -105,7 +110,7 @@ namespace Observatory.Herald
|
||||
|
||||
private async Task<string> RetrieveAudioToFile(string text)
|
||||
{
|
||||
return await RetrieveAudioSsmlToFile($"<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\"><voice name=\"\">{text}</voice></speak>");
|
||||
return await RetrieveAudioSsmlToFile($"<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\"><voice name=\"\">{System.Security.SecurityElement.Escape(text)}</voice></speak>");
|
||||
}
|
||||
|
||||
private async Task<string> RetrieveAudioSsmlToFile(string ssml)
|
||||
@ -113,7 +118,7 @@ namespace Observatory.Herald
|
||||
return await speechManager.GetAudioFileFromSsml(ssml, voice, style, rate);
|
||||
}
|
||||
|
||||
private void PlayAudioRequestsSequentially(Task<string>[] requestTasks)
|
||||
private void PlayAudioRequestsSequentially(List<Task<string>> requestTasks)
|
||||
{
|
||||
foreach (var request in requestTasks)
|
||||
{
|
||||
@ -131,7 +136,12 @@ namespace Observatory.Herald
|
||||
while (audioPlayer.Playing)
|
||||
Thread.Sleep(50);
|
||||
|
||||
// Explicit stop to ensure device is ready for next file.
|
||||
// ...hopefully.
|
||||
audioPlayer.Stop(true).Wait();
|
||||
|
||||
}
|
||||
speechManager.CommitCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace NetCoreAudio.Interfaces
|
||||
Task Play(string fileName);
|
||||
Task Pause();
|
||||
Task Resume();
|
||||
Task Stop();
|
||||
Task Stop(bool force);
|
||||
Task SetVolume(byte percent);
|
||||
}
|
||||
}
|
||||
|
@ -71,9 +71,9 @@ namespace NetCoreAudio
|
||||
/// Stops any current playback and clears the buffer. Sets Playing and Paused flags to false.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task Stop()
|
||||
public async Task Stop(bool force = false)
|
||||
{
|
||||
await _internalPlayer.Stop();
|
||||
await _internalPlayer.Stop(force);
|
||||
}
|
||||
|
||||
private void OnPlaybackFinished(object sender, EventArgs e)
|
||||
|
@ -56,7 +56,7 @@ namespace NetCoreAudio.Players
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
public Task Stop(bool force = false)
|
||||
{
|
||||
if (_process != null)
|
||||
{
|
||||
|
@ -76,15 +76,15 @@ namespace NetCoreAudio.Players
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
public Task Stop(bool force = false)
|
||||
{
|
||||
if (Playing)
|
||||
if (Playing || force)
|
||||
{
|
||||
ExecuteMciCommand($"Stop {_fileName}");
|
||||
Playing = false;
|
||||
Paused = false;
|
||||
_playbackTimer.Stop();
|
||||
_playStopwatch.Stop();
|
||||
_playbackTimer?.Stop();
|
||||
_playStopwatch?.Stop();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
2
ObservatoryHerald/ObservatoryAPI.Designer.cs
generated
2
ObservatoryHerald/ObservatoryAPI.Designer.cs
generated
@ -19,7 +19,7 @@ namespace Observatory.Herald {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class ObservatoryAPI {
|
||||
|
@ -9,6 +9,7 @@ using System.Text.Json;
|
||||
using System.Xml;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Observatory.Herald
|
||||
{
|
||||
@ -19,7 +20,8 @@ namespace Observatory.Herald
|
||||
private string ApiEndpoint;
|
||||
private DirectoryInfo cacheLocation;
|
||||
private int cacheSize;
|
||||
private Action<Exception, String> ErrorLogger;
|
||||
private Action<Exception, string> ErrorLogger;
|
||||
private ConcurrentDictionary<string, CacheData> cacheIndex;
|
||||
|
||||
internal SpeechRequestManager(
|
||||
HeraldSettings settings, HttpClient httpClient, string cacheFolder, Action<Exception, String> errorLogger)
|
||||
@ -29,6 +31,7 @@ namespace Observatory.Herald
|
||||
this.httpClient = httpClient;
|
||||
cacheSize = Math.Max(settings.CacheSize, 1);
|
||||
cacheLocation = new DirectoryInfo(cacheFolder);
|
||||
ReadCache();
|
||||
ErrorLogger = errorLogger;
|
||||
|
||||
if (!Directory.Exists(cacheLocation.FullName))
|
||||
@ -103,26 +106,27 @@ namespace Observatory.Herald
|
||||
XmlNamespaceManager ssmlNs = new(ssmlDoc.NameTable);
|
||||
ssmlNs.AddNamespace("ssml", ssmlNamespace);
|
||||
ssmlNs.AddNamespace("mstts", "http://www.w3.org/2001/mstts");
|
||||
ssmlNs.AddNamespace("emo", "http://www.w3.org/2009/10/emotionml");
|
||||
|
||||
var voiceNode = ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs);
|
||||
voiceNode.Attributes.GetNamedItem("name").Value = voiceName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(styleName))
|
||||
{
|
||||
var expressAsNode = ssmlDoc.CreateElement("express-as", "http://www.w3.org/2001/mstts");
|
||||
expressAsNode.SetAttribute("style", styleName);
|
||||
expressAsNode.InnerXml = voiceNode.InnerXml;
|
||||
voiceNode.InnerXml = expressAsNode.OuterXml;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rate))
|
||||
{
|
||||
var prosodyNode = ssmlDoc.CreateElement("prosody", ssmlNamespace);
|
||||
var prosodyNode = ssmlDoc.CreateElement("ssml", "prosody", ssmlNamespace);
|
||||
prosodyNode.SetAttribute("rate", rate);
|
||||
prosodyNode.InnerXml = voiceNode.InnerXml;
|
||||
voiceNode.InnerXml = prosodyNode.OuterXml;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(styleName))
|
||||
{
|
||||
var expressAsNode = ssmlDoc.CreateElement("mstts", "express-as", "http://www.w3.org/2001/mstts");
|
||||
expressAsNode.SetAttribute("style", styleName);
|
||||
expressAsNode.InnerXml = voiceNode.InnerXml;
|
||||
voiceNode.InnerXml = expressAsNode.OuterXml;
|
||||
}
|
||||
|
||||
return ssmlDoc.OuterXml;
|
||||
}
|
||||
|
||||
@ -229,10 +233,8 @@ namespace Observatory.Herald
|
||||
return demonym;
|
||||
}
|
||||
|
||||
private async void UpdateAndPruneCache(FileInfo currentFile)
|
||||
private void ReadCache()
|
||||
{
|
||||
Dictionary<string, CacheData> cacheIndex;
|
||||
|
||||
string cacheIndexFile = cacheLocation + "CacheIndex.json";
|
||||
|
||||
if (File.Exists(cacheIndexFile))
|
||||
@ -240,7 +242,7 @@ namespace Observatory.Herald
|
||||
var indexFileContent = File.ReadAllText(cacheIndexFile);
|
||||
try
|
||||
{
|
||||
cacheIndex = JsonSerializer.Deserialize<Dictionary<string, CacheData>>(indexFileContent);
|
||||
cacheIndex = JsonSerializer.Deserialize<ConcurrentDictionary<string, CacheData>>(indexFileContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -258,9 +260,13 @@ namespace Observatory.Herald
|
||||
var cacheFiles = cacheLocation.GetFiles("*.mp3");
|
||||
foreach (var file in cacheFiles.Where(file => !cacheIndex.ContainsKey(file.Name)))
|
||||
{
|
||||
cacheIndex.Add(file.Name, new(file.CreationTime, 0));
|
||||
cacheIndex.TryAdd(file.Name, new(file.CreationTime, 0));
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateAndPruneCache(FileInfo currentFile)
|
||||
{
|
||||
var cacheFiles = cacheLocation.GetFiles("*.mp3");
|
||||
if (cacheIndex.ContainsKey(currentFile.Name))
|
||||
{
|
||||
cacheIndex[currentFile.Name] = new(
|
||||
@ -270,13 +276,15 @@ namespace Observatory.Herald
|
||||
}
|
||||
else
|
||||
{
|
||||
cacheIndex.Add(currentFile.Name, new(DateTime.UtcNow, 1));
|
||||
cacheIndex.TryAdd(currentFile.Name, new(DateTime.UtcNow, 1));
|
||||
}
|
||||
|
||||
var currentCacheSize = cacheFiles.Sum(f => f.Length);
|
||||
while (currentCacheSize > cacheSize * 1024 * 1024)
|
||||
{
|
||||
var indexedCacheSize = cacheFiles
|
||||
.Where(f => cacheIndex.ContainsKey(f.Name))
|
||||
.Sum(f => f.Length);
|
||||
|
||||
while (indexedCacheSize > cacheSize * 1024 * 1024)
|
||||
{
|
||||
var staleFile = (from file in cacheIndex
|
||||
orderby file.Value.HitCount, file.Value.Created
|
||||
select file.Key).First();
|
||||
@ -284,16 +292,19 @@ namespace Observatory.Herald
|
||||
if (staleFile == currentFile.Name)
|
||||
break;
|
||||
|
||||
currentCacheSize -= new FileInfo(cacheLocation + staleFile).Length;
|
||||
File.Delete(cacheLocation + staleFile);
|
||||
cacheIndex.Remove(staleFile);
|
||||
cacheIndex.TryRemove(staleFile, out _);
|
||||
}
|
||||
}
|
||||
|
||||
internal async void CommitCache()
|
||||
{
|
||||
string cacheIndexFile = cacheLocation + "CacheIndex.json";
|
||||
|
||||
// Race conditions between title and detail speech make a collision here possible.
|
||||
// Wait for file to become writable, but return control to call site while we wait.
|
||||
System.Diagnostics.Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
|
||||
// Race condition isn't a concern anymore, but should check this anyway to be safe.
|
||||
// (Maybe someone is poking at the file with notepad?)
|
||||
while (!IsFileWritable(cacheIndexFile) && stopwatch.ElapsedMilliseconds < 1000)
|
||||
await Task.Factory.StartNew(() => System.Threading.Thread.Sleep(100));
|
||||
|
||||
@ -325,7 +336,7 @@ namespace Observatory.Herald
|
||||
}
|
||||
}
|
||||
|
||||
public class CacheData
|
||||
private class CacheData
|
||||
{
|
||||
public CacheData(DateTime Created, int HitCount)
|
||||
{
|
||||
|
Reference in New Issue
Block a user