using System.Runtime; using System.Windows.Forms; using RemSound.Core; namespace RemSound.App; internal static class Program { [STAThread] private static void Main() { // SustainedLowLatency tells the GC to avoid full (gen 2) collections while audio is streaming. // Gen 0/1 collections still happen but are sub-millisecond; the long pauses that were causing // the receiver to fall behind in clusters of 4-5 underruns at a time were almost certainly // gen 2 sweeps. This trades a bit of memory headroom (the GC will hold on to garbage longer) // for dramatically more predictable timing — exactly the trade real-time audio wants. GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; ApplicationConfiguration.Initialize(); // F1 anywhere = open the bundled manual. Installed *before* the first ShowDialog so // it works on the profile picker (the very first thing the user sees). The filter // is per-thread and modifier-aware: bare F1 only, so Shift/Ctrl/Alt+F1 stay free. HelpLauncher.Install(); // Outer loop: lets ProfileManagementDialog change the profiles folder mid-session. // When that happens, MainForm sets ReloadFromScratch=true, we re-read AppConfig, build // a fresh ProfileStore, and re-show ProfileSelectionDialog so the user picks a profile // (or blank template) from the *new* folder. Inner loop handles the cheaper "switch to // a profile in the same folder" case. while (true) { var appConfig = AppConfig.Load(); var store = appConfig.CreateStore(); Profile? profile; string? title; // Auto-load shortcut: if AppConfig.StartWithProfileTitle is set and the named // profile actually exists in the current store, skip the picker entirely and // load that profile directly. This is what the Startup behaviour dialog's // "Start with a specific profile" toggle drives. Combined with the Windows // auto-start registry entry and the StartMinimised flag, it lets the user // boot a machine and have RemSound up and streaming with no clicks. Falls // through to the normal picker if the configured profile no longer exists // (deleted since it was selected, or the profiles folder changed) so the user // isn't stuck. Profile? autoLoaded = null; string? autoLoadedTitle = null; if (!string.IsNullOrWhiteSpace(appConfig.StartWithProfileTitle)) { try { autoLoaded = store.Load(appConfig.StartWithProfileTitle!); if (autoLoaded is not null) autoLoadedTitle = appConfig.StartWithProfileTitle; } catch { /* fall back to picker */ } } if (autoLoaded is not null) { profile = autoLoaded; title = autoLoadedTitle; } else { using var dialog = new ProfileSelectionDialog(store); if (dialog.ShowDialog() != DialogResult.OK) return; // ProfileSelectionDialog can have changed the folder via its Browse button; // if so, it's already saved AppConfig and rebuilt its internal store. Pick up // its post-Browse store reference for the rest of the session. store = dialog.Store; profile = dialog.SelectedProfile; title = dialog.SelectedTitle; } // Switch-profile loop: when the user clicks "Switch to profile" in the Manage // Profiles dialog, the form sets NextProfileTitleToLoad and closes; we re-open // MainForm under the newly chosen profile. Null = user closed the form normally // → exit. ReloadFromScratch = the user changed the profiles FOLDER mid-session, // so we break out of this inner loop and let the outer loop redo the selection // dialog under the new folder. var reloadFromScratch = false; string? nextPath = null; while (true) { using var form = new MainForm(store, profile, title, nextPath); Application.Run(form); if (form.ReloadFromScratch) { reloadFromScratch = true; break; } // Path-based reload (File → Open profile from a path that may be outside // the active store's BaseDirectory) takes precedence — read JSON directly // from that path. Falls back to title-based store.Load when no path is set // (e.g. legacy switch-by-title flows that pre-date the path tracking). nextPath = form.NextProfilePathToLoad; var nextTitle = form.NextProfileTitleToLoad; if (!string.IsNullOrEmpty(nextPath)) { try { var json = File.ReadAllText(nextPath); profile = System.Text.Json.JsonSerializer.Deserialize(json) ?? Profile.NewBlank(); title = !string.IsNullOrEmpty(nextTitle) ? nextTitle : Path.GetFileNameWithoutExtension(nextPath); } catch { // Malformed / unreadable JSON. Fall back to blank template under // whatever title we have, rather than crashing the loop. profile = Profile.NewBlank(); title = !string.IsNullOrEmpty(nextTitle) ? nextTitle : Path.GetFileNameWithoutExtension(nextPath); nextPath = null; } } else if (!string.IsNullOrEmpty(nextTitle)) { title = nextTitle; profile = store.Load(nextTitle) ?? Profile.NewBlank(); } else { return; // form closed normally — exit app } } if (!reloadFromScratch) return; } } }