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
+