Advertisement
FocusedWolf

Synapse 4: Override Default Audio BS On Startup

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