diff --git a/src/Speech/Controller/AIVOICEController.cs b/src/Speech/Controller/AIVOICEController.cs index 9579055..2237373 100644 --- a/src/Speech/Controller/AIVOICEController.cs +++ b/src/Speech/Controller/AIVOICEController.cs @@ -300,6 +300,23 @@ public float GetPitchRange() return GetMaster().PitchRange; } + public SoundStream ExportToStream(string text) + { + _ttsControl.Text = text; + + var filePath = Path.Combine(Path.GetTempPath(), $"{this.GetType().Name}_{(uint)text.GetHashCode()}.wav"); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + _ttsControl.SaveAudioToFile(filePath); + if(File.Exists(filePath)) + { + return SoundStream.Open(filePath); + } + return null; + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/CeVIO64Controller.cs b/src/Speech/Controller/CeVIO64Controller.cs index 2dab7ee..280d256 100644 --- a/src/Speech/Controller/CeVIO64Controller.cs +++ b/src/Speech/Controller/CeVIO64Controller.cs @@ -258,6 +258,16 @@ public uint GetVoiceQuality() return _talker.Alpha; } + public SoundStream ExportToStream(string text) + { + string tempFile = Path.GetTempFileName(); + if (_talker.OutputWaveToFile(text, tempFile)) + { + return SoundStream.Open(tempFile); + } + return null; + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/CeVIOAIController.cs b/src/Speech/Controller/CeVIOAIController.cs index 7fadbe5..d74f10f 100644 --- a/src/Speech/Controller/CeVIOAIController.cs +++ b/src/Speech/Controller/CeVIOAIController.cs @@ -259,6 +259,16 @@ public uint GetVoiceQuality() return _talker.Alpha; } + public SoundStream ExportToStream(string text) + { + string tempFile = Path.GetTempFileName(); + if (_talker.OutputWaveToFile(text, tempFile)) + { + return SoundStream.Open(tempFile); + } + return null; + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/CeVIOController.cs b/src/Speech/Controller/CeVIOController.cs index 5a3465f..fe08e88 100644 --- a/src/Speech/Controller/CeVIOController.cs +++ b/src/Speech/Controller/CeVIOController.cs @@ -258,6 +258,16 @@ public uint GetVoiceQuality() return _talker.Alpha; } + public SoundStream ExportToStream(string text) + { + string tempFile = Path.GetTempFileName(); + if (_talker.OutputWaveToFile(text, tempFile)) + { + return SoundStream.Open(tempFile); + } + return null; + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/ISpeechController.cs b/src/Speech/Controller/ISpeechController.cs index 16f09f3..8372273 100644 --- a/src/Speech/Controller/ISpeechController.cs +++ b/src/Speech/Controller/ISpeechController.cs @@ -76,6 +76,12 @@ public interface ISpeechController /// /// 起動していれば true bool IsActive(); + /// + /// 指定した文字列を合成した音声を取得します + /// + /// 合成する文字列 + /// 出力された音声の Stream + SoundStream ExportToStream(string text); } } diff --git a/src/Speech/Controller/OtomachiUnaTalkController.cs b/src/Speech/Controller/OtomachiUnaTalkController.cs index c00611e..4a97b63 100644 --- a/src/Speech/Controller/OtomachiUnaTalkController.cs +++ b/src/Speech/Controller/OtomachiUnaTalkController.cs @@ -254,6 +254,11 @@ private void RestoreMinimizedWindow() } } + public SoundStream ExportToStream(string text) + { + throw new NotImplementedException(); + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/SAPI5Controller.cs b/src/Speech/Controller/SAPI5Controller.cs index 131e909..28c87eb 100644 --- a/src/Speech/Controller/SAPI5Controller.cs +++ b/src/Speech/Controller/SAPI5Controller.cs @@ -176,6 +176,16 @@ public float GetPitchRange() return 1f; } + public SoundStream ExportToStream(string text) + { + var ms = new MemoryStream(); + synthesizer.SetOutputToWaveStream(ms); + synthesizer.Speak(text); + synthesizer.SetOutputToDefaultAudioDevice(); + ms.Position = 0; + return new SoundStream(ms); + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/VOICEVOXController.cs b/src/Speech/Controller/VOICEVOXController.cs index 7822649..bcec96b 100644 --- a/src/Speech/Controller/VOICEVOXController.cs +++ b/src/Speech/Controller/VOICEVOXController.cs @@ -100,43 +100,23 @@ private string ReplaceParam(string str, string key, float value) public void Play(string text) { string tempFile = Path.GetTempFileName(); - - var content = new StringContent("", Encoding.UTF8, @"application/json"); - var encodeText = Uri.EscapeDataString(text); - - int talkerNo = _enumerator.Names[_libraryName]; - - string queryData = ""; - using (var client = new HttpClient()) + try { - try - { - var response = client.PostAsync($"{_baseUrl}/audio_query?text={encodeText}&speaker={talkerNo}", content).GetAwaiter().GetResult(); - if (response.StatusCode != HttpStatusCode.OK) { return; } - queryData = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var soundData = ExportToStream(text); - // 音量等のパラメータを反映させる - queryData = UpdateParam(queryData); - - content = new StringContent(queryData, Encoding.UTF8, @"application/json"); - response = client.PostAsync($"{_baseUrl}/synthesis?speaker={talkerNo}", content).GetAwaiter().GetResult(); - if (response.StatusCode != HttpStatusCode.OK) { return; } - - var soundData = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); - - using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None)) - { - soundData.CopyTo(fileStream); - - } - - SoundPlayer sp = new SoundPlayer(); - sp.Play(tempFile); - } - finally + using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None)) { - OnFinished(); + soundData.CopyTo(fileStream); + } + + SoundPlayer sp = new SoundPlayer(); + sp.Play(tempFile); + File.Delete(tempFile); + } + finally + { + OnFinished(); } } @@ -220,6 +200,32 @@ public float GetPitchRange() return Intonation; } + public SoundStream ExportToStream(string text) + { + var content = new StringContent("", Encoding.UTF8, @"application/json"); + var encodeText = Uri.EscapeDataString(text); + + int talkerNo = _enumerator.Names[_libraryName]; + + string queryData = ""; + using (var client = new HttpClient()) + { + var response = client.PostAsync($"{_baseUrl}/audio_query?text={encodeText}&speaker={talkerNo}", content).GetAwaiter().GetResult(); + if (response.StatusCode != HttpStatusCode.OK) { return null; } + queryData = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + // 音量等のパラメータを反映させる + queryData = UpdateParam(queryData); + + content = new StringContent(queryData, Encoding.UTF8, @"application/json"); + response = client.PostAsync($"{_baseUrl}/synthesis?speaker={talkerNo}", content).GetAwaiter().GetResult(); + if (response.StatusCode != HttpStatusCode.OK) { return null; } + + return new SoundStream(response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()); + } + } + + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/Voiceroid2Controller.cs b/src/Speech/Controller/Voiceroid2Controller.cs index 992f3dd..75d4df1 100644 --- a/src/Speech/Controller/Voiceroid2Controller.cs +++ b/src/Speech/Controller/Voiceroid2Controller.cs @@ -1,6 +1,7 @@ using Codeer.Friendly; using Codeer.Friendly.Windows; using Codeer.Friendly.Windows.Grasp; +using Codeer.Friendly.Windows.NativeStandardControls; using RM.Friendly.WPFStandardControls; using System; using System.Collections.Generic; @@ -107,6 +108,14 @@ private void timer_Elapsed(object sender, EventArgs e) } } + private bool CheckPlaying() + { + WPFButtonBase playButton = new WPFButtonBase(_root.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 0)); + var d = playButton.LogicalTree(); + System.Windows.Visibility v = (System.Windows.Visibility)(d[2])["Visibility"]().Core; + return !System.Windows.Visibility.Visible.Equals(v); + } + private void StopSpeech() { _timer.Stop(); @@ -179,12 +188,11 @@ public void Activate() /// 再生する文字列 public void Play(string text) { - SetText(text); + SetTextAndPlay(text); } - internal virtual void SetText(string text) + internal virtual void SetTextAndPlay(string text) { - text = text.Trim() == "" ? "." : text; - string t = _libraryName + _promptString + text; + string t = AssembleText(text); if (_queue.Count == 0) { WPFTextBox textbox = new WPFTextBox(_root.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2)); @@ -196,6 +204,17 @@ internal virtual void SetText(string text) _queue.Enqueue(t); } } + internal virtual void SetText(string text) + { + string t = AssembleText(text); + WPFTextBox textbox = new WPFTextBox(_root.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2)); + textbox.EmulateChangeText(t); + } + internal virtual string AssembleText(string text) + { + text = text.Trim() == "" ? "." : text; + return _libraryName + _promptString + text; + } /// /// VOICEROID2 に入力された文字列を再生します @@ -297,6 +316,231 @@ private float GetEffect(EffectType t) return Convert.ToSingle(textbox.Text); } + /// + /// ファイル分割設定 + /// + public enum ExportSplitSetting + { + /// + /// 一つのファイルに書き出す + /// + OneFile, + /// + /// 1文毎に区切って複数のファイルに書き出す + /// + Sentence, + /// + /// 指定された文字列で区切って複数のファイルに書き出す + /// + Delimiter + } + + /// + /// 音声保存設定を保持するクラス + /// + public class ExportSettings + { + /// + /// ファイル分割設定 + /// + public ExportSplitSetting SplitSetting { get; set; } = ExportSplitSetting.OneFile; + /// + /// 区切り文字列 + /// + public string SplitString { get; set; } = "/"; + /// + /// 開始ポーズ(ミリ秒) + /// + public long PauseStart { get; set; } = 0; + /// + /// 終了ポーズ(ミリ秒) + /// + public long PauseEnd { get; set; } = 800; + /// + /// テキストファイルを音声ファイルと一緒に保存する + /// + public bool SaveWithText { get; set; } = false; + /// + /// 音声保存時に毎回設定を表示する + /// + public bool ShowSettings { get; set; } = true; + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append(nameof(SplitSetting)); + sb.Append(":"); + sb.Append(SplitSetting); + sb.Append(", "); + + sb.Append(nameof(SplitString)); + sb.Append(":"); + sb.Append(SplitString); + sb.Append(", "); + + sb.Append(nameof(PauseStart)); + sb.Append(":"); + sb.Append(PauseStart); + sb.Append(", "); + + sb.Append(nameof(PauseEnd)); + sb.Append(":"); + sb.Append(PauseEnd); + sb.Append(", "); + + sb.Append(nameof(SaveWithText)); + sb.Append(":"); + sb.Append(SaveWithText); + sb.Append(", "); + + sb.Append(nameof(ShowSettings)); + sb.Append(":"); + sb.Append(ShowSettings); + sb.Append(", "); + + return sb.ToString(); + } + } + + public static void ExportSetting(WindowControl win, bool isSet, ExportSettings exsettings) + { + var export1File = new WPFToggleButton(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 6, 1, 3, 4)); + var exportSentence = new WPFToggleButton(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 6, 1, 3, 5)); + var exportSplit = new WPFToggleButton(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 6, 1, 3, 6)); + var splitString = new WPFTextBox(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 6, 1, 3, 7, 1)); + WPFTextBox pauseStart = null; + WPFTextBox pauseEnd = null; + try + { + pauseStart = new WPFTextBox(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 7, 1, 9, 0, 4)); ; + pauseEnd = new WPFTextBox(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 7, 1, 12, 0, 4)); + } + catch (WindowIdentifyException e) + { + // VOICEROID2 Editor 2.1.1.0 で要素が取得できなくなった + // 取得に失敗した場合はないものとして扱う + } + var saveWithText = new WPFToggleButton(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 9, 1, 2)); + var showSettings = new WPFToggleButton(win.IdentifyFromLogicalTreeIndex(0, 0, 0, 10)); + if (isSet) + { + switch (exsettings.SplitSetting) + { + case ExportSplitSetting.OneFile: + export1File.EmulateCheck(true); + break; + case ExportSplitSetting.Sentence: + exportSentence.EmulateCheck(true); + break; + case ExportSplitSetting.Delimiter: + exportSplit.EmulateCheck(true); + break; + } + splitString.EmulateChangeText(exsettings.SplitString); + pauseStart?.EmulateChangeText(exsettings.PauseStart.ToString()); + pauseEnd?.EmulateChangeText(exsettings.PauseEnd.ToString()); + saveWithText.EmulateCheck(exsettings.SaveWithText); + showSettings.EmulateCheck(exsettings.ShowSettings); + return; + } + if (export1File.IsChecked.GetValueOrDefault(true)) + { + exsettings.SplitSetting = ExportSplitSetting.OneFile; + } + if (exportSentence.IsChecked.GetValueOrDefault(false)) + { + exsettings.SplitSetting = ExportSplitSetting.Sentence; + } + if (exportSplit.IsChecked.GetValueOrDefault(false)) + { + exsettings.SplitSetting = ExportSplitSetting.Delimiter; + } + exsettings.SplitString = splitString.Text; + if (pauseStart != null) + { + exsettings.PauseStart = long.Parse(pauseStart.Text); + } + if (pauseEnd != null) + { + exsettings.PauseEnd = long.Parse(pauseEnd.Text); + } + exsettings.SaveWithText = saveWithText.IsChecked.GetValueOrDefault(false); + exsettings.ShowSettings = showSettings.IsChecked.GetValueOrDefault(true); + } + + public SoundStream ExportToStream(string text) + { + if (CheckPlaying()) + { + // 再生中だと音声保存メニューを開けない + throw new InvalidOperationException("再生中のため処理できません"); + } + + var top = _app.FromZTop(); + if (top.TypeFullName != "AI.Talk.Editor.MainWindow") + { + // TODO: 復帰処理を書く + throw new InvalidOperationException("何らかのウィンドウが開かれているため処理できません"); + } + SetText(text); + + var saveSoundMenu = new WPFMenuItem(_root.IdentifyFromLogicalTreeIndex(0, 3, 0, 7)); + var saveWaveAsync = new Async(); + saveSoundMenu.EmulateClick(saveWaveAsync); + + var saveWaveWindow = _root.WaitForNextModal(); + SaveFileDialog saveFileDialog = null; + Async okAsync = null; + if (saveWaveWindow.TypeFullName == "AI.Talk.Editor.SaveWaveWindow") + { + ExportSettings settings = new ExportSettings(); + ExportSetting(saveWaveWindow, false, settings); + settings.SplitSetting = ExportSplitSetting.OneFile; + settings.SaveWithText = false; + ExportSetting(saveWaveWindow, true, settings); + + var okButton = new WPFButtonBase(saveWaveWindow.IdentifyFromLogicalTreeIndex(0, 1, 0)); + okAsync = new Async(); + okButton.EmulateClick(okAsync); + + saveFileDialog = new SaveFileDialog(saveWaveWindow.WaitForNextModal()); + } + else + { + // 設定の「音声保存時に毎回設定を表示する」の場合は設定画面が出ない + // 設定を変更できないのであとの処理でエラーになる可能性がある + Console.Error.WriteLine("「音声保存時に毎回設定を表示する」にチェックが入っていないためエラーが発生する可能性があります"); + saveFileDialog = new SaveFileDialog(saveWaveWindow); + } + + var filePath = Path.Combine(Path.GetTempPath(), $"{this.GetType().Name}_{(uint)text.GetHashCode()}.wav"); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + saveFileDialog.Save(filePath); + + while (true) + { + var dialog = _app.FromZTop(); + if (dialog.TypeFullName == "AI.Talk.Editor.ProgressWindow") + { + Thread.Sleep(50); + continue; + } + var button = dialog.GetFromWindowClass("Button"); + foreach (var b in button) + { + var nb = new NativeButton(b); + nb.EmulateClick(); + } + break; + } + okAsync?.WaitForCompletion(); + saveWaveAsync.WaitForCompletion(); + return SoundStream.Open(filePath); + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/Voiceroid64Controller.cs b/src/Speech/Controller/Voiceroid64Controller.cs index 0b3e2be..88f6d61 100644 --- a/src/Speech/Controller/Voiceroid64Controller.cs +++ b/src/Speech/Controller/Voiceroid64Controller.cs @@ -1,6 +1,7 @@ using Codeer.Friendly; using Codeer.Friendly.Windows; using Codeer.Friendly.Windows.Grasp; +using Codeer.Friendly.Windows.NativeStandardControls; using RM.Friendly.WPFStandardControls; using System; using System.Collections.Generic; @@ -104,6 +105,14 @@ private void timer_Elapsed(object sender, EventArgs e) } } + private bool CheckPlaying() + { + WPFButtonBase playButton = new WPFButtonBase(_root.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 0)); + var d = playButton.LogicalTree(); + System.Windows.Visibility v = (System.Windows.Visibility)(d[2])["Visibility"]().Core; + return !System.Windows.Visibility.Visible.Equals(v); + } + private void StopSpeech() { _timer.Stop(); @@ -177,12 +186,11 @@ public void Activate() /// 再生する文字列 public void Play(string text) { - SetText(text); + SetTextAndPlay(text); } - internal virtual void SetText(string text) + internal virtual void SetTextAndPlay(string text) { - text = text.Trim() == "" ? "." : text; - string t = _libraryName + _promptString + text; + string t = AssembleText(text); if (_queue.Count == 0) { WPFTextBox textbox = new WPFTextBox(_root.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2)); @@ -194,6 +202,17 @@ internal virtual void SetText(string text) _queue.Enqueue(t); } } + internal virtual void SetText(string text) + { + string t = AssembleText(text); + WPFTextBox textbox = new WPFTextBox(_root.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2)); + textbox.EmulateChangeText(t); + } + internal virtual string AssembleText(string text) + { + text = text.Trim() == "" ? "." : text; + return _libraryName + _promptString + text; + } /// /// VOICEROID2 に入力された文字列を再生します @@ -295,6 +314,79 @@ private float GetEffect(EffectType t) return Convert.ToSingle(textbox.Text); } + public SoundStream ExportToStream(string text) + { + if (CheckPlaying()) + { + // 再生中だと音声保存メニューを開けない + throw new InvalidOperationException("再生中のため処理できません"); + } + + var top = _app.FromZTop(); + if (top.TypeFullName != "AI.Talk.Editor.MainWindow") + { + // TODO: 復帰処理を書く + throw new InvalidOperationException("何らかのウィンドウが開かれているため処理できません"); + } + SetText(text); + + var saveSoundMenu = new WPFMenuItem(_root.IdentifyFromLogicalTreeIndex(0, 3, 0, 7)); + var saveWaveAsync = new Async(); + saveSoundMenu.EmulateClick(saveWaveAsync); + + var saveWaveWindow = _root.WaitForNextModal(); + SaveFileDialog saveFileDialog = null; + Async okAsync = null; + if (saveWaveWindow.TypeFullName == "AI.Talk.Editor.SaveWaveWindow") + { + Voiceroid2Controller.ExportSettings settings = new Voiceroid2Controller.ExportSettings(); + Voiceroid2Controller.ExportSetting(saveWaveWindow, false, settings); + settings.SplitSetting = Voiceroid2Controller.ExportSplitSetting.OneFile; + settings.SaveWithText = false; + Voiceroid2Controller.ExportSetting(saveWaveWindow, true, settings); + + var okButton = new WPFButtonBase(saveWaveWindow.IdentifyFromLogicalTreeIndex(0, 1, 0)); + okAsync = new Async(); + okButton.EmulateClick(okAsync); + + saveFileDialog = new SaveFileDialog(saveWaveWindow.WaitForNextModal()); + } + else + { + // 設定の「音声保存時に毎回設定を表示する」の場合は設定画面が出ない + // 設定を変更できないのであとの処理でエラーになる可能性がある + Console.Error.WriteLine("「音声保存時に毎回設定を表示する」にチェックが入っていないためエラーが発生する可能性があります"); + saveFileDialog = new SaveFileDialog(saveWaveWindow); + } + + var filePath = Path.Combine(Path.GetTempPath(), $"{this.GetType().Name}_{(uint)text.GetHashCode()}.wav"); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + saveFileDialog.Save(filePath); + + while (true) + { + var dialog = _app.FromZTop(); + if (dialog.TypeFullName == "AI.Talk.Editor.ProgressWindow") + { + Thread.Sleep(50); + continue; + } + var button = dialog.GetFromWindowClass("Button"); + foreach (var b in button) + { + var nb = new NativeButton(b); + nb.EmulateClick(); + } + break; + } + okAsync?.WaitForCompletion(); + saveWaveAsync.WaitForCompletion(); + return SoundStream.Open(filePath); + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/Controller/VoiceroidPlusController.cs b/src/Speech/Controller/VoiceroidPlusController.cs index 722b65d..7c86c9d 100644 --- a/src/Speech/Controller/VoiceroidPlusController.cs +++ b/src/Speech/Controller/VoiceroidPlusController.cs @@ -120,9 +120,7 @@ public void Activate() /// 再生する文字列 public void Play(string text) { - WindowControl speechTextBox = _root.IdentifyFromZIndex(2, 0, 0, 1, 0, 1, 1); - AppVar textbox = speechTextBox.AppVar; - textbox["Text"](text); + SetText(text); Play(); } /// @@ -140,6 +138,12 @@ public virtual void Play() _timer.Start(); } } + internal virtual void SetText(string text) + { + WindowControl speechTextBox = _root.IdentifyFromZIndex(2, 0, 0, 1, 0, 1, 1); + AppVar textbox = speechTextBox.AppVar; + textbox["Text"](text); + } /// /// VOICEROID+ の再生を停止します(停止ボタンを押す) /// @@ -262,6 +266,38 @@ protected void RestoreMinimizedWindow() } } + public SoundStream ExportToStream(string text) + { + SetText(text); + + WindowControl playButton = _root.IdentifyFromZIndex(2, 0, 0, 1, 0, 1, 0, 1); + AppVar button = playButton.AppVar; + string ButtonText = (string)button["Text"]().Core; + if (ButtonText.Trim() != "音声保存") + { + return null; + } + var saveTask = Task.Run(() => button["PerformClick"]()); + var saveFileDialog = new SaveFileDialog(_root.WaitForNextModal()); + + var filePathBase = Path.Combine(Path.GetTempPath(), $"{this.GetType().Name}_{(uint)text.GetHashCode()}"); + var filePath = $"{filePathBase}.wav"; + var textFilePath = $"{filePathBase}.txt"; + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + if (File.Exists(textFilePath)) + { + // テキストファイルを出力する設定の場合テキストファイルが出力されるので削除する + File.Delete(textFilePath); + } + saveFileDialog.Save(filePath); + + saveTask.Wait(); + return SoundStream.Open(filePath); + } + #region IDisposable Support private bool disposedValue = false; diff --git a/src/Speech/SaveFileDialog.cs b/src/Speech/SaveFileDialog.cs new file mode 100644 index 0000000..6ad6ec6 --- /dev/null +++ b/src/Speech/SaveFileDialog.cs @@ -0,0 +1,55 @@ +using Codeer.Friendly.Windows.Grasp; +using Codeer.Friendly.Windows.NativeStandardControls; + +namespace Speech +{ + /// + /// 名前を付けて保存ダイアログをラップします + /// + internal class SaveFileDialog + { + /// + /// このクラスでラップしているWindowControlインスタンスを取得します + /// + public WindowControl Window { get; private set; } + + /// + /// ファイル保存ダイアログを指定して初期化します + /// + /// 名前を付けて保存ダイアログのインスタンス + public SaveFileDialog(WindowControl dialog) + { + Window = dialog; + } + + /// + /// ダイアログのファイル名を設定します + /// + /// 保存先パス + public void SetFilePath(string path) + { + var combobox = Window.GetFromWindowClass("ComboBox"); + var textbox = new NativeEdit(combobox[combobox.Length - 1]); // 一番最後に取得できたものをファイルパス指定とする + textbox.EmulateChangeText(path); + } + + /// + /// 保存ボタンを押します + /// + public void Save() + { + var save = new NativeButton(Window.IdentifyFromWindowText("保存(&S)")); // TODO: 日本語環境でしか動かない + save.EmulateClick(); + } + + /// + /// ファイル名を設定して保存ボタンを押します + /// + /// 保存先パス + public void Save(string path) + { + SetFilePath(path); + Save(); + } + } +} diff --git a/src/Speech/SoundStream.cs b/src/Speech/SoundStream.cs new file mode 100644 index 0000000..fa0989c --- /dev/null +++ b/src/Speech/SoundStream.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; + +namespace Speech +{ + public class SoundStream : Stream, IDisposable + { + /// + /// ファイルパスを指定してインスタンスを初期化します + /// + /// ファイルパス + /// Close時にファイルを削除するか指定します + public static SoundStream Open(string path, bool deleteOnClose = true) + { + var fo = FileOptions.SequentialScan; + if(deleteOnClose) + { + fo |= FileOptions.DeleteOnClose; + } + var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 524288, fo); + return new SoundStream(fs); + } + + /// + /// 元となるStreamを指定してインスタンスを初期化します + /// + /// 元となるStreamインスタンス + public SoundStream(Stream stream) + { + this.BaseStream = stream; + } + public Stream BaseStream { get; private set; } + + public override bool CanRead => BaseStream.CanRead; + + public override bool CanSeek => BaseStream.CanSeek; + + public override bool CanWrite => BaseStream.CanWrite; + + public override long Length => BaseStream.Length; + + public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; } + + public override void Flush() + { + BaseStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return BaseStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return BaseStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + BaseStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + BaseStream.Write(buffer, offset, count); + } + + public new void Dispose() + { + BaseStream?.Dispose(); + } + } +} diff --git a/src/Speech/Speech.csproj b/src/Speech/Speech.csproj index 8eba9fc..1888624 100644 --- a/src/Speech/Speech.csproj +++ b/src/Speech/Speech.csproj @@ -56,37 +56,49 @@ ..\packages\Codeer.Friendly.2.6.1\lib\net40\Codeer.Friendly.Dynamic.dll - - ..\packages\Codeer.Friendly.Windows.2.15.0\lib\net20\Codeer.Friendly.Windows.dll + + ..\packages\Codeer.Friendly.Windows.2.16.0\lib\net20\Codeer.Friendly.Windows.dll - - ..\packages\Codeer.Friendly.Windows.Grasp.2.12.0\lib\net35\Codeer.Friendly.Windows.Grasp.2.0.dll + + ..\packages\Codeer.Friendly.Windows.Grasp.2.14.1\lib\net35\Codeer.Friendly.Windows.Grasp.2.0.dll - - ..\packages\Codeer.Friendly.Windows.Grasp.2.12.0\lib\net35\Codeer.Friendly.Windows.Grasp.3.5.dll + + ..\packages\Codeer.Friendly.Windows.Grasp.2.14.1\lib\net35\Codeer.Friendly.Windows.Grasp.3.5.dll - - ..\packages\Codeer.TestAssistant.GeneratorToolKit.3.10.0\lib\net20\Codeer.TestAssistant.GeneratorToolKit.dll + + ..\packages\Codeer.Friendly.Windows.NativeStandardControls.2.16.9\lib\net40\Codeer.Friendly.Windows.NativeStandardControls.dll + + + ..\packages\Codeer.Friendly.Windows.NativeStandardControls.2.16.9\lib\net40\Codeer.Friendly.Windows.NativeStandardControls.4.0.dll + + + ..\packages\Codeer.Friendly.Windows.NativeStandardControls.2.16.9\lib\net40\Codeer.Friendly.Windows.NativeStandardControls.Generator.dll + + + ..\packages\Codeer.TestAssistant.GeneratorToolKit.3.13.0\lib\net40\Codeer.TestAssistant.GeneratorToolKit.dll + + + ..\packages\Codeer.TestAssistant.GeneratorToolKit.3.13.0\lib\net40\Codeer.TestAssistant.GeneratorToolKit.4.0.dll ..\packages\NAudio.1.8.4\lib\net35\NAudio.dll - - ..\packages\RM.Friendly.WPFStandardControls.1.46.1\lib\net40\RM.Friendly.WPFStandardControls.3.0.dll + + ..\packages\RM.Friendly.WPFStandardControls.1.59.0\lib\net40\RM.Friendly.WPFStandardControls.3.0.dll - - ..\packages\RM.Friendly.WPFStandardControls.1.46.1\lib\net40\RM.Friendly.WPFStandardControls.3.0.Generator.dll + + ..\packages\RM.Friendly.WPFStandardControls.1.59.0\lib\net40\RM.Friendly.WPFStandardControls.3.0.Generator.dll - - ..\packages\RM.Friendly.WPFStandardControls.1.46.1\lib\net40\RM.Friendly.WPFStandardControls.3.5.dll + + ..\packages\RM.Friendly.WPFStandardControls.1.59.0\lib\net40\RM.Friendly.WPFStandardControls.3.5.dll - - ..\packages\RM.Friendly.WPFStandardControls.1.46.1\lib\net40\RM.Friendly.WPFStandardControls.4.0.dll + + ..\packages\RM.Friendly.WPFStandardControls.1.59.0\lib\net40\RM.Friendly.WPFStandardControls.4.0.dll - - ..\packages\RM.Friendly.WPFStandardControls.1.46.1\lib\net40\RM.Friendly.WPFStandardControls.4.0.Generator.dll + + ..\packages\RM.Friendly.WPFStandardControls.1.59.0\lib\net40\RM.Friendly.WPFStandardControls.4.0.Generator.dll @@ -104,6 +116,7 @@ + @@ -130,6 +143,7 @@ + diff --git a/src/Speech/app.config b/src/Speech/app.config index 3c048ae..92d2591 100644 --- a/src/Speech/app.config +++ b/src/Speech/app.config @@ -8,12 +8,20 @@ - + + + + + + + + + \ No newline at end of file diff --git a/src/Speech/packages.config b/src/Speech/packages.config index 42ddcee..1bcef8f 100644 --- a/src/Speech/packages.config +++ b/src/Speech/packages.config @@ -1,9 +1,10 @@  - - - + + + + - + \ No newline at end of file diff --git a/src/SpeechWebServer/Program.cs b/src/SpeechWebServer/Program.cs index 106e7fa..fbf11ce 100644 --- a/src/SpeechWebServer/Program.cs +++ b/src/SpeechWebServer/Program.cs @@ -158,7 +158,31 @@ static void Main(string[] args) whisper = true; } + bool export = false; + if (queryString["export"] != null) + { + bool.TryParse(queryString["export"], out export); + } + Console.WriteLine("=> " + context.Request.RemoteEndPoint.Address); + if (export) + { + try + { + using (var result = ExportMode(voiceName, engineName, voiceText, location, ep)) + { + response.StatusCode = 200; + response.ContentType = "audio/wav"; + result.CopyTo(response.OutputStream); + } + } + catch (Exception ex) + { + response.StatusCode = 500; + throw new Exception("Error", ex); + } + continue; + } response.StatusCode = 200; response.ContentType = "text/plain; charset=utf-8"; @@ -235,6 +259,20 @@ private static ISpeechController ActivateInstance(string libraryName, string eng return engine; } + private static Stream ExportMode(string libraryName, string engineName, string text, string location, EngineParameters ep) + { + var engine = ActivateInstance(libraryName, engineName, text, location, ep); + if (engine == null) + { + return null; + } + engine.Finished += (s, a) => + { + engine.Dispose(); + }; + return engine.ExportToStream(text); + } + private static void OneShotPlayMode(string libraryName, string engineName, string text, string location, EngineParameters ep) { var engine = ActivateInstance(libraryName, engineName, text, location, ep);