diff --git a/OpenUtau.Core/Classic/PresampWatcher.cs b/OpenUtau.Core/Classic/PresampWatcher.cs new file mode 100644 index 000000000..9ca54ac68 --- /dev/null +++ b/OpenUtau.Core/Classic/PresampWatcher.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using Serilog; + +namespace OpenUtau.Core { + public class PresampWatcher : IDisposable { + public bool Paused { get; set; } + + private FileSystemWatcher watcher; + private Action reloadCallback; + + public PresampWatcher(string path, Action reloadCallback) { + this.reloadCallback = reloadCallback; + + watcher = new FileSystemWatcher(path); + watcher.Changed += OnFileChanged; + watcher.Created += OnFileChanged; + watcher.Deleted += OnFileChanged; + watcher.Renamed += OnFileChanged; + watcher.Error += OnError; + + watcher.Filter = "presamp.ini"; + // Set to false since presamp.ini is always at the root of the voicebank + watcher.IncludeSubdirectories = false; + watcher.EnableRaisingEvents = true; + } + + private void OnFileChanged(object sender, FileSystemEventArgs e) { + if (Paused) { + return; + } + Log.Information($"Presamp File \"{e.FullPath}\" {e.ChangeType}"); + reloadCallback?.Invoke(); + } + + private void OnError(object sender, ErrorEventArgs e) { + Log.Error($"Presamp Watcher error {e}"); + } + public void Dispose() { + if (watcher != null) { + watcher.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/OpenUtau.Core/Classic/YamlWatcher.cs b/OpenUtau.Core/Classic/YamlWatcher.cs new file mode 100644 index 000000000..870047867 --- /dev/null +++ b/OpenUtau.Core/Classic/YamlWatcher.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Serilog; + +namespace OpenUtau.Core { + public class YamlWatcher : IDisposable { + public bool Paused { get; set; } + + private FileSystemWatcher watcher; + private Action reloadCallback; + + public YamlWatcher(string path, Action reloadCallback) { + this.reloadCallback = reloadCallback; + + watcher = new FileSystemWatcher(path); + watcher.Changed += OnFileChanged; + watcher.Created += OnFileChanged; + watcher.Deleted += OnFileChanged; + watcher.Renamed += OnFileChanged; + watcher.Error += OnError; + + // Filters specifically for .yaml. + watcher.Filter = "*.yaml"; + watcher.IncludeSubdirectories = true; + watcher.EnableRaisingEvents = true; + } + + private void OnFileChanged(object sender, FileSystemEventArgs e) { + if (Paused) { + return; + } + Log.Information($"YAML File \"{e.FullPath}\" {e.ChangeType}"); + + // Execute the refresh logic passed in during initialization + reloadCallback?.Invoke(); + } + + private void OnError(object sender, ErrorEventArgs e) { + Log.Error($"YAML Watcher error {e}"); + } + + public void Dispose() { + if (watcher != null) { + watcher.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs index 9d30ea192..a9c8dc0b4 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs @@ -27,6 +27,10 @@ public abstract class DiffSingerBasePhonemizer : MachineLearningPhonemizer IG2p g2p; Dictionary phonemeTokens; DiffSingerSpeakerEmbedManager speakerEmbedManager; + private static int globalDsGeneration = 0; + private int localDsGeneration = 0; + private static YamlWatcher dsWatcher; + private static string currentlyWatchedDsDir; string defaultPause = "SP"; protected virtual string GetDictionaryName()=>"dsdict.yaml"; @@ -35,9 +39,11 @@ public abstract class DiffSingerBasePhonemizer : MachineLearningPhonemizer private bool _singerLoaded; public override void SetSinger(USinger singer) { - if (_singerLoaded && singer == this.singer) return; + if (_singerLoaded && singer == this.singer && localDsGeneration == globalDsGeneration) return; try { + localDsGeneration = globalDsGeneration; _singerLoaded = _executeSetSinger(singer); + SetupYamlWatcher(rootPath); } catch { _singerLoaded = false; throw; @@ -103,6 +109,32 @@ private bool _executeSetSinger(USinger singer) { return true; } + private void SetupYamlWatcher(string directory) { + if (string.IsNullOrEmpty(directory) || currentlyWatchedDsDir == directory) { + return; + } + + if (dsWatcher != null) { + dsWatcher.Dispose(); + dsWatcher = null; + } + + currentlyWatchedDsDir = directory; + + if (Directory.Exists(directory)) { + dsWatcher = new YamlWatcher(directory, () => { + Log.Information($"[DiffSingerBasePhonemizer] Detected YAML change in {directory}. Reloading globally..."); + System.Threading.Thread.Sleep(200); + globalDsGeneration++; + + // Signal OpenUtau to re-run the timeline runner + if (this.singer != null) { + OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); + } + }); + } + } + protected virtual IG2p LoadG2p(string rootPath, bool useLangId = false) { //Each phonemizer has a delicated dictionary name, such as dsdict-en.yaml, dsdict-ru.yaml. //If this dictionary exists, load it. diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 2ea0f4271..b8db43de1 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -202,7 +202,9 @@ protected override IG2p LoadBaseDictionary() { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer != null && this.singer.Loaded) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { + this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; consExceptions.Clear(); if (stop != null) consExceptions.AddRange(stop); diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index b308e6bba..630e2ea7e 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -212,7 +212,9 @@ protected override IG2p LoadBaseDictionary() { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer != null && this.singer.Loaded) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { + this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; string file = Path.Combine(this.singer.Location, YamlFileName); if (!File.Exists(file)) { diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index 96171865e..1c842b5e4 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -182,7 +182,9 @@ protected override IG2p LoadBaseDictionary() { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer != null && this.singer.Loaded) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { + this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; consExceptions.Clear(); if (stop != null) consExceptions.AddRange(stop); diff --git a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs index fceed12e9..e6d78768b 100644 --- a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Classic; using OpenUtau.Api; +using OpenUtau.Core; using OpenUtau.Core.Ustx; //using Serilog; @@ -23,6 +25,11 @@ public class JapanesePresampPhonemizer : Phonemizer { private UProject project; private UTrack track; + private static int globalPresampGeneration = 0; + private int localPresampGeneration = 0; + private static PresampWatcher presampWatcher; + private static string currentlyWatchedPresampDir; + // in case voicebank is missing certain symbols static readonly string[] substitution = new string[] { "ty,ch,ts=t", "j,dy=d", "gy=g", "ky=k", "py=p", "ny=n", "ry=r", "my=m", "hy,f=h", "by,v=b", "dz=z", "l=r", "ly=l" @@ -45,16 +52,42 @@ public override void SetUp(Note[][] groups, UProject project, UTrack track) { } public override void SetSinger(USinger singer) { - if (this.singer == singer) { + bool generationChanged = this.localPresampGeneration != globalPresampGeneration; + + if (this.singer == singer && !generationChanged) { return; } this.singer = singer; if (this.singer == null) { return; } + this.localPresampGeneration = globalPresampGeneration; + + if (this.presamp == null || generationChanged) { + this.presamp = new Presamp(); + this.presamp.ReadPresampIni(singer.Location, singer.TextFileEncoding); + } + SetupPresampWatcher(singer.Location); + } - presamp = new Presamp(); - presamp.ReadPresampIni(singer.Location, singer.TextFileEncoding); + private void SetupPresampWatcher(string directory) { + if (string.IsNullOrEmpty(directory) || currentlyWatchedPresampDir == directory) { + return; + } + if (presampWatcher != null) { + presampWatcher.Dispose(); + presampWatcher = null; + } + currentlyWatchedPresampDir = directory; + if (Directory.Exists(directory)) { + presampWatcher = new PresampWatcher(directory, () => { + System.Threading.Thread.Sleep(200); + globalPresampGeneration++; + if (this.singer != null) { + OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); + } + }); + } } public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index c576430f0..a9d3c890a 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using static OpenUtau.Api.Phonemizer; using System.Collections; +using OpenUtau.Core; namespace OpenUtau.Plugin.Builtin { /// @@ -256,8 +257,9 @@ private Result HandleError() { } public override void SetSinger(USinger singer) { - if (this.singer != singer) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; if (this.singer == null || !this.singer.Loaded) { return; @@ -291,6 +293,8 @@ public override void SetSinger(USinger singer) { file = Path.Combine(PluginDir, YamlFileName); } + SetupYamlWatcher(file); + if (!string.IsNullOrEmpty(file)) { bool shouldWriteTemplate = false; bool shouldBackupOldFile = false; @@ -337,7 +341,7 @@ public override void SetSinger(USinger singer) { if (File.Exists(file)) { try { - var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); + var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)) ?? new YAMLData(); if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); @@ -345,7 +349,7 @@ public override void SetSinger(USinger singer) { var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); vowels = backupVowels.Concat(yamlVowels).Distinct().ToArray(); - tails = (tails ?? Array.Empty()).Concat(data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol) ?? Array.Empty()).Distinct().ToArray(); + tails = new string[] { "-", "R" }.Concat(data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol) ?? Array.Empty()).Distinct().ToArray(); fricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); aspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); @@ -384,7 +388,7 @@ public override void SetSinger(USinger singer) { } } } - + yamlFallbacks.Clear(); if (data?.fallbacks != null) { yamlFallbacks.Clear(); foreach (var df in data.fallbacks) { @@ -412,6 +416,10 @@ public override void SetSinger(USinger singer) { protected IG2p dictionary => dictionaries[GetType()]; protected bool isDictionaryLoading => dictionaries[GetType()] == null; protected double TransitionBasicLengthMs => 100; + public static YamlWatcher yamlWatcher; + public static string currentlyWatchedYaml; + public static int globalYamlGeneration = 0; + public int localYamlGeneration = 0; private Dictionary dictionaries = new Dictionary(); private const string FORCED_ALIAS_SYMBOL = "?"; @@ -745,6 +753,43 @@ protected double GetTransitionBasicLengthMsByConstant() { return TransitionBasicLengthMs * GetTempoNoteLengthFactor(); } + private void SetupYamlWatcher(string yamlFilePath) { + if (string.IsNullOrEmpty(yamlFilePath) || currentlyWatchedYaml == yamlFilePath) { + return; + } + + if (yamlWatcher != null) { + yamlWatcher.Dispose(); + yamlWatcher = null; + } + + currentlyWatchedYaml = yamlFilePath; + string directory = Path.GetDirectoryName(yamlFilePath); + + if (Directory.Exists(directory)) { + yamlWatcher = new YamlWatcher(directory, () => { + Log.Information($"[SyllableBasedPhonemizer] Detected change in {YamlFileName}, incrementing generation..."); + + System.Threading.Thread.Sleep(200); + + lock (dictionaries) { + if (dictionaries.ContainsKey(this.GetType())) { + dictionaries.Remove(this.GetType()); + } + } + + backupVowels = null; + backupConsonants = null; + backupDictionaryReplacements = null; + globalYamlGeneration++; + + if (this.singer != null) { + OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); + } + }); + } + } + /// /// a note length modifier, from 1 to 0.3. Used to make transition notes shorter on high tempo ///