From c2756fc1bd19ef0f7111da03fe34d88ab86a1359 Mon Sep 17 00:00:00 2001 From: InuInu2022 <97830071+InuInu2022@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:49:10 +0900 Subject: [PATCH] feat: support presets --- README.md | 1 + .../View/TalkPresetsView.xaml | 49 +++++ .../View/TalkPresetsView.xaml.cs | 58 ++++++ .../View/VoiSonaTalkPresetDisplayAttribute.cs | 38 ++++ .../ViewModel/TalkPresetsViewModel.cs | 173 ++++++++++++++++++ .../ViewModel/TalkSettingViewModel.cs | 1 + src/YMM4VoiSonaPlugin/VoiSonaTalkParameter.cs | 46 +++-- src/YMM4VoiSonaPlugin/VoiSonaTalkSettings.cs | 29 ++- src/YMM4VoiSonaPlugin/VoiSonaTalkSpeaker.cs | 15 +- .../VoiSonaTalkStyleParameter.cs | 5 + .../YMM4VoiSonaPlugin.csproj | 4 + 11 files changed, 401 insertions(+), 18 deletions(-) create mode 100644 src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml create mode 100644 src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml.cs create mode 100644 src/YMM4VoiSonaPlugin/View/VoiSonaTalkPresetDisplayAttribute.cs create mode 100644 src/YMM4VoiSonaPlugin/ViewModel/TalkPresetsViewModel.cs diff --git a/README.md b/README.md index 60d1d31..c28070e 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,4 @@ YMM4のボイスとして **「VoiSona Talk」(ボイソナトーク)** を使 - SonaBridge - MIT - FlaUI - MIT - Epoxy - Apache-2.0 license + - Material.Icons.WPF - MIT diff --git a/src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml b/src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml new file mode 100644 index 0000000..84437b4 --- /dev/null +++ b/src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml.cs b/src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml.cs new file mode 100644 index 0000000..deb78d4 --- /dev/null +++ b/src/YMM4VoiSonaPlugin/View/TalkPresetsView.xaml.cs @@ -0,0 +1,58 @@ +using System.Windows; +using System.Windows.Controls; + +using YMM4VoiSonaPlugin.ViewModel; + +using YukkuriMovieMaker.Commons; + +namespace YMM4VoiSonaPlugin.View; + +/// +/// Interaction logic for TalkPresetsView.xaml +/// +public partial class TalkPresetsView : UserControl, IPropertyEditorControl2 +{ + public event EventHandler? BeginEdit; + public event EventHandler? EndEdit; + public ItemProperty[]? ItemProperties { get; set; } + + public TalkPresetsView() + { + InitializeComponent(); + DataContextChanged += TalkPresetsView_DataContextChanged; + } + + public void SetEditorInfo(IEditorInfo info) + { + if (DataContext is not TalkPresetsViewModel vm) return; + vm.EditorInfo = info; + } + + private void TalkPresetsView_DataContextChanged( + object sender, DependencyPropertyChangedEventArgs e) + { + if (e.OldValue is TalkPresetsView oldVm) + { + oldVm.BeginEdit -= TalkPresetsView_BeginEdit; + oldVm.EndEdit -= TalkPresetsView_EndEdit; + } + if (e.NewValue is TalkPresetsView newVm) + { + newVm.BeginEdit += TalkPresetsView_BeginEdit; + newVm.EndEdit += TalkPresetsView_EndEdit; + } + } + + private void TalkPresetsView_BeginEdit(object? sender, EventArgs e) + { + BeginEdit?.Invoke(this, e); + } + + private void TalkPresetsView_EndEdit(object? sender, EventArgs e) + { + //var vm = DataContext as TalkPresetsViewModel; + + EndEdit?.Invoke(this, e); + } + +} diff --git a/src/YMM4VoiSonaPlugin/View/VoiSonaTalkPresetDisplayAttribute.cs b/src/YMM4VoiSonaPlugin/View/VoiSonaTalkPresetDisplayAttribute.cs new file mode 100644 index 0000000..f8fcdae --- /dev/null +++ b/src/YMM4VoiSonaPlugin/View/VoiSonaTalkPresetDisplayAttribute.cs @@ -0,0 +1,38 @@ +using YukkuriMovieMaker.Commons; +using System.Windows; +using YMM4VoiSonaPlugin.ViewModel; + +namespace YMM4VoiSonaPlugin.View; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class VoiSonaTalkPresetDisplayAttribute : PropertyEditorAttribute2 +{ + public override void SetBindings(FrameworkElement control, ItemProperty[] itemProperties) + { + if (control is not TalkPresetsView editor) return; + + editor.ItemProperties = itemProperties; + if(itemProperties?[0].PropertyOwner is VoiSonaTalkParameter vsParam) + { + int? oldIndex = vsParam.PresetIndex; + + editor.DataContext = new TalkPresetsViewModel(vsParam.Preset, oldIndex); + } + if(editor.DataContext is not TalkPresetsViewModel vm) return; + vm.ItemProperties = itemProperties; + } + + public override FrameworkElement Create() + { + return new TalkPresetsView(); + } + + public override void ClearBindings(FrameworkElement control) + { + if (control is not TalkPresetsView editor) return; + editor.ItemProperties = null; + if(editor.DataContext is not TalkPresetsViewModel vm) return; + vm.Dispose(); + editor.DataContext = null; + } +} diff --git a/src/YMM4VoiSonaPlugin/ViewModel/TalkPresetsViewModel.cs b/src/YMM4VoiSonaPlugin/ViewModel/TalkPresetsViewModel.cs new file mode 100644 index 0000000..a098875 --- /dev/null +++ b/src/YMM4VoiSonaPlugin/ViewModel/TalkPresetsViewModel.cs @@ -0,0 +1,173 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows; + +using Epoxy; + +using SonaBridge; +using SonaBridge.Core.Common; + +using YmmeUtil.Ymm4; + +using YukkuriMovieMaker.Commons; + +namespace YMM4VoiSonaPlugin.ViewModel; + +[ViewModel] +public class TalkPresetsViewModel: IPropertyEditorControl, IDisposable +{ + public ObservableCollection Presets { get; set; } = []; + public int PresetIndex { get; set; } + public Command ReloadPresets { get; set; } + + [IgnoreInject] + public IEditorInfo? EditorInfo { get; set; } + public ItemProperty[]? ItemProperties { get; set; } + + public event EventHandler? BeginEdit; + public event EventHandler? EndEdit; + + static readonly ITalkAutoService _service = + new TalkServiceProvider() + .GetService(); + bool _isPresetLoad; + bool _disposedValue; + + public TalkPresetsViewModel( + IList? presets, + int? presetIndex = null + ) + { + if(presets is not null) + { + Presets = [..presets]; + } + if(presetIndex is null || Presets.Count > presetIndex) + { + PresetIndex = presetIndex ?? -1; + } + ReloadPresets = Command.Factory.Create(async ()=>{ + var voice = EditorInfo?.Voice?.Speaker?.SpeakerName; + if(voice is null) return; + + // (re)load presets + TaskbarUtil.StartIndeterminate(); + var presets = await _service + .GetPresetsAsync(voice) + .ConfigureAwait(true); + TaskbarUtil.FinishIndeterminate(); + Presets = [.. presets]; + WindowUtil.FocusBack(); + PresetIndex = -1; + }); + } + + [PropertyChanged(nameof(PresetIndex))] + [SuppressMessage("","IDE0051")] + private async ValueTask PresetIndexChangedAsync(int index) + { + if (index < 0) return; + if (Presets[index] is not string preset) return; + if (EditorInfo?.Voice?.Speaker?.SpeakerName is not string voice) return; + + _isPresetLoad = true; + + TaskbarUtil.StartIndeterminate(); + BeginEdit?.Invoke(this, EventArgs.Empty); + await _service.SetPresetsAsync(voice, preset) + .ConfigureAwait(true); + + //PresetIndex = -1; //reset combo + + if(ItemProperties?[0].PropertyOwner is VoiSonaTalkParameter vsParam) + { + var globalParams = await _service.GetGlobalParamsAsync() + .ConfigureAwait(true); + var styles = await _service.GetStylesAsync(voice) + .ConfigureAwait(true); + var items = styles + .Select(s => new VoiSonaTalkStyleParameter() { + DisplayName = s.Key, + Value = s.Value, + Description = $"Style: {s.Key}", + }); + vsParam.PresetIndex = index; + vsParam.Alpha = globalParams["Alpha"]; + vsParam.Husky = globalParams["Hus."]; + vsParam.Pitch = globalParams["Pitch"]; + vsParam.Speed = globalParams["Speed"]; + vsParam.Intonation = globalParams["Into."]; + vsParam.Volume = globalParams["Volume"]; + vsParam.ItemsCollection = [.. items]; + } + EndEdit?.Invoke(this, EventArgs.Empty); + TaskbarUtil.FinishIndeterminate(); + + WindowUtil.FocusBack(); + _isPresetLoad = false; + } + + [PropertyChanged(nameof(ItemProperties))] + [SuppressMessage("","IDE0051")] + private ValueTask ItemPropertiesChangedAsync(ItemProperty[] value) + { + if(value is null or []){ return default; } + + if(ItemProperties?[0].PropertyOwner is VoiSonaTalkParameter vsParam) + { + vsParam.PropertyChanged += ResetPresetSelectionEvent; + } + return default; + } + + void ResetPresetSelectionEvent(object? sender, PropertyChangedEventArgs e) + { + var isTargetReset = e.PropertyName switch + { + "Speed" or "Volume" or "Pitch" or "Alpha" or "Into." or "Hus." => true, + "ItemsCollection.Value" => true, + _ => false, + }; + if (!isTargetReset) return; + if (_isPresetLoad) return; + + // reset combo + PresetIndex = -1; + if(ItemProperties?[0].PropertyOwner is VoiSonaTalkParameter vsParam) + { + vsParam.PresetIndex = -1; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if(ItemProperties?[0].PropertyOwner is VoiSonaTalkParameter vsParam) + { + vsParam.PropertyChanged -= ResetPresetSelectionEvent; + } + _service.Dispose(); + } + + _disposedValue = true; + } + } + + // 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします + // ~TalkPresetsViewModel() + // { + // // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します + // Dispose(disposing: false); + // } + + public void Dispose() + { + // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/YMM4VoiSonaPlugin/ViewModel/TalkSettingViewModel.cs b/src/YMM4VoiSonaPlugin/ViewModel/TalkSettingViewModel.cs index 5a11df4..7029932 100644 --- a/src/YMM4VoiSonaPlugin/ViewModel/TalkSettingViewModel.cs +++ b/src/YMM4VoiSonaPlugin/ViewModel/TalkSettingViewModel.cs @@ -94,6 +94,7 @@ async ValueTask PreloadAsync() IsPreloading = false; IsPreloadButtonEnabled = true; TaskbarUtil.FinishIndeterminate(); + WindowUtil.FocusBack(); } static async Task OpenUrlAsync(string openUrl) diff --git a/src/YMM4VoiSonaPlugin/VoiSonaTalkParameter.cs b/src/YMM4VoiSonaPlugin/VoiSonaTalkParameter.cs index 1a4aa85..e715eb8 100644 --- a/src/YMM4VoiSonaPlugin/VoiSonaTalkParameter.cs +++ b/src/YMM4VoiSonaPlugin/VoiSonaTalkParameter.cs @@ -1,12 +1,15 @@ using System.Collections.Immutable; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.Text.Json.Serialization; + +using Newtonsoft.Json; using YukkuriMovieMaker.Controls; using YukkuriMovieMaker.Plugin.Voice; +using YMM4VoiSonaPlugin.View; +using System.Diagnostics; + namespace YMM4VoiSonaPlugin; public partial class VoiSonaTalkParameter : VoiceParameterBase @@ -20,8 +23,8 @@ public partial class VoiSonaTalkParameter : VoiceParameterBase ImmutableList _styles = []; string _voice = ""; - int _preset; - IReadOnlyList<(string Name, object Value)> _presets = []; + + ImmutableList? _preset; public string Voice { @@ -29,18 +32,27 @@ public string Voice set => Set(ref _voice, value); } + #region synthesis_options /* - [Display(Name = "プリセット", Description = "プリセットを選択")] - [CommonComboBox("Name", "Value", nameof(Presets))] - public int Preset {get => _preset; set => Set(ref _preset, value); } + bool _isDoSynth; - [JsonIgnore] - public IReadOnlyList<(string Name, object Value)> Presets{ - get => _presets; - set => Set(ref _presets, value); + [Display(GroupName = "合成オプション", Name = "パラメータ変更再合成", Description = "パラメータ変更時に再合成をするかどうか。")] + [ToggleSlider] + public bool IsDoSynth { + get => _isDoSynth; + set => Set(ref _isDoSynth, value); } */ + #endregion + + + [Display(Name = "プリセット", Description = "プリセットを選択")] + [VoiSonaTalkPresetDisplay] + public ImmutableList? Preset {get => _preset; set => Set(ref _preset, value); } + + public int PresetIndex { get; set; } = -1; + [Display(Name = nameof(Speed), Description = "話速を調整")] [TextBoxSlider("F2", "", 0.2, 5, Delay = -1)] [Range(0.2, 5)] @@ -102,13 +114,23 @@ public double Husky } [Display(AutoGenerateField = true)] + [JsonProperty] public ImmutableList ItemsCollection { get => _styles; set { UnsubscribeFromItems(_styles); - Set(ref _styles, value); + try + { + Set(ref _styles, value); + } + catch (Exception e) + { + Debug.WriteLine(e.Message); + Debug.WriteLine(e.StackTrace); + } + SubscribeToItems(_styles); } } diff --git a/src/YMM4VoiSonaPlugin/VoiSonaTalkSettings.cs b/src/YMM4VoiSonaPlugin/VoiSonaTalkSettings.cs index 0100b5a..48635e8 100644 --- a/src/YMM4VoiSonaPlugin/VoiSonaTalkSettings.cs +++ b/src/YMM4VoiSonaPlugin/VoiSonaTalkSettings.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using Epoxy; using YmmeUtil.Ymm4; +using System.Diagnostics.CodeAnalysis; namespace YMM4VoiSonaPlugin; @@ -41,17 +42,25 @@ public string[] Speakers get { return _speakers; } set { Set(ref _speakers, value); } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0016")] + [SuppressMessage("Design", "MA0016")] public Dictionary> SpeakersStyles { get { return _speakersStyles; } set { Set(ref _speakersStyles, value); } } + [SuppressMessage("Design", "MA0016")] + public Dictionary> SpeakersPresets + { + get { return _speakersPresets; } + set { Set(ref _speakersPresets, value); } + } + ITalkAutoService? _service; bool _isCached; string[] _speakers = []; Dictionary> _speakersStyles = new(StringComparer.Ordinal); + Dictionary> _speakersPresets = new(StringComparer.Ordinal); public override void Initialize() { @@ -84,16 +93,25 @@ await UIThread.InvokeAsync(()=>{ double total = Speakers.Length; var index = 1; - var dic = new Dictionary>(StringComparer.Ordinal); + var styleDic = new Dictionary>(StringComparer.Ordinal); + var presetDic = new Dictionary>(StringComparer.Ordinal); foreach (var item in Speakers) { if (item is null) continue; await _service.SetCastAsync(item) .ConfigureAwait(false); + + //styles var styles = await _service .GetStylesAsync(item) .ConfigureAwait(false); - dic.Add(item, styles.ToDictionary()); + styleDic.Add(item, styles.ToDictionary()); + + //presets + var presets = await _service + .GetPresetsAsync(item) + .ConfigureAwait(false); + presetDic.Add(item, [..presets]); await UIThread.InvokeAsync(()=>{ TaskbarUtil.ShowProgress(index / total); @@ -101,7 +119,8 @@ await UIThread.InvokeAsync(()=>{ }).ConfigureAwait(false); index++; } - SpeakersStyles = dic; + SpeakersStyles = styleDic; + SpeakersPresets = presetDic; IsCached = true; @@ -109,5 +128,7 @@ await UIThread.InvokeAsync(()=>{ TaskbarUtil.FinishIndeterminate(); return ValueTask.CompletedTask; }).ConfigureAwait(false); + + WindowUtil.FocusBack(); } } diff --git a/src/YMM4VoiSonaPlugin/VoiSonaTalkSpeaker.cs b/src/YMM4VoiSonaPlugin/VoiSonaTalkSpeaker.cs index a03eaac..2f03d90 100644 --- a/src/YMM4VoiSonaPlugin/VoiSonaTalkSpeaker.cs +++ b/src/YMM4VoiSonaPlugin/VoiSonaTalkSpeaker.cs @@ -38,6 +38,7 @@ public class VoiSonaTalkSpeaker : IVoiceSpeaker readonly string _voiceName; ReadOnlyDictionary _styles; + IReadOnlyList _presets; public VoiSonaTalkSpeaker(string voiceName) { @@ -51,6 +52,7 @@ public VoiSonaTalkSpeaker(string voiceName) ); _voiceName = voiceName; _styles = new(new Dictionary(StringComparer.Ordinal)); + _presets = []; } public async Task ConvertKanjiToYomiAsync(string text, IVoiceParameter voiceParameter) @@ -130,6 +132,7 @@ await Console.Error .ConfigureAwait(false); await UIThread.InvokeAsync(()=>{ TaskbarUtil.ShowError(); + WindowUtil.FocusBack(); return ValueTask.CompletedTask; }).ConfigureAwait(false); } @@ -139,6 +142,7 @@ await UIThread.InvokeAsync(()=>{ await UIThread.InvokeAsync(()=>{ TaskbarUtil.FinishIndeterminate(); + WindowUtil.FocusBack(); return ValueTask.CompletedTask; }).ConfigureAwait(false); } @@ -147,10 +151,16 @@ await UIThread.InvokeAsync(()=>{ public IVoiceParameter CreateVoiceParameter() { - if(!VoiSonaTalkSettings.Default.SpeakersStyles.TryGetValue(_voiceName, out var saved)){ + if( + !VoiSonaTalkSettings.Default.SpeakersStyles.TryGetValue(_voiceName, out var savedStyles) + ){ return new VoiSonaTalkParameter(); } - _styles = saved.AsReadOnly(); + var hasPresets = VoiSonaTalkSettings.Default.SpeakersPresets + .TryGetValue(_voiceName, out var savedPresets); + _styles = savedStyles.AsReadOnly(); + if(hasPresets) _presets = savedPresets!.AsReadOnly(); + return new VoiSonaTalkParameter { Voice = _voiceName, @@ -161,6 +171,7 @@ public IVoiceParameter CreateVoiceParameter() Description=$"Style: {v.Key}", }) .ToImmutableList(), + Preset = [.._presets], }; } diff --git a/src/YMM4VoiSonaPlugin/VoiSonaTalkStyleParameter.cs b/src/YMM4VoiSonaPlugin/VoiSonaTalkStyleParameter.cs index a3483d8..a4d9b3d 100644 --- a/src/YMM4VoiSonaPlugin/VoiSonaTalkStyleParameter.cs +++ b/src/YMM4VoiSonaPlugin/VoiSonaTalkStyleParameter.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + using YMM4VoiSonaPlugin.View; using YukkuriMovieMaker.Controls; @@ -14,12 +16,15 @@ public partial class VoiSonaTalkStyleParameter : VoiceParameterBase string _displayName = string.Empty; string _description = string.Empty; + [JsonProperty] public string DisplayName { get => _displayName; init => Set(ref _displayName, value); } + [JsonProperty] public string Description { get => _description; init => Set(ref _description, value); } [VoiSonaStyleDisplay] [TextBoxSlider("F2", "", -1, 2, Delay = -1)] [Range(0, 1)] [DefaultValue(0.0)] + [JsonProperty] public double Value { get => _value; set => Set(ref _value, value);} } \ No newline at end of file diff --git a/src/YMM4VoiSonaPlugin/YMM4VoiSonaPlugin.csproj b/src/YMM4VoiSonaPlugin/YMM4VoiSonaPlugin.csproj index 8124188..192c66c 100644 --- a/src/YMM4VoiSonaPlugin/YMM4VoiSonaPlugin.csproj +++ b/src/YMM4VoiSonaPlugin/YMM4VoiSonaPlugin.csproj @@ -56,6 +56,10 @@ true true + + true + true +