From ba9178f45a9276a2a96b82abe58a9a66d6035853 Mon Sep 17 00:00:00 2001 From: CdrSonan <64283097+CdrSonan@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:37:43 +0200 Subject: [PATCH 1/3] Add resampler error handling with subprocess output copying --- OpenUtau.Core/Classic/ExeResampler.cs | 9 +++++++-- OpenUtau.Core/Util/ProcessRunner.cs | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/OpenUtau.Core/Classic/ExeResampler.cs b/OpenUtau.Core/Classic/ExeResampler.cs index e17557513..31524b817 100644 --- a/OpenUtau.Core/Classic/ExeResampler.cs +++ b/OpenUtau.Core/Classic/ExeResampler.cs @@ -102,10 +102,15 @@ public string DoResamplerReturnsFile(ResamplerItem args, ILogger logger) { string ArgParam = FormattableString.Invariant( $"\"{args.inputTemp}\" \"{tmpFile}\" {MusicMath.GetToneName(args.tone)} {args.velocity} \"{args.GetFlagsString()}\" {args.offset} {args.durRequired} {args.consonant} {args.cutoff} {args.volume} {args.modulation} !{args.tempo} {Base64.Base64EncodeInt12(args.pitches)}"); logger.Information($" > [thread-{threadId}] {FilePath} {ArgParam}"); + string resamplerOutput; if (useWine) { - ProcessRunner.Run(winePath, $"{FilePath} {ArgParam}", logger); + resamplerOutput = ProcessRunner.Run(winePath, $"{FilePath} {ArgParam}", logger); } else { - ProcessRunner.Run(FilePath, ArgParam, logger); + resamplerOutput = ProcessRunner.Run(FilePath, ArgParam, logger); + } + // check if the file has been created + if (!File.Exists(tmpFile)) { + throw new Exception($"Resampler failed to create output file: {tmpFile}.\nResampler output:\n{resamplerOutput}\nErrors like this are often caused by a misconfigured oto.ini. Consider generating a singer error report."); } return tmpFile; } diff --git a/OpenUtau.Core/Util/ProcessRunner.cs b/OpenUtau.Core/Util/ProcessRunner.cs index b599de30d..cc472e430 100644 --- a/OpenUtau.Core/Util/ProcessRunner.cs +++ b/OpenUtau.Core/Util/ProcessRunner.cs @@ -7,11 +7,12 @@ namespace OpenUtau.Core.Util { public static class ProcessRunner { public static bool DebugSwitch { get; set; } - public static void Run(string file, string args, ILogger logger, string workDir = null, int timeoutMs = 60000) { + public static string Run(string file, string args, ILogger logger, string workDir = null, int timeoutMs = 60000) { if (!File.Exists(file)) { throw new FileNotFoundException($"Executable {file} not found."); } var threadId = Thread.CurrentThread.ManagedThreadId; + var output = ""; using (var proc = new Process()) { proc.StartInfo = new ProcessStartInfo(file, args) { Environment = {{"LANG", "ja_JP.utf8"}}, @@ -25,12 +26,14 @@ public static void Run(string file, string args, ILogger logger, string workDir proc.OutputDataReceived += (o, e) => { if (!string.IsNullOrEmpty(e.Data)) { logger.Information($"ProcessRunner >>> [thread-{threadId}] {e.Data}"); + output += $"{e.Data}\n"; } }; } proc.ErrorDataReceived += (o, e) => { if (!string.IsNullOrEmpty(e.Data)) { logger.Error($"ProcessRunner >>> [thread-{threadId}] {e.Data}"); + output += $"{e.Data}\n"; } }; proc.Start(); @@ -42,17 +45,20 @@ public static void Run(string file, string args, ILogger logger, string workDir proc.WaitForExit(); } else { if (proc.WaitForExit(timeoutMs)) { - return; + output += $"Exit code {proc.ExitCode}"; + return output; } logger.Warning($"ProcessRunner >>> [thread-{threadId}] Timeout, killing..."); try { proc.Kill(); logger.Warning($"ProcessRunner >>> [thread-{threadId}] Killed."); + output += "Killed due to timeout."; } catch (Exception e) { logger.Error(e, $"ProcessRunner >>> [thread-{threadId}] Failed to kill"); } } } + return output; } } } From 68fa677a64f4cb378a4af61c1180b7d80f377c52 Mon Sep 17 00:00:00 2001 From: Johannes Klatt <64283097+CdrSonan@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:47:57 +0200 Subject: [PATCH 2/3] Localized and improved resampler error message and added compatibility with aggregate exceptions --- OpenUtau.Core/Render/IRenderer.cs | 3 ++ OpenUtau.Core/Render/RenderEngine.cs | 3 ++ OpenUtau.Core/Util/ProcessRunner.cs | 79 +++++++++++++++++++--------- OpenUtau/Strings/Strings.axaml | 1 + OpenUtau/Strings/Strings.de-DE.axaml | 1 + 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/OpenUtau.Core/Render/IRenderer.cs b/OpenUtau.Core/Render/IRenderer.cs index a1d420cc1..eeed8365b 100644 --- a/OpenUtau.Core/Render/IRenderer.cs +++ b/OpenUtau.Core/Render/IRenderer.cs @@ -7,6 +7,9 @@ namespace OpenUtau.Core.Render { public class NoResamplerException : Exception { } public class NoWavtoolException : Exception { } + public class ResamplerFailedException : Exception { + public ResamplerFailedException(string message) : base(message) {} + } /// /// Render result of a phrase. diff --git a/OpenUtau.Core/Render/RenderEngine.cs b/OpenUtau.Core/Render/RenderEngine.cs index 75bc3d35b..e97a3ae4f 100644 --- a/OpenUtau.Core/Render/RenderEngine.cs +++ b/OpenUtau.Core/Render/RenderEngine.cs @@ -120,6 +120,9 @@ public Tuple> RenderMixdown(TaskScheduler uiScheduler, ref } else if (innerEx.Any(e => e is DllNotFoundException)) { DocManager.Inst.ExecuteCmd(new ErrorMessageNotification( new MessageCustomizableException("Failed to render.", ": ", flatEx))); + } else if (innerEx.Any(e => e is ResamplerFailedException)) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification( + new MessageCustomizableException("Failed to render.", "", flatEx))); } else { DocManager.Inst.ExecuteCmd(new ErrorMessageNotification( new MessageCustomizableException("Failed to render.", "", flatEx))); diff --git a/OpenUtau.Core/Util/ProcessRunner.cs b/OpenUtau.Core/Util/ProcessRunner.cs index cc472e430..c30956da7 100644 --- a/OpenUtau.Core/Util/ProcessRunner.cs +++ b/OpenUtau.Core/Util/ProcessRunner.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Text; using System.Threading; using Serilog; @@ -12,53 +13,83 @@ public static string Run(string file, string args, ILogger logger, string workDi throw new FileNotFoundException($"Executable {file} not found."); } var threadId = Thread.CurrentThread.ManagedThreadId; - var output = ""; + var output = new StringBuilder(); + var outputLock = new object(); + + // Signals used to ensure the async stdout/stderr readers have flushed all data before we read `output`. + using var stdoutDone = new ManualResetEventSlim(false); + using var stderrDone = new ManualResetEventSlim(false); using (var proc = new Process()) { proc.StartInfo = new ProcessStartInfo(file, args) { - Environment = {{"LANG", "ja_JP.utf8"}}, + Environment = { { "LANG", "ja_JP.utf8" } }, UseShellExecute = false, - RedirectStandardOutput = DebugSwitch, + RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = workDir, }; - if (DebugSwitch) { - proc.OutputDataReceived += (o, e) => { - if (!string.IsNullOrEmpty(e.Data)) { - logger.Information($"ProcessRunner >>> [thread-{threadId}] {e.Data}"); - output += $"{e.Data}\n"; - } - }; - } + + proc.OutputDataReceived += (o, e) => { + if (e.Data == null) { + stdoutDone.Set(); + return; + } + if (DebugSwitch) { + logger.Information($"ProcessRunner >>> [thread-{threadId}] {e.Data}"); + } + lock (outputLock) { + output.AppendLine(e.Data); + } + }; proc.ErrorDataReceived += (o, e) => { - if (!string.IsNullOrEmpty(e.Data)) { - logger.Error($"ProcessRunner >>> [thread-{threadId}] {e.Data}"); - output += $"{e.Data}\n"; + if (e.Data == null) { + stderrDone.Set(); + return; + } + logger.Error($"ProcessRunner >>> [thread-{threadId}] {e.Data}"); + lock (outputLock) { + output.AppendLine(e.Data); } }; + proc.Start(); - if (DebugSwitch) { - proc.BeginOutputReadLine(); - } + proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); + + bool exited; if (timeoutMs <= 0) { proc.WaitForExit(); + exited = true; } else { - if (proc.WaitForExit(timeoutMs)) { - output += $"Exit code {proc.ExitCode}"; - return output; - } + exited = proc.WaitForExit(timeoutMs); + } + + if (!exited) { logger.Warning($"ProcessRunner >>> [thread-{threadId}] Timeout, killing..."); try { - proc.Kill(); + proc.Kill(entireProcessTree: true); logger.Warning($"ProcessRunner >>> [thread-{threadId}] Killed."); - output += "Killed due to timeout."; } catch (Exception e) { logger.Error(e, $"ProcessRunner >>> [thread-{threadId}] Failed to kill"); } } + + try { + proc.WaitForExit(); + } catch { /* process already disposed/exited */ } + + stdoutDone.Wait(TimeSpan.FromSeconds(5)); + stderrDone.Wait(TimeSpan.FromSeconds(5)); + + lock (outputLock) { + if (!exited) { + output.AppendLine("Killed due to timeout."); + } else { + output.Append("Exit code ").Append(proc.ExitCode); + } + return output.ToString(); + } } - return output; } } } diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index 0be61d251..cee5f11f0 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -189,6 +189,7 @@ Do you want to continue by splitting at the nearest position after current playh Try installing the latest Visual C++ Redistributable. https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170 Character not allowed in regular expression - regular expression error - + Resampler failed to create output file(s). Resampler errors like this are often caused by oto.ini configuration issues. Consider generating a singer error report to check for such issues. Installing "{0}"... To use exe resamplers or wavtools on Linux: diff --git a/OpenUtau/Strings/Strings.de-DE.axaml b/OpenUtau/Strings/Strings.de-DE.axaml index 3910a43c0..71aa73200 100644 --- a/OpenUtau/Strings/Strings.de-DE.axaml +++ b/OpenUtau/Strings/Strings.de-DE.axaml @@ -100,6 +100,7 @@ + Der Resampler konnte keine Ausgabedatei(en) erzeugen. Resampler-Fehler wie dieser werden oft von oto.ini-Konfigurationsfehlern ausgelöst. Generieren Sie einen Sänger-Fehlerbericht, um derartige Fehler zu finden.