using RemSound.Core; namespace RemSound.App; /// /// Default cue-sound resolution. The cue WAVs ship as numbered variants in sounds\ - /// "connect 1.wav", "connect 2.wav", ... - and the user picks which one is the default for each /// cue in Preferences (stored machine-wide in ). This /// helper discovers the variants for a cue and resolves which one is the active default. /// /// The full resolution order used wherever a cue is loaded (MainForm, the startup cue, the /// Preferences preview) is: /// 1. the user's per-profile custom WAV (handled by the caller) - highest priority; /// 2. the machine-wide chosen default variant, if it still exists on disk; /// 3. the first available variant (the lowest-numbered - the "1"s) - the shipped default; /// 4. nothing - the cue is silent. /// /// The count of variants is never assumed: whatever "<base> <n>.wav" files are present /// are offered, so adding more sounds later needs no code change. Matching is case-insensitive so /// a stray capital (e.g. "Profile menu open 1.wav") still resolves. /// internal static class CueSounds { /// The variant filenames available for a cue, sorted by their trailing number. The /// base name comes from (the historical single name, e.g. /// "connect.wav" -> base "connect"), matched against "connect.wav" and "connect <n>.wav" /// in sounds\. Returns filenames only (no path); empty when none are present. public static IReadOnlyList Variants(string defaultFileName) { var baseName = Path.GetFileNameWithoutExtension(defaultFileName); var dir = AppConfig.SoundsDirectory; if (string.IsNullOrEmpty(baseName) || !Directory.Exists(dir)) return Array.Empty(); var matches = new List<(int Order, string Name)>(); try { foreach (var full in Directory.EnumerateFiles(dir, "*.wav")) { var name = Path.GetFileName(full); var stem = Path.GetFileNameWithoutExtension(name); if (stem.Equals(baseName, StringComparison.OrdinalIgnoreCase)) { matches.Add((0, name)); // the bare, unnumbered name sorts first } else if (stem.Length > baseName.Length + 1 && stem.StartsWith(baseName + " ", StringComparison.OrdinalIgnoreCase) && int.TryParse(stem[(baseName.Length + 1)..], out var n)) { matches.Add((n, name)); } } } catch { return Array.Empty(); } var ordered = matches.OrderBy(m => m.Order).ToList(); // If numbered variants exist, drop the bare unnumbered file: it labels as "Sound 1" (below), the // same as " 1.wav", so the two would show as indistinguishable "Sound 1" rows a screen-reader // user can't tell apart. The bare name is a legacy single-file fallback; numbered is what ships. if (ordered.Exists(m => m.Order >= 1) && ordered.Exists(m => m.Order == 0)) ordered.RemoveAll(m => m.Order == 0); return ordered.Select(m => m.Name).ToList(); } /// The "Sound N" label for a variant filename, for the Preferences listbox. /// "connect 2.wav" -> "Sound 2"; an unnumbered "connect.wav" -> "Sound 1". public static string VariantLabel(string defaultFileName, string variantFileName) { var baseName = Path.GetFileNameWithoutExtension(defaultFileName); var stem = Path.GetFileNameWithoutExtension(variantFileName); if (stem.Length > baseName.Length + 1 && stem.StartsWith(baseName + " ", StringComparison.OrdinalIgnoreCase) && int.TryParse(stem[(baseName.Length + 1)..], out var n)) { return $"Sound {n}"; } return "Sound 1"; } /// The chosen default variant filename for a cue: the machine-wide pick if it still /// exists among the variants, otherwise the first variant (the "1"). Null when the cue has no /// variants on disk at all. public static string? ResolveDefaultFileName(string cueId, string defaultFileName, AppConfig cfg) { var variants = Variants(defaultFileName); if (variants.Count == 0) return null; if (cfg.DefaultCueSounds.TryGetValue(cueId, out var chosen)) { var match = variants.FirstOrDefault(v => v.Equals(chosen, StringComparison.OrdinalIgnoreCase)); if (match is not null) return match; } return variants[0]; } /// Full path to the chosen default WAV for a cue, or null when none resolves. public static string? ResolveDefaultPath(string cueId, string defaultFileName, AppConfig cfg) { var name = ResolveDefaultFileName(cueId, defaultFileName, cfg); return name is null ? null : Path.Combine(AppConfig.SoundsDirectory, name); } }