Initial commit: RemSound v1.0
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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<Profile>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user