FocusedWolf

Synapse 4: Override Default Audio BS On Startup

Nov 4th, 2024 (edited)
316
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 28.23 KB | None | 0 0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Runtime.CompilerServices;
  7. using System.ServiceProcess;
  8. using System.Text;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using AudioSwitcher.AudioApi.CoreAudio;
  12. // NuGet dependencies:
  13. // AudioSwitcher.AudioApi              <-- [x] Include Prerelease > 4.0.0-alpha5.
  14. // AudioSwitcher.AudioApi.CoreAudio    <-- [x] Include Prerelease > 4.0.0-alpha5.
  15.  
  16. // Version 25
  17.  
  18. // POSTED ONLINE: https://pastebin.com/NEbxVfnQ
  19.  
  20. // Razer.com post where i shared this code: https://insider.razer.com/razer-synapse-4-55/synapse-4-keeps-changing-default-audio-device-68915?postid=235801#post235801
  21. //
  22. // 11/14/2024 - Did Synapse 4 break? It no longer can set default playback devices for me even when i click the "SET AS DEFAULT" buttons in the GUI under "SOUND" and "MIC".
  23. //              Added a configurable abort timer to handle these situations instead of waiting forever.
  24. //
  25. // 7/16/2025 - Implemented a wait for Windows audio services to start to see if that fixes the issue where this line [CoreAudioController coreAudioController = new CoreAudioController();] was causing this program to hang and leak memory.
  26. //
  27. // 7/25/2025 - Tested with the raw code of CoreAudio projects and noticed less issues. Now testing with pre-release alpha build NuGets.
  28.  
  29. namespace Sound
  30. {
  31.     class Program
  32.     {
  33.         // Usage: $ Sound.exe [option[="value"]] ...
  34.         //
  35.         //     goodplayback - The desired playback device, e.g. 'CABLE Input (VB-Audio Virtual Cable)'.
  36.         //     goodrecording - The desired recording device, e.g. 'Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)'.
  37.         //     badplayback - The undesired playback device, e.g. 'Speakers (Razer Audio Controller - Game)'.
  38.         //     badrecording - The undesired recording device, e.g. 'Headset Microphone (Razer Audio Controller - Chat)'.
  39.         //     badprocess - The undesired process that changes sound settings, i.e. 'RazerAppEngine'.
  40.         //     recheck - The number of checks to perform to ensure sound devices are configured properly. The default value is 5.
  41.         //     delay - The delay used to reduce CPU usage spikes when performing repetitive tasks. The default value is 200.
  42.         //     abort - The delay before giving up waiting for bad processes to alter sound devices. The default value is 30.
  43.         //     devices - List playback and recording devices. The default value is False.
  44.         //     nopause - Prevent this program from pausing before exit. The default value is False.
  45.         //
  46.         // Example: $ Sound.exe ^
  47.         //                goodplayback="CABLE Input (VB-Audio Virtual Cable)" ^
  48.         //                goodrecording="Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)" ^
  49.         //                badplayback="Speakers (Razer Audio Controller - Game)" ^
  50.         //                badrecording="Headset Microphone (Razer Audio Controller - Chat)" ^
  51.         //                badprocess="RazerAppEngine" ^
  52.         //                recheck="5" ^
  53.         //                delay="200" ^
  54.         //                abort="30" ^
  55.         //                nopause
  56.         //
  57.         // Note: Use [$ Sound.exe devices] to get the device names to use with the arguments.
  58.         // Note: If your device lacks a microphone, then don't use [goodrecording="..."] and [badrecording="..."] options.
  59.  
  60.         // How i use this program:
  61.         //     I have a Start.bat file that runs on startup with $ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v "Start" /t REG_SZ /d \"%~dp0Start.bat\" /f
  62.         //
  63.         //     And one of its lines looks like this:
  64.         //
  65.         //     "Sound\Sound\bin\Debug\Sound.exe" ^
  66.         //         goodplayback="CABLE Input (VB-Audio Virtual Cable)" ^
  67.         //         goodrecording="Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)" ^
  68.         //         badplayback="Speakers (Razer Audio Controller - Game)" ^
  69.         //         badrecording="Headset Microphone (Razer Audio Controller - Chat)" ^
  70.         //         badprocess="RazerAppEngine" ^
  71.         //         recheck="4" ^
  72.         //         delay="200" ^
  73.         //         abort="10" ^
  74.         //         nopause
  75.  
  76.         static async Task Main(string[] args)
  77.         {
  78.             Arguments arguments = new Arguments(args);
  79.  
  80.             #region Wait for Windows audio services to start
  81.  
  82.             {
  83.                 Output.WriteLine($" Waiting for Windows audio services to start . . .");
  84.  
  85.                 CancellationTokenSource cts = (arguments.Abort > 0) ? new CancellationTokenSource(TimeSpan.FromSeconds(arguments.Abort)) : new CancellationTokenSource(); // Time delay abort or no time delay abort.
  86.  
  87.                 try
  88.                 {
  89.                     while (true)
  90.                     {
  91.                         if (Services.IsRunning("AudioEndpointBuilder") && Services.IsRunning("Audiosrv"))
  92.                             break;
  93.  
  94.                         await Task.Delay(arguments.Delay, cts.Token).ConfigureAwait(false);
  95.                     }
  96.                 }
  97.                 catch (OperationCanceledException)
  98.                 {
  99.                     Output.WriteLine();
  100.                     Output.WriteLine($" Stopped waiting after {arguments.Abort} seconds . . .");
  101.                 }
  102.                 finally
  103.                 {
  104.                     cts.Dispose();
  105.                 }
  106.             }
  107.  
  108.             #endregion Wait for Windows audio services to start
  109.  
  110.             #region Wait for Synapse to start and begin changing sound settings
  111.  
  112.             if (string.IsNullOrWhiteSpace(arguments.BadProcess) == false &&
  113.                 (arguments.GoodPlaybackDevice != null || arguments.GoodRecordingDevice != null))
  114.             {
  115.                 Output.WriteLine();
  116.                 Output.WriteLine($" Waiting for {arguments.BadProcess} to start . . .");
  117.  
  118.                 CancellationTokenSource cts = (arguments.Abort > 0) ? new CancellationTokenSource(TimeSpan.FromSeconds(arguments.Abort)) : new CancellationTokenSource(); // Time delay abort or no time delay abort.
  119.  
  120.                 try
  121.                 {
  122.                     while (Process.GetProcessesByName(arguments.BadProcess).Any() == false)
  123.                         await Task.Delay(arguments.Delay, cts.Token).ConfigureAwait(false);
  124.  
  125.                     if (arguments.BadPlaybackDevice != null || arguments.BadRecordingDevice != null)
  126.                     {
  127.                         Output.WriteLine();
  128.                         Output.WriteLine(" Waiting for bad sound devices to be set as default . . .");
  129.                     }
  130.  
  131.                     while (true)
  132.                     {
  133.                         CoreAudioController coreAudioController = new CoreAudioController(); // CoreAudioController calls RefreshSystemDevices() in its constructor with no other way to refresh devices.
  134.                         var defaultPlaybackDevice = coreAudioController.DefaultPlaybackDevice;
  135.                         var defaultRecordingDevice = coreAudioController.DefaultCaptureDevice;
  136.  
  137.                         bool isBadPlaybackDeviceSet = arguments.BadPlaybackDevice == null ||
  138.                             (defaultPlaybackDevice != null && defaultPlaybackDevice.FullName.Equals(arguments.BadPlaybackDevice, StringComparison.InvariantCultureIgnoreCase));
  139.  
  140.                         bool isBadRecordingDeviceSet = arguments.BadRecordingDevice == null ||
  141.                             (defaultRecordingDevice != null && defaultRecordingDevice.FullName.Equals(arguments.BadRecordingDevice, StringComparison.InvariantCultureIgnoreCase));
  142.  
  143.                         if ((isBadPlaybackDeviceSet && isBadRecordingDeviceSet) ||
  144.                             (arguments.GoodPlaybackDevice == null && isBadRecordingDeviceSet) ||
  145.                             (isBadPlaybackDeviceSet && arguments.GoodRecordingDevice == null))
  146.                             break;
  147.  
  148.                         await Task.Delay(arguments.Delay, cts.Token).ConfigureAwait(false);
  149.                     }
  150.                 }
  151.                 catch (OperationCanceledException)
  152.                 {
  153.                     Output.WriteLine();
  154.                     Output.WriteLine($" Stopped waiting after {arguments.Abort} seconds . . .");
  155.                 }
  156.                 finally
  157.                 {
  158.                     cts.Dispose();
  159.                 }
  160.             }
  161.  
  162.             #endregion Wait for Synapse to start and begin changing sound settings
  163.  
  164.             #region Set desired sound devices
  165.  
  166.             bool forcePauseExit = false;
  167.  
  168.             if (arguments.GoodPlaybackDevice != null ||
  169.                 arguments.GoodRecordingDevice != null)
  170.             {
  171.                 for (int check = 0, attempt = 0; check < arguments.ReCheck; check++)
  172.                 {
  173.                     try
  174.                     {
  175.                         // TODO: After a recent Razer Synapse update (not 100% sure that's the cause of the problem) i find this program can hang indefinitely leading to a memory leak.
  176.                         //       Attaching a debugger reveals this is the line that causes the hanging issue.
  177.                         //       Testing code that waits for Windows audio services to start to see if that helps.
  178.                         using (CoreAudioController coreAudioController = new CoreAudioController()) // CoreAudioController calls RefreshSystemDevices() in its constructor with no other way to refresh devices.
  179.                         {
  180.                             bool success = true;
  181.  
  182.                             // If default playback device does not match desired playback device.
  183.                             if (arguments.GoodPlaybackDevice != null &&
  184.                                 coreAudioController.DefaultPlaybackDevice.FullName.Equals(arguments.GoodPlaybackDevice, StringComparison.InvariantCultureIgnoreCase) == false)
  185.                             {
  186.                                 check = 0; // Restart loop.
  187.                                 Output.WriteLine();
  188.                                 Output.WriteLine($" ! Unwanted default playback device detected: {coreAudioController.DefaultPlaybackDevice.FullName}");
  189.                                 Output.WriteLine($"   Changing default playback device to: {arguments.GoodPlaybackDevice}");
  190.                                 success &= await coreAudioController.SetDefaultDeviceAsync(arguments.GoodPlaybackDevice).ConfigureAwait(false);
  191.                             }
  192.  
  193.                             // If default recording device does not match desired recording device.
  194.                             if (arguments.GoodRecordingDevice != null &&
  195.                                 coreAudioController.DefaultCaptureDevice.FullName.Equals(arguments.GoodRecordingDevice, StringComparison.InvariantCultureIgnoreCase) == false)
  196.                             {
  197.                                 check = 0; // Restart loop.
  198.                                 Output.WriteLine();
  199.                                 Output.WriteLine($" ! Unwanted default recording device detected: {coreAudioController.DefaultCaptureDevice.FullName}");
  200.                                 Output.WriteLine($"   Changing default recording device to: {arguments.GoodRecordingDevice}");
  201.                                 success &= await coreAudioController.SetDefaultDeviceAsync(arguments.GoodRecordingDevice).ConfigureAwait(false);
  202.                             }
  203.  
  204.                             if (success)
  205.                             {
  206.                                 // If not asked to wait for a "bad" process to alter default sound devices, or to only perform one check.
  207.                                 if (arguments.BadProcess == null ||
  208.                                     arguments.ReCheck == 1)
  209.                                     break;
  210.  
  211.                                 if (check == 0)
  212.                                     Output.WriteLine(); // Add a blank line before write-same-line output.
  213.                                 Output.WriteSameLine($" + Checking default sound devices . . . {(check + 1) / (double)arguments.ReCheck:P0}");
  214.                             }
  215.  
  216.                             else
  217.                             {
  218.                                 check = 0; // Restart loop.
  219.                                 attempt++;
  220.                                 Output.WriteLine();
  221.                                 Output.WriteLine($" ! Retry attempt {attempt} of {MAX_RETRIES} due to device configuration failure.");
  222.  
  223.                                 if (attempt >= MAX_RETRIES)
  224.                                 {
  225.                                     arguments.NoPause.Value = false; // Disable no-pause because of setting-sound-device-as-default error.
  226.                                     break;
  227.                                 }
  228.  
  229.                                 forcePauseExit = true;
  230.                             }
  231.                         }
  232.                     }
  233.                     catch (Exception e)
  234.                     {
  235.                         Logger.TraceMessage(e.ToString());
  236.                         forcePauseExit = true;
  237.                     }
  238.  
  239.                     //await Task.Delay(arguments.Delay).ConfigureAwait(false);
  240.                     //TODO: maybe 200 isn't enough of a delay
  241.                     await Task.Delay(1000).ConfigureAwait(false);
  242.                 }
  243.             }
  244.  
  245.             #endregion Set desired sound devices
  246.  
  247.             if (arguments.Devices)
  248.                 await new CoreAudioController().DisplaySoundDevicesAsync().ConfigureAwait(false); // CoreAudioController calls RefreshSystemDevices() in its constructor with no other way to refresh devices.
  249.  
  250.             if (arguments.NoPause == false || forcePauseExit)
  251.                 Output.Pause();
  252.         }
  253.  
  254.         private const int MAX_RETRIES = 3;
  255.     }
  256.  
  257.     // TODO: testing if an exception is being raised.
  258.     public static class Logger
  259.     {
  260.         public static void TraceMessage(string message,
  261.                                  [CallerMemberName] string memberName = "",
  262.                                  [CallerFilePath] string sourceFilePath = "",
  263.                                  [CallerLineNumber] int sourceLineNumber = 0)
  264.         {
  265.             Output.WriteLine();
  266.             Output.WriteLine("-----");
  267.             Output.WriteLine("message: " + message);
  268.             Output.WriteLine("member name: " + memberName);
  269.             Output.WriteLine("source file path: " + sourceFilePath);
  270.             Output.WriteLine("source line number: " + sourceLineNumber);
  271.             Output.WriteLine("-----");
  272.         }
  273.     }
  274.  
  275.     public static class Services
  276.     {
  277.         public static bool IsRunning(string name)
  278.         {
  279.             if (string.IsNullOrWhiteSpace(name))
  280.                 throw new ArgumentException(nameof(name));
  281.  
  282.             try
  283.             {
  284.                 using (var sc = new ServiceController(name))
  285.                     return (sc.Status == ServiceControllerStatus.Running);
  286.             }
  287.             catch
  288.             {
  289.                 return false;
  290.             }
  291.         }
  292.     }
  293.  
  294.     public static class CoreAudioControllerExtensions
  295.     {
  296.         public static async Task<bool> SetDefaultDeviceAsync(this CoreAudioController coreAudioController, string deviceName)
  297.         {
  298.             if (coreAudioController == null)
  299.                 throw new ArgumentNullException(nameof(coreAudioController));
  300.  
  301.             if (string.IsNullOrEmpty(deviceName))
  302.                 throw new ArgumentNullException(nameof(deviceName));
  303.  
  304.             try
  305.             {
  306.                 var devices = await coreAudioController.GetDevicesAsync().ConfigureAwait(false);
  307.  
  308.                 foreach (CoreAudioDevice device in devices)
  309.                 {
  310.                     if (device.FullName.Equals(deviceName, StringComparison.InvariantCultureIgnoreCase))
  311.                     {
  312.                         bool setAsDefault = await device.SetAsDefaultAsync().ConfigureAwait(false);
  313.                         bool setAsDefaultCommunications = await device.SetAsDefaultCommunicationsAsync().ConfigureAwait(false);
  314.  
  315.                         if (setAsDefault == false ||
  316.                             setAsDefaultCommunications == false)
  317.                         {
  318.                             Output.WriteLine();
  319.                             Output.WriteLine($" Error: Failed to set device '{deviceName}' as default.");
  320.                             return false;
  321.                         }
  322.  
  323.                         return true; // Successfully set the device as default.
  324.                     }
  325.                 }
  326.             }
  327.             catch (Exception ex)
  328.             {
  329.                 Output.WriteLine();
  330.                 Output.WriteLine($" Error: Failed to set device '{deviceName}' as default - {ex.Message}");
  331.                 return false;
  332.             }
  333.  
  334.             Output.WriteLine();
  335.             Output.WriteLine($" Error: Could not find audio device: {deviceName}");
  336.             return false;
  337.         }
  338.  
  339.         public static async Task DisplaySoundDevicesAsync(this CoreAudioController coreAudioController)
  340.         {
  341.             if (coreAudioController == null)
  342.                 throw new ArgumentNullException(nameof(coreAudioController));
  343.  
  344.             CoreAudioDevice defaultPlaybackDevice = coreAudioController.DefaultPlaybackDevice;
  345.             Output.WriteLine();
  346.             Output.WriteLine(" Playback devices:");
  347.  
  348.             foreach (CoreAudioDevice device in await coreAudioController.GetPlaybackDevicesAsync().ConfigureAwait(false))
  349.             {
  350.                 if (device == null)
  351.                     continue;
  352.  
  353.                 string isDefault = (defaultPlaybackDevice != null && device.Id == defaultPlaybackDevice.Id) ? "*" : " ";
  354.                 Output.WriteLine($"   {isDefault} {device.FullName}");
  355.             }
  356.  
  357.             CoreAudioDevice defaultRecordingDevice = coreAudioController.DefaultCaptureDevice;
  358.             Output.WriteLine();
  359.             Output.WriteLine(" Recording devices:");
  360.  
  361.             foreach (CoreAudioDevice device in await coreAudioController.GetCaptureDevicesAsync().ConfigureAwait(false))
  362.             {
  363.                 if (device == null)
  364.                     continue;
  365.  
  366.                 string isDefault = (defaultRecordingDevice != null && device.Id == defaultRecordingDevice.Id) ? "*" : " ";
  367.                 Output.WriteLine($"   {isDefault} {device.FullName}");
  368.             }
  369.         }
  370.     }
  371.  
  372.     public class Arguments
  373.     {
  374.         public Argument<string> GoodPlaybackDevice { get; } = new Argument<string>("goodplayback", "The desired playback device, e.g. 'CABLE Input (VB-Audio Virtual Cable)'.");
  375.         public Argument<string> GoodRecordingDevice { get; } = new Argument<string>("goodrecording", "The desired recording device, e.g. 'Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)'.");
  376.         public Argument<string> BadPlaybackDevice { get; } = new Argument<string>("badplayback", "The undesired playback device, e.g. 'Speakers (Razer Audio Controller - Game)'.");
  377.         public Argument<string> BadRecordingDevice { get; } = new Argument<string>("badrecording", "The undesired recording device, e.g. 'Headset Microphone (Razer Audio Controller - Chat)'.");
  378.         public Argument<string> BadProcess { get; } = new Argument<string>("badprocess", "The undesired process that changes sound settings, i.e. 'RazerAppEngine'.");
  379.         public Argument<int> ReCheck { get; } = new Argument<int>("recheck", "The number of checks to perform to ensure sound devices are configured properly.", 5);
  380.         public Argument<int> Delay { get; } = new Argument<int>("delay", "The delay used to reduce CPU usage spikes when performing repetitive tasks.", 200);
  381.         public Argument<int> Abort { get; } = new Argument<int>("abort", "The delay before giving up waiting for bad processes to alter sound devices.", 30);
  382.         public Argument<bool> Devices { get; } = new Argument<bool>("devices", "List playback and recording devices.");
  383.         public Argument<bool> NoPause { get; } = new Argument<bool>("nopause", "Prevent this program from pausing before exit.");
  384.  
  385.         public Arguments(string[] args)
  386.         {
  387.             // Get all properties of type Argument<T>.
  388.             _arguments = GetType()
  389.                 .GetProperties(BindingFlags.Public | BindingFlags.Instance)
  390.                 .Where(prop => prop.PropertyType.IsGenericType &&
  391.                                prop.PropertyType.GetGenericTypeDefinition() == typeof(Argument<>));
  392.  
  393.             bool success = true;
  394.  
  395.             foreach (string arg in args)
  396.             {
  397.                 bool argParsed = _arguments.Any(property =>
  398.                 {
  399.                     dynamic argument = property.GetValue(this);
  400.                     if (ReferenceEquals(argument, null) == false) // Null check without using the overloaded equality operator in Argument<T>.
  401.                         return argument.TryParse(arg);
  402.                     return false;
  403.                 });
  404.  
  405.                 if (argParsed == false)
  406.                 {
  407.                     success = false;
  408.                     Output.WriteLine($" Error: Unparsed argument detected: {arg}");
  409.                 }
  410.             }
  411.  
  412.             if (args.Length == 0 ||
  413.                 success == false)
  414.             {
  415.                 DisplayUsage();
  416.                 Environment.Exit(1);
  417.             }
  418.         }
  419.  
  420.         public void DisplayUsage()
  421.         {
  422.             string programName = AppDomain.CurrentDomain.FriendlyName;
  423.  
  424.             StringBuilder sb = new StringBuilder();
  425.             sb.AppendLine();
  426.             sb.AppendLine($" Usage: $ {programName} [option[=\"value\"]] ...");
  427.             sb.AppendLine();
  428.  
  429.             foreach (PropertyInfo property in _arguments)
  430.             {
  431.                 dynamic argument = property.GetValue(this);
  432.                 if (ReferenceEquals(argument, null) == false) // Null check without using the overloaded equality operator in Argument<T>.
  433.                 {
  434.                     string defaultValue = argument.DefaultValue != null ? $" The default value is {argument.DefaultValue}." : string.Empty;
  435.                     sb.AppendLine($"     {argument.Name} - {argument.Description}{defaultValue}");
  436.                 }
  437.             }
  438.  
  439.             sb.AppendLine();
  440.             sb.AppendLine($" Example: $ {programName} ^");
  441.             sb.AppendLine($"                {GoodPlaybackDevice.Name}=\"CABLE Input (VB-Audio Virtual Cable)\" ^");
  442.             sb.AppendLine($"                {GoodRecordingDevice.Name}=\"Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)\" ^");
  443.             sb.AppendLine($"                {BadPlaybackDevice.Name}=\"Speakers (Razer Audio Controller - Game)\" ^");
  444.             sb.AppendLine($"                {BadRecordingDevice.Name}=\"Headset Microphone (Razer Audio Controller - Chat)\" ^");
  445.             sb.AppendLine($"                {BadProcess.Name}=\"RazerAppEngine\" ^");
  446.             sb.AppendLine($"                {ReCheck.Name}=\"{ReCheck.DefaultValue}\" ^");
  447.             sb.AppendLine($"                {Delay.Name}=\"{Delay.DefaultValue}\" ^");
  448.             sb.AppendLine($"                {Abort.Name}=\"{Abort.DefaultValue}\" ^");
  449.             sb.AppendLine($"                {NoPause.Name}");
  450.             sb.AppendLine();
  451.             sb.AppendLine($" Note: Use [$ {programName} {Devices.Name}] to get the device names to use with the arguments.");
  452.             sb.Append($" Note: If your device lacks a microphone, then don't use [goodrecording=\"...\"] and [badrecording=\"...\"] options.");
  453.             Output.WriteLine(sb.ToString());
  454.             Output.Pause();
  455.         }
  456.  
  457.         private IEnumerable<PropertyInfo> _arguments;
  458.     }
  459.  
  460.     public class Argument<T>
  461.     {
  462.         public string Name { get; }
  463.         public string Description { get; }
  464.         public T DefaultValue { get; }
  465.         public T Value { get; set; }
  466.  
  467.         public Argument(string name, string description, T defaultValue = default)
  468.         {
  469.             Name = name;
  470.             Description = description;
  471.             DefaultValue = defaultValue;
  472.             Value = defaultValue;
  473.         }
  474.  
  475.         public static implicit operator T(Argument<T> argument) => (argument is null) ? default : argument.Value;
  476.  
  477.         public static bool operator !=(Argument<T> left, Argument<T> right) => !(left == right);
  478.  
  479.         public static bool operator ==(Argument<T> left, Argument<T> right)
  480.         {
  481.             if (ReferenceEquals(left, right))
  482.                 return true;
  483.  
  484.             if ((ReferenceEquals(left.Value, null) && ReferenceEquals(right, null)) ||
  485.                 (ReferenceEquals(left, null)) && ReferenceEquals(right.Value, null))
  486.                 return true;
  487.  
  488.             if (ReferenceEquals(left, null) || ReferenceEquals(right, null))
  489.                 return false;
  490.  
  491.             return EqualityComparer<T>.Default.Equals(left.Value, right.Value);
  492.         }
  493.  
  494.         public override bool Equals(object obj) => obj is Argument<T> other && this == other;
  495.  
  496.         public override int GetHashCode() => Value?.GetHashCode() ?? 0;
  497.  
  498.         public override string ToString() => Value?.ToString() ?? base.ToString();
  499.  
  500.         public bool TryParse(string arg)
  501.         {
  502.             // Split 'name=value' into 'name' and 'value' parts.
  503.             string[] parts = arg.Split(new[] { '=' }, 2);
  504.             string name = parts[0].Trim();
  505.  
  506.             if (name.Equals(Name, StringComparison.InvariantCultureIgnoreCase) == false)
  507.                 return false;
  508.  
  509.             if (parts.Length > 1)
  510.             {
  511.                 string value = parts[1].Trim();
  512.                 try
  513.                 {
  514.                     // Try to convert the value to the expected type.
  515.                     Value = (T)Convert.ChangeType(value, typeof(T));
  516.                     return true;
  517.                 }
  518.                 catch
  519.                 {
  520.                     return false;
  521.                 }
  522.             }
  523.  
  524.             if (typeof(T) == typeof(bool) &&
  525.                 parts.Length == 1)
  526.             {
  527.                 // Treat [name] args as shorthand for "name=true".
  528.                 Value = (T)(object)true;
  529.                 return true;
  530.             }
  531.  
  532.             return false;
  533.         }
  534.     }
  535.  
  536.     public static class Output
  537.     {
  538.         public static void Pause()
  539.         {
  540.             lock (_syncRoot)
  541.             {
  542.                 WriteLine();
  543.                 Write(" Press any key to continue . . . ");
  544.             }
  545.  
  546.             Console.ReadKey(); // Moved outside lock to avoid blocking other threads on input.
  547.         }
  548.  
  549.         public static void WriteSameLine(string value)
  550.         {
  551.             lock (_syncRoot)
  552.             {
  553.                 // Move the cursor home.
  554.                 Console.SetCursorPosition(0, Console.CursorTop);
  555.  
  556.                 // Clear the current line by overwriting it with spaces.
  557.                 Write(new string(' ', Console.WindowWidth));
  558.  
  559.                 // Move the cursor home.
  560.                 Console.SetCursorPosition(0, Console.CursorTop);
  561.  
  562.                 Write(value);
  563.             }
  564.         }
  565.  
  566.         #region WriteLine
  567.  
  568.         public static void WriteLine()
  569.         {
  570.             lock (_syncRoot)
  571.             {
  572.                 EnsureNewLine();
  573.                 Console.WriteLine();
  574.             }
  575.         }
  576.  
  577.         public static void WriteLine(string value)
  578.         {
  579.             lock (_syncRoot)
  580.             {
  581.                 EnsureNewLine();
  582.                 Console.WriteLine(value);
  583.             }
  584.         }
  585.  
  586.         public static void WriteLine(object value)
  587.         {
  588.             lock (_syncRoot)
  589.             {
  590.                 EnsureNewLine();
  591.                 Console.WriteLine(value);
  592.             }
  593.         }
  594.  
  595.         #endregion WriteLine
  596.  
  597.         public static void Write(string value)
  598.         {
  599.             lock (_syncRoot)
  600.             {
  601.                 Console.Write(value);
  602.                 _isSameLine = true;
  603.             }
  604.         }
  605.  
  606.         private static void EnsureNewLine()
  607.         {
  608.             if (_isSameLine)
  609.             {
  610.                 // Add a new line to the console to get under the same-line writing that was last used.
  611.                 Console.WriteLine();
  612.                 _isSameLine = false;
  613.             }
  614.         }
  615.  
  616.         private static bool _isSameLine = false;
  617.         private static readonly object _syncRoot = new object();
  618.     }
  619. }
Advertisement
Add Comment
Please, Sign In to add comment