FocusedWolf

Synapse 4: Override Default Audio BS On Startup

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