├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── SimpleVoiceroid2Proxy.sln ├── SimpleVoiceroid2Proxy ├── Logger.cs ├── Program.cs ├── Server │ ├── Controller.cs │ ├── HttpContext.cs │ └── HttpServer.cs ├── SimpleVoiceroid2Proxy.csproj ├── Utils.cs ├── VoiceroidEngine.cs ├── app.manifest └── packages.lock.json └── renovate.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'SimpleVoiceroid2Proxy/**' 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup MSBuild 19 | uses: microsoft/setup-msbuild@v2 20 | 21 | - name: MSBuild 22 | run: msbuild SimpleVoiceroid2Proxy.sln -property:Configuration="Release" -property:Platform="Any CPU" -m -restore 23 | 24 | - name: Upload Artifacts 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: SimpleVoiceroid2Proxy 28 | if-no-files-found: error 29 | path: SimpleVoiceroid2Proxy/bin/Release 30 | 31 | - name: Create Release 32 | shell: powershell 33 | if: startsWith(github.ref, 'refs/tags/') 34 | run: Compress-Archive -Path SimpleVoiceroid2Proxy/bin/Release/* -DestinationPath SimpleVoiceroid2Proxy.zip 35 | 36 | - name: Upload Release 37 | uses: softprops/action-gh-release@v2 38 | if: startsWith(github.ref, 'refs/tags/') 39 | with: 40 | files: SimpleVoiceroid2Proxy.zip 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleVoiceroid2Proxy 2 | 3 | VOICEROID2 を HTTP API で操作して読み上げさせるコンソールアプリケーションです。(要管理者権限) 4 | 5 | [Voiceroid2Proxy](https://github.com/kanosaki/Voiceroid2Proxy) を基に機能拡張を行いました。 6 | 7 | ![main](https://user-images.githubusercontent.com/7302150/138386989-04c02510-18d7-4903-be67-ceb30bec3771.png) 8 | 9 | ## ACT.Hojoring との連携 10 | 11 | [ACT.Hojoring](https://github.com/anoyetta/ACT.Hojoring) で棒読みちゃんの代わりに使用することで **低遅延** **UI 妨害のない** 読み上げが可能です。 12 | FFXIV プレイ中に読み上げが行われてもウィンドウが前面に出現しないため, 操作がブロックされることはありません。 13 | 14 | ![act](https://user-images.githubusercontent.com/7302150/138386948-cda0e694-c93e-47e9-a54a-eee8a00819be.png) 15 | 16 | ## Discord との連携 17 | 18 | BetterDiscord 用のプラグイン [VoiceChatNotificationsForVoiceroid2Proxy.plugin.js](https://github.com/SlashNephy/.github/blob/master/env/discord/plugins/VoiceChatNotificationsForVoiceroid2Proxy.plugin.js) を使用することで, VC 関連の通知 (VC に人が入ってきた場合, ミュートした場合など) を VOICEROID2 に読み上げさせることが可能です。 19 | 20 | ## HTTP API 21 | 22 | 現在実装されている HTTP API は以下の通りです。 23 | LAN に `4532/tcp` を開放するので同一ネットワーク内から操作が可能です。 24 | 25 | - `GET /talk` 26 | パラメータ `text` を渡すことで読み上げを行えます。ただし `GET` リクエストは URL の長さの制約を受けるので `POST` を推奨します。(ACT.Hojoring との互換性のため `GET` 対応しています) 27 | 28 | 「てすと」を読み上げる例: 29 | ``` 30 | http://localhost:4532/talk?text=%E3%81%A6%E3%81%99%E3%81%A8 31 | ``` 32 | 33 | - `POST /talk` 34 | ペイロードとして JSON を送信することで読み上げを行えます。 35 | 36 | 「てすと」を読み上げる例: 37 | ```json 38 | { 39 | "text": "てすと" 40 | } 41 | ``` 42 | 43 | ## コマンド 44 | 45 | 特定の文字列を `text` に含めることで特殊な操作を行えます。 46 | 47 | - `結月ゆかり>` 48 | 話者名を指定することで特定の話者に読み上げさせることが可能です。(VOICEROID2 の機能で、VOICEROID2 側の設定から記号を変更できます) 49 | 他の話者も指定可能です。 50 | この指定は `text` の先頭で行う必要があります。 51 | 52 | - `` 53 | 読み上げのキューをクリアします。 54 | 55 | - ``, `` 56 | 現在の読み上げを 一時停止 / 再開 します。 57 | 58 | - ``, `` 59 | 読み上げの割り込みモードを 有効化 / 無効化 します。 60 | デフォルトでは割り込み (読み上げ中に別のテキストを受け取ると中断し, 新しいテキストを読み上げます) が有効になっています。 61 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleVoiceroid2Proxy", "SimpleVoiceroid2Proxy\SimpleVoiceroid2Proxy.csproj", "{163B3D21-D738-4F74-9362-4C65E5F0E15B}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {163B3D21-D738-4F74-9362-4C65E5F0E15B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {163B3D21-D738-4F74-9362-4C65E5F0E15B}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {163B3D21-D738-4F74-9362-4C65E5F0E15B}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {163B3D21-D738-4F74-9362-4C65E5F0E15B}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleVoiceroid2Proxy; 4 | 5 | public sealed class ConsoleLogger : ILogger 6 | { 7 | public static readonly ILogger Instance = new ConsoleLogger(); 8 | 9 | public void Debug(string message) 10 | { 11 | #if DEBUG 12 | Log(LogLevel.DEBUG, message); 13 | #endif 14 | } 15 | 16 | public void Info(string message) 17 | { 18 | Log(LogLevel.INFO, message); 19 | } 20 | 21 | public void Warn(string message) 22 | { 23 | Log(LogLevel.WARN, message); 24 | } 25 | 26 | public void Error(Exception exception, string message) 27 | { 28 | Log(LogLevel.ERROR, $"{message}\n{exception}"); 29 | } 30 | 31 | private void Log(LogLevel level, string message) 32 | { 33 | Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}"); 34 | } 35 | } 36 | 37 | public interface ILogger 38 | { 39 | public void Debug(string message); 40 | public void Info(string message); 41 | public void Warn(string message); 42 | public void Error(Exception exception, string message); 43 | } 44 | 45 | public enum LogLevel 46 | { 47 | DEBUG, 48 | INFO, 49 | WARN, 50 | ERROR, 51 | } 52 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using SimpleVoiceroid2Proxy.Server; 3 | 4 | namespace SimpleVoiceroid2Proxy; 5 | 6 | public static class Program 7 | { 8 | private static readonly HttpServer server = new(); 9 | public static readonly VoiceroidEngine VoiceroidEngine = new(); 10 | 11 | static Program() 12 | { 13 | Utils.KillDuplicateProcesses(); 14 | } 15 | 16 | public static void Main() 17 | { 18 | Task.WaitAll( 19 | server.ListenAsync(), 20 | VoiceroidEngine.TalkAsync("準備完了!") 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/Server/Controller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Linq; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | using System.Web; 9 | 10 | namespace SimpleVoiceroid2Proxy.Server; 11 | 12 | public sealed class Controller 13 | { 14 | public async Task HandleAsync(HttpContext context) 15 | { 16 | try 17 | { 18 | using (context) 19 | { 20 | switch (context.Request.Url.AbsolutePath) 21 | { 22 | case "/talk" when context.Request.HttpMethod == "GET": 23 | await HandleGetTalkAsync(context); 24 | return; 25 | case "/talk" when context.Request.HttpMethod == "POST": 26 | await HandlePostTalkAsync(context); 27 | return; 28 | case "/talk" when context.Request.HttpMethod == "OPTIONS": 29 | HandleOptionsTalk(context); 30 | return; 31 | default: 32 | await context.RespondJson(HttpStatusCode.NotFound, new Dictionary{ 33 | {"success", false}, 34 | {"message", "not found."}, 35 | }); 36 | return; 37 | } 38 | } 39 | } 40 | catch (Exception exception) 41 | { 42 | ConsoleLogger.Instance.Error(exception, "internal server error occurred"); 43 | 44 | await context.RespondJson(HttpStatusCode.InternalServerError, new Dictionary{ 45 | {"success", false}, 46 | {"message", "internal server error occurred."}, 47 | }); 48 | } 49 | } 50 | 51 | private async Task HandleGetTalkAsync(HttpContext context) 52 | { 53 | await HandleTalkAsync(context, context.Query.GetValues("text")?.FirstOrDefault()); 54 | } 55 | 56 | private async Task HandlePostTalkAsync(HttpContext context) 57 | { 58 | string? text = null; 59 | if (context.RequestMediaType == "application/x-www-form-urlencoded") 60 | { 61 | using var reader = new StreamReader(context.Request.InputStream); 62 | var body = reader.ReadToEnd(); 63 | var form = HttpUtility.ParseQueryString(body); 64 | text = form.GetValues("text")?.FirstOrDefault(); 65 | } 66 | else 67 | { 68 | var payload = await JsonSerializer.DeserializeAsync>(context.Request.InputStream); 69 | text = payload!["text"].GetString(); 70 | } 71 | 72 | await HandleTalkAsync(context, text); 73 | } 74 | 75 | private async Task HandleTalkAsync(HttpContext context, string? text) 76 | { 77 | context.Response.AddHeader("Content-Type", "application/json"); 78 | context.Response.AddHeader("Access-Control-Allow-Origin", "*"); 79 | context.Response.AddHeader("Access-Control-Allow-Method", "GET,POST"); 80 | context.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type"); 81 | 82 | if (string.IsNullOrWhiteSpace(text)) 83 | { 84 | await context.RespondJson(HttpStatusCode.BadRequest, new Dictionary 85 | { 86 | {"success", false}, 87 | {"message", "`text` parameter is null or empty."}, 88 | }); 89 | return; 90 | } 91 | 92 | await Program.VoiceroidEngine.TalkAsync(text!); 93 | await context.RespondJson(HttpStatusCode.OK, new Dictionary 94 | { 95 | {"success", true}, 96 | {"message", $"Talked `{text}`."}, 97 | {"text", text}, 98 | }); 99 | } 100 | 101 | private void HandleOptionsTalk(HttpContext context) 102 | { 103 | context.Response.AddHeader("Access-Control-Allow-Origin", "*"); 104 | context.Response.AddHeader("Access-Control-Allow-Method", "GET, POST, OPTIONS"); 105 | context.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type"); 106 | context.Response.AddHeader("Access-Control-Max-Age", "7200"); 107 | context.Response.StatusCode = (int)HttpStatusCode.NoContent; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/Server/HttpContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Net; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | using System.Web; 10 | 11 | namespace SimpleVoiceroid2Proxy.Server; 12 | 13 | public sealed class HttpContext(HttpListenerContext Context) : IDisposable 14 | { 15 | public HttpListenerRequest Request => Context.Request; 16 | public HttpListenerResponse Response => Context.Response; 17 | public NameValueCollection Query => HttpUtility.ParseQueryString(Request.Url.Query, Encoding.UTF8); 18 | public string? RequestMediaType 19 | { 20 | get 21 | { 22 | MediaTypeHeaderValue.TryParse(Request.ContentType, out var value); 23 | return value.MediaType; 24 | } 25 | } 26 | 27 | public async Task RespondJson(HttpStatusCode code, Dictionary payload) 28 | { 29 | await JsonSerializer.SerializeAsync(Context.Response.OutputStream, payload); 30 | Response.StatusCode = (int)code; 31 | } 32 | 33 | public void Dispose() 34 | { 35 | Response.Close(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/Server/HttpServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | 5 | namespace SimpleVoiceroid2Proxy.Server; 6 | 7 | public sealed class HttpServer : IDisposable 8 | { 9 | private const int Port = 4532; 10 | 11 | private readonly HttpListener listener = new(); 12 | private readonly Controller controller = new(); 13 | 14 | public HttpServer() 15 | { 16 | listener.Prefixes.Add($"http://+:{Port}/"); 17 | } 18 | 19 | public async Task ListenAsync() 20 | { 21 | listener.Start(); 22 | 23 | while (listener.IsListening) 24 | { 25 | try 26 | { 27 | var context = await listener.GetContextAsync(); 28 | 29 | var request = new HttpContext(context); 30 | await controller.HandleAsync(request); 31 | } 32 | catch 33 | { 34 | return; 35 | } 36 | } 37 | } 38 | 39 | public void Dispose() 40 | { 41 | listener.Close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/SimpleVoiceroid2Proxy.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net481 4 | x86 5 | Exe 6 | preview 7 | enable 8 | true 9 | app.manifest 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace SimpleVoiceroid2Proxy; 7 | 8 | public static class Utils 9 | { 10 | public static void KillDuplicateProcesses() 11 | { 12 | var currentProcess = Process.GetCurrentProcess(); 13 | var imageName = Assembly.GetExecutingAssembly() 14 | .Location 15 | .Split(Path.DirectorySeparatorChar) 16 | .Last() 17 | .Replace(".exe", ""); 18 | 19 | foreach (var process in Process.GetProcessesByName(imageName)) 20 | { 21 | if (process.Id != currentProcess.Id) 22 | { 23 | process.Kill(); 24 | process.WaitForExit(); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/VoiceroidEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text.RegularExpressions; 6 | using System.Threading; 7 | using System.Threading.Channels; 8 | using System.Threading.Tasks; 9 | using System.Windows.Controls; 10 | using Codeer.Friendly.Windows; 11 | using Codeer.Friendly.Windows.Grasp; 12 | using Microsoft.Win32; 13 | using RM.Friendly.WPFStandardControls; 14 | 15 | namespace SimpleVoiceroid2Proxy; 16 | 17 | public class VoiceroidEngine : IDisposable 18 | { 19 | private readonly Process process; 20 | private readonly WindowsAppFriend app; 21 | 22 | private readonly WPFTextBox talkTextBox; 23 | private readonly WPFButtonBase playButton; 24 | private readonly WPFButtonBase stopButton; 25 | private readonly WPFButtonBase moveButton; 26 | 27 | private readonly Channel queue = Channel.CreateUnbounded(); 28 | private static readonly TimeSpan TalkCooldown = TimeSpan.FromMilliseconds(200); 29 | private DateTime lastPlay; 30 | private volatile bool interrupt = true; 31 | private volatile bool paused; 32 | 33 | public VoiceroidEngine() 34 | { 35 | ConsoleLogger.Instance.Info("VOICEROID init started..."); 36 | 37 | process = GetOrCreateVoiceroidProcess(); 38 | app = new WindowsAppFriend(process); 39 | 40 | try 41 | { 42 | // Timeout = 60 sec 43 | for (var i = 0; i < 1200; i++) 44 | { 45 | var window = app.FromZTop(); 46 | WinApi.ShowWindow(window.Handle, WinApi.SwMinimize); 47 | 48 | var tree = window.GetFromTypeFullName("AI.Talk.Editor.TextEditView") 49 | .FirstOrDefault() 50 | ?.LogicalTree(); 51 | if (tree == null) 52 | { 53 | Thread.Sleep(50); 54 | continue; 55 | } 56 | 57 | var text = tree.ByType().Single(); 58 | var play = tree.ByBinding("PlayCommand").Single(); 59 | var stop = tree.ByBinding("StopCommand").Single(); 60 | // var move = tree.ByBinding("MoveToBeginningCommand").Single(); 61 | 62 | talkTextBox = new WPFTextBox(text); 63 | playButton = new WPFButtonBase(play); 64 | stopButton = new WPFButtonBase(stop); 65 | moveButton = new WPFButtonBase(tree[15]); 66 | // moveButton = new WPFButtonBase(move); 67 | 68 | ConsoleLogger.Instance.Info("VOICEROID ready!"); 69 | Task.Run(ConsumeAsync); 70 | 71 | return; 72 | } 73 | } 74 | catch (Exception exception) 75 | { 76 | ConsoleLogger.Instance.Error(exception, "VOICEROID init failed."); 77 | throw new ApplicationException("VOICEROID init failed."); 78 | } 79 | 80 | throw new TimeoutException("VOICEROID init timed out."); 81 | } 82 | 83 | public void Dispose() 84 | { 85 | queue.Writer.TryComplete(); 86 | app.Dispose(); 87 | process.Kill(); 88 | process.Dispose(); 89 | } 90 | 91 | private static readonly Regex BracketRegex = new(@"<(?.+?)>", RegexOptions.Compiled); 92 | 93 | public async Task TalkAsync(string text) 94 | { 95 | text = BracketRegex.Replace(text, match => 96 | { 97 | switch (match.Groups["command"].Value) 98 | { 99 | case "clear": 100 | while (queue.Reader.TryRead(out _)) 101 | { 102 | } 103 | 104 | ConsoleLogger.Instance.Info("**********"); 105 | break; 106 | case "pause": 107 | paused = true; 108 | 109 | ConsoleLogger.Instance.Info("********** => Paused."); 110 | break; 111 | case "resume": 112 | paused = false; 113 | 114 | ConsoleLogger.Instance.Info("********** => Resumed."); 115 | break; 116 | case "interrupt_enable": 117 | interrupt = true; 118 | 119 | ConsoleLogger.Instance.Info("********** => Interrupt enabled."); 120 | break; 121 | case "interrupt_disable": 122 | interrupt = false; 123 | 124 | ConsoleLogger.Instance.Info("********** => Interrupt disabled."); 125 | break; 126 | default: 127 | return match.Value; 128 | } 129 | 130 | return string.Empty; 131 | }); 132 | 133 | await queue.Writer.WriteAsync(text); 134 | } 135 | 136 | private async Task ConsumeAsync() 137 | { 138 | while (await queue.Reader.WaitToReadAsync()) 139 | { 140 | while (queue.Reader.TryRead(out var text)) 141 | { 142 | await SpeakAsync(text); 143 | } 144 | } 145 | } 146 | 147 | private async Task SpeakAsync(string text) 148 | { 149 | // VOICEROID2 が発話中の時は「先頭」ボタンが無効になるので、それを利用して発話中かどうかを判定します 150 | while (!interrupt && !moveButton.IsEnabled) 151 | { 152 | await Task.Delay(50); // spin wait 153 | } 154 | 155 | while (paused) 156 | { 157 | await Task.Delay(500); 158 | } 159 | 160 | var cooldown = TalkCooldown - (DateTime.Now - lastPlay); 161 | if (cooldown.TotalMilliseconds > 0) 162 | { 163 | await Task.Delay(cooldown); 164 | } 165 | 166 | stopButton.EmulateClick(); 167 | talkTextBox.EmulateChangeText(text); 168 | moveButton.EmulateClick(); 169 | playButton.EmulateClick(); 170 | 171 | lastPlay = DateTime.Now; 172 | ConsoleLogger.Instance.Info($"=> {text}"); 173 | } 174 | 175 | private static Process GetOrCreateVoiceroidProcess() 176 | { 177 | return (Process.GetProcessesByName("VoiceroidEditor").FirstOrDefault() ?? Process.Start(new ProcessStartInfo 178 | { 179 | FileName = FindVoiceroidPath(), 180 | WindowStyle = ProcessWindowStyle.Minimized, 181 | }))!; 182 | } 183 | 184 | private static string FindVoiceroidPath() 185 | { 186 | return Registry.ClassesRoot 187 | .OpenSubKey(@"Installer\Assemblies") 188 | ?.GetSubKeyNames() 189 | .Where(x => x.EndsWith("VoiceroidEditor.exe")) 190 | .Select(x => x.Replace('|', '\\')) 191 | .FirstOrDefault() ?? throw new ApplicationException("VOICEROID not found."); 192 | } 193 | 194 | private static class WinApi 195 | { 196 | public const int SwMinimize = 6; 197 | 198 | [DllImport("user32.dll")] 199 | public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 59 | 60 | 61 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SimpleVoiceroid2Proxy/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | ".NETFramework,Version=v4.8.1": { 5 | "Codeer.Friendly.Windows.Grasp": { 6 | "type": "Direct", 7 | "requested": "[2.15.0, )", 8 | "resolved": "2.15.0", 9 | "contentHash": "3M4NEwfdaBdFhZQlq228biqLl4w8jvoZcstVqZP5wg36inf7C5d3X/oaPQPVMob1QENBXZ3ZLxukJjQE0Dk5tw==", 10 | "dependencies": { 11 | "Codeer.Friendly.Windows": "2.18.0" 12 | } 13 | }, 14 | "Microsoft.NETFramework.ReferenceAssemblies": { 15 | "type": "Direct", 16 | "requested": "[1.0.3, )", 17 | "resolved": "1.0.3", 18 | "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", 19 | "dependencies": { 20 | "Microsoft.NETFramework.ReferenceAssemblies.net481": "1.0.3" 21 | } 22 | }, 23 | "RM.Friendly.WPFStandardControls": { 24 | "type": "Direct", 25 | "requested": "[1.60.0, )", 26 | "resolved": "1.60.0", 27 | "contentHash": "hp5JaOGY3eobtlwnk6aMhSu0mCu5T8OV5B2NL/lLqDBUTqM1/zBr1XJQPpUTQQ3dqLoUw6XcMeEkx/0LmS0H6w==", 28 | "dependencies": { 29 | "Codeer.Friendly.Windows.Grasp": "2.15.0", 30 | "Codeer.TestAssistant.GeneratorToolKit": "3.13.0" 31 | } 32 | }, 33 | "System.Text.Json": { 34 | "type": "Direct", 35 | "requested": "[8.0.5, )", 36 | "resolved": "8.0.5", 37 | "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==", 38 | "dependencies": { 39 | "Microsoft.Bcl.AsyncInterfaces": "8.0.0", 40 | "System.Buffers": "4.5.1", 41 | "System.Memory": "4.5.5", 42 | "System.Runtime.CompilerServices.Unsafe": "6.0.0", 43 | "System.Text.Encodings.Web": "8.0.0", 44 | "System.Threading.Tasks.Extensions": "4.5.4", 45 | "System.ValueTuple": "4.5.0" 46 | } 47 | }, 48 | "System.Threading.Channels": { 49 | "type": "Direct", 50 | "requested": "[8.0.0, )", 51 | "resolved": "8.0.0", 52 | "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==", 53 | "dependencies": { 54 | "System.Threading.Tasks.Extensions": "4.5.4" 55 | } 56 | }, 57 | "Codeer.Friendly": { 58 | "type": "Transitive", 59 | "resolved": "2.7.0", 60 | "contentHash": "WblR8Y6kaIqi9vmgZ0j8X0gdoZ6HEkDxsLEtRiBXZsnhKXzqTs6Qj40hlhjhyNzxQESTq18fXgRYVM1CGUNX7g==" 61 | }, 62 | "Codeer.Friendly.Windows": { 63 | "type": "Transitive", 64 | "resolved": "2.18.0", 65 | "contentHash": "/uhokzQwYBJwFunNi+0filmJRlUFfZc+Y3Gz0HLwDqoZZ1/+6MoX3wTTsMEl2lefgm64ZsbbZ8fPyhY8njIscg==", 66 | "dependencies": { 67 | "Codeer.Friendly": "2.7.0" 68 | } 69 | }, 70 | "Codeer.TestAssistant.GeneratorToolKit": { 71 | "type": "Transitive", 72 | "resolved": "3.13.0", 73 | "contentHash": "m+Lizo1XwZsWRo71S1se9LYdUPEcHx1TbkpZcmuqCyYyF9G1OqLBRvNWVYvbIWM+yb6ARKbtC8k7qPpzp63ASg==" 74 | }, 75 | "Microsoft.Bcl.AsyncInterfaces": { 76 | "type": "Transitive", 77 | "resolved": "8.0.0", 78 | "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", 79 | "dependencies": { 80 | "System.Threading.Tasks.Extensions": "4.5.4" 81 | } 82 | }, 83 | "Microsoft.NETFramework.ReferenceAssemblies.net481": { 84 | "type": "Transitive", 85 | "resolved": "1.0.3", 86 | "contentHash": "Vv/20vgHS7VglVOVh8J3Iz/MA+VYKVRp9f7r2qiKBMuzviTOmocG70yq0Q8T5OTmCONkEAIJwETD1zhEfLkAXQ==" 87 | }, 88 | "System.Buffers": { 89 | "type": "Transitive", 90 | "resolved": "4.5.1", 91 | "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" 92 | }, 93 | "System.Memory": { 94 | "type": "Transitive", 95 | "resolved": "4.5.5", 96 | "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", 97 | "dependencies": { 98 | "System.Buffers": "4.5.1", 99 | "System.Numerics.Vectors": "4.5.0", 100 | "System.Runtime.CompilerServices.Unsafe": "4.5.3" 101 | } 102 | }, 103 | "System.Numerics.Vectors": { 104 | "type": "Transitive", 105 | "resolved": "4.5.0", 106 | "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" 107 | }, 108 | "System.Runtime.CompilerServices.Unsafe": { 109 | "type": "Transitive", 110 | "resolved": "6.0.0", 111 | "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" 112 | }, 113 | "System.Text.Encodings.Web": { 114 | "type": "Transitive", 115 | "resolved": "8.0.0", 116 | "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", 117 | "dependencies": { 118 | "System.Buffers": "4.5.1", 119 | "System.Memory": "4.5.5", 120 | "System.Runtime.CompilerServices.Unsafe": "6.0.0" 121 | } 122 | }, 123 | "System.Threading.Tasks.Extensions": { 124 | "type": "Transitive", 125 | "resolved": "4.5.4", 126 | "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", 127 | "dependencies": { 128 | "System.Runtime.CompilerServices.Unsafe": "4.5.3" 129 | } 130 | }, 131 | "System.ValueTuple": { 132 | "type": "Transitive", 133 | "resolved": "4.5.0", 134 | "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>SlashNephy/.github:renovate-config" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------