FocusedWolf

Synapse 4: Cancel Razer forcing their sound devices as default on startup

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