using System; using System.Drawing; using System.Threading; using System.Windows.Forms; using RemSound.Core; namespace RemSound.App; /// /// A small "Loading audio driver..." window shown on its OWN dedicated UI thread while the /// main thread builds . /// /// Why it exists: opening an ASIO driver is slow — 1-3 seconds on many drivers — and that /// happens synchronously during MainForm construction. Without a splash the user sees /// nothing, or a blank "Not Responding" window shell, for those seconds, and it looks hung. /// /// Why a SEPARATE thread: the ASIO driver must keep being opened on the main UI thread — /// that is the STA, message-pumping thread every ASIO / COM driver is most compatible with, /// and moving the driver open off it risks breaking drivers we cannot test. But the main /// thread is therefore blocked for the whole open, so a splash shown on it would itself be /// frozen. Running the splash on its own STA thread with its own message loop lets it paint /// and stay alive while the main thread is busy. No ASIO or driver code is touched — only /// this cosmetic window runs on the side thread. /// /// Shown only for profiles that actually use an ASIO driver; a WASAPI-only profile builds /// the main window near-instantly and gets no splash (it would just flash). /// internal sealed class AsioLoadingSplash { private readonly Thread thread; private readonly ManualResetEventSlim shown = new(false); private volatile Form? form; private AsioLoadingSplash() { thread = new Thread(RunSplash) { IsBackground = true, Name = "RemSound startup splash", }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); // Wait (briefly, capped) for the window to actually be on screen before the caller // begins the slow ASIO work — so the user sees the splash, not a blank moment. The // cap means a splash hiccup can never stall startup. shown.Wait(TimeSpan.FromSeconds(2)); } /// /// Starts the splash only when has an ASIO driver selected — /// the only case where MainForm construction is slow. Returns null for WASAPI-only /// profiles. Dismiss the returned handle once the main window has been built. /// public static AsioLoadingSplash? StartIfNeeded(Profile? profile) { if (string.IsNullOrWhiteSpace(profile?.AsioDriverName)) return null; try { return new AsioLoadingSplash(); } catch { // The splash is purely cosmetic — if it can't even be created, let the app // start silently rather than have a splash failure block launch. return null; } } private void RunSplash() { try { using var splash = new Form { Text = "RemSound", FormBorderStyle = FormBorderStyle.FixedDialog, StartPosition = FormStartPosition.CenterScreen, ControlBox = false, MinimizeBox = false, MaximizeBox = false, ShowInTaskbar = false, TopMost = true, ClientSize = new Size(380, 96), AccessibleName = "RemSound is starting", }; splash.Controls.Add(new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleCenter, Text = "Loading audio driver, please wait...", AccessibleName = "Loading audio driver, please wait", }); splash.Shown += (_, _) => shown.Set(); form = splash; Application.Run(splash); } catch { // Never let a splash failure escape on its own thread. } finally { // Unblock the constructor even if the window never managed to show. shown.Set(); } } /// /// Closes the splash. Call from the main thread once has been /// constructed. Fire-and-forget — the splash's own background thread tears itself down. /// public void Dismiss() { try { shown.Set(); var f = form; if (f is not null && f.IsHandleCreated && !f.IsDisposed) { f.BeginInvoke(() => { try { f.Close(); } catch { /* already gone */ } }); } } catch { // Cosmetic teardown — never throw into the startup path. } } }