Guest User

Untitled

a guest
Oct 16th, 2026
36
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 15.02 KB | None | 0 0
  1. // sbx4out - switch Sound Blaster X4 output between speakers and headphones.
  2. //
  3. // Reuses Creative's own CTCDC.dll (which handles the serial port + firmware
  4. // unlock) and sends the same raw "5A" passthrough packets Creative.App sends:
  5. //   get current : 5A 2C 01 01
  6. //   get support : 5A 2C 01 02
  7. //   set target  : 5A 2C 05 00 <uint32 LE mask>   (Lineout=1, Headphone=4, Toggle=0xFF000000)
  8. //
  9. // Must be built x86 (CTCDC.dll is 32-bit). CTCDC instances in different processes
  10. // hand the COM port over via a global mutex: Creative.App releases it to us and
  11. // does not see our changes. Afterwards we post a fake WM_POWERBROADCAST resume to
  12. // Creative.App's hidden CTCDC window, which makes it reconnect and call
  13. // RefreshAllFeatures() so its UI/tray matches the device again.
  14.  
  15. using System;
  16. using System.Collections.Generic;
  17. using System.IO;
  18. using System.Linq;
  19. using System.Runtime.InteropServices;
  20. using System.Threading;
  21.  
  22. static class Program
  23. {
  24.     const ushort VID = 0x041E, PID_X4 = 0x3278;
  25.     const byte CMD_OUTPUT_TARGET = 0x2C;
  26.     const uint CMD_READ_PASSTHROUGH = 1000, CMD_WRITE_PASSTHROUGH = 1001;
  27.     const uint NOTIFY_PASSTHROUGH = 0xFFFFFFFE;
  28.     const uint MASK_LINEOUT = 1, MASK_HEADPHONE = 4, MASK_TOGGLE = 0xFF000000;
  29.  
  30.     static readonly Guid CLSID_CTCDC = new Guid("66FC4CF0-56C8-4523-A92B-CE69FCD7556A");
  31.     static readonly Guid IID_ICTCDC = new Guid("669E9C0E-AD66-48C3-8228-29A55C2E9977");
  32.     static readonly Guid IID_IClassFactory = new Guid("00000001-0000-0000-C000-000000000046");
  33.  
  34.     static bool verbose;
  35.     static readonly List<byte[]> notified = new List<byte[]>();
  36.     static NotifyProc keepAlive;
  37.  
  38.     static int Main(string[] args)
  39.     {
  40.         verbose = args.Contains("-v");
  41.         string cmd = args.FirstOrDefault(a => !a.StartsWith("-")) ?? "get";
  42.         bool resync = !args.Contains("--no-resync");
  43.         if (cmd == "resync")
  44.         {
  45.             Console.WriteLine("notified " + ResyncCreativeApp() + " Creative.App window(s)");
  46.             return 0;
  47.         }
  48.         string dir = Environment.GetEnvironmentVariable("CREATIVE_APP_DIR")
  49.                      ?? @"C:\Program Files (x86)\Creative\Creative App";
  50.         try
  51.         {
  52.             ICTCDC dev = OpenDevice(dir);
  53.             try { return Run(dev, cmd); }
  54.             finally
  55.             {
  56.                 dev.Close(0);
  57.                 dev.Shutdown(0);
  58.                 if (resync) Log("resync notified " + ResyncCreativeApp() + " window(s)");
  59.             }
  60.         }
  61.         catch (Exception e)
  62.         {
  63.             Console.Error.WriteLine("error: " + e.Message);
  64.             return 1;
  65.         }
  66.     }
  67.  
  68.     static int Run(ICTCDC dev, string cmd)
  69.     {
  70.         uint mask;
  71.         switch (cmd)
  72.         {
  73.             case "get":
  74.                 Console.WriteLine(Describe(GetTarget(dev)));
  75.                 return 0;
  76.             case "supported":
  77.                 Console.WriteLine(string.Join(", ", GetSupported(dev).Select(Describe)));
  78.                 return 0;
  79.             case "speakers":
  80.                 mask = SpeakerMask(dev);
  81.                 break;
  82.             case "headphones":
  83.                 mask = MASK_HEADPHONE;
  84.                 break;
  85.             case "toggle":
  86.                 mask = (GetTarget(dev) & MASK_HEADPHONE) != 0 ? SpeakerMask(dev) : MASK_HEADPHONE;
  87.                 break;
  88.             case "toggle-native":
  89.                 SetTarget(dev, MASK_TOGGLE);
  90.                 Thread.Sleep(150);
  91.                 Console.WriteLine(Describe(GetTarget(dev)));
  92.                 return 0;
  93.             default:
  94.                 Console.Error.WriteLine("usage: sbx4out [get|supported|speakers|headphones|toggle|toggle-native|resync] [-v] [--no-resync]");
  95.                 return 2;
  96.         }
  97.         uint now = SwitchTo(dev, mask);
  98.         Console.WriteLine(Describe(now));
  99.         if (now == mask) return 0;
  100.         Console.Error.WriteLine("error: device did not switch to " + Describe(mask));
  101.         return 3;
  102.     }
  103.  
  104.     // Sets the target and polls until the device reports it. The X4 echoes a new target
  105.     // immediately but keeps reporting the old one until the switch has physically happened:
  106.     // ~0.5s for line out, or until ~5s after the last switch to headphones (it holds
  107.     // headphones that long). A line-out request sent while a headphone switch is still
  108.     // reconfiguring is dropped outright, so re-send it until it sticks. Returning early
  109.     // would print a stale state and make the next toggle go the wrong way.
  110.     static uint SwitchTo(ICTCDC dev, uint mask)
  111.     {
  112.         DateTime start = DateTime.UtcNow, sent = start;
  113.         SetTarget(dev, mask);
  114.         uint now;
  115.         while ((now = GetTarget(dev)) != mask && DateTime.UtcNow - start < TimeSpan.FromSeconds(8))
  116.         {
  117.             if (DateTime.UtcNow - sent >= TimeSpan.FromSeconds(1))
  118.             {
  119.                 Log("not switched yet, re-sending");
  120.                 SetTarget(dev, mask);
  121.                 sent = DateTime.UtcNow;
  122.             }
  123.             Thread.Sleep(50);
  124.         }
  125.         Log("settled after " + (int)(DateTime.UtcNow - start).TotalMilliseconds + " ms");
  126.         return now;
  127.     }
  128.  
  129.     // ---- protocol ----
  130.  
  131.     static uint GetTarget(ICTCDC dev)
  132.     {
  133.         byte[] res = Transact(dev, new byte[] { 0x5A, CMD_OUTPUT_TARGET, 0x01, 0x01 }, 0x01);
  134.         return BitConverter.ToUInt32(res, 4);   // 5A 2C len op <u32>
  135.     }
  136.  
  137.     static uint[] GetSupported(ICTCDC dev)
  138.     {
  139.         byte[] res = Transact(dev, new byte[] { 0x5A, CMD_OUTPUT_TARGET, 0x01, 0x02 }, 0x02);
  140.         int count = res[2] >= 2 ? (res[2] - 2) / 4 : 0;   // 5A 2C len op last/num <u32...>
  141.         return Enumerable.Range(0, count).Select(i => BitConverter.ToUInt32(res, 5 + i * 4)).ToArray();
  142.     }
  143.  
  144.     static uint SpeakerMask(ICTCDC dev)
  145.     {
  146.         try
  147.         {
  148.             uint[] s = GetSupported(dev);
  149.             foreach (uint m in s) if ((m & MASK_HEADPHONE) == 0 && m != 0) return m;
  150.         }
  151.         catch (Exception e) { Log("supported query failed: " + e.Message); }
  152.         return MASK_LINEOUT;
  153.     }
  154.  
  155.     static void SetTarget(ICTCDC dev, uint mask)
  156.     {
  157.         byte[] pkt = new byte[] { 0x5A, CMD_OUTPUT_TARGET, 0x05, 0x00 }.Concat(BitConverter.GetBytes(mask)).ToArray();
  158.         Write(dev, pkt);
  159.     }
  160.  
  161.     static string Describe(uint m)
  162.     {
  163.         var names = new List<string>();
  164.         if ((m & 1) != 0) names.Add("lineout");
  165.         if ((m & 2) != 0) names.Add("amp");
  166.         if ((m & 4) != 0) names.Add("headphones");
  167.         if ((m & 8) != 0) names.Add("rca");
  168.         if ((m & 16) != 0) names.Add("bt");
  169.         if ((m & 32) != 0) names.Add("usbhost");
  170.         if ((m & 64) != 0) names.Add("spdif");
  171.         if ((m & 128) != 0) names.Add("headphone-mirror");
  172.         if ((m & 256) != 0) names.Add("lineout-mirror");
  173.         return string.Format("0x{0:X} ({1})", m, names.Count > 0 ? string.Join("+", names) : "none");
  174.     }
  175.  
  176.     // ---- transport via CTCDC.dll passthrough ----
  177.  
  178.     static byte[] Transact(ICTCDC dev, byte[] pkt, byte op)
  179.     {
  180.         lock (notified) notified.Clear();
  181.         Write(dev, pkt);
  182.         DateTime deadline = DateTime.UtcNow.AddMilliseconds(1500);
  183.         while (DateTime.UtcNow < deadline)
  184.         {
  185.             byte[] r = TryRead(dev);
  186.             if (r == null) lock (notified) { r = notified.FirstOrDefault(IsMatch(pkt[1], op)); }
  187.             if (r != null && IsMatch(pkt[1], op)(r)) return r;
  188.             if (r != null) Log("ignored: " + Hex(r));
  189.             Thread.Sleep(20);
  190.         }
  191.         throw new TimeoutException("no response to " + Hex(pkt));
  192.     }
  193.  
  194.     static Func<byte[], bool> IsMatch(byte cmd, byte op)
  195.     {
  196.         return r => r.Length >= 4 && r[0] == 0x5A && r[1] == cmd && r[3] == op;
  197.     }
  198.  
  199.     static void Write(ICTCDC dev, byte[] pkt)
  200.     {
  201.         Log("tx " + Hex(pkt));
  202.         IntPtr buf = Marshal.AllocHGlobal(pkt.Length);
  203.         IntPtr param = Marshal.AllocHGlobal(8);
  204.         try
  205.         {
  206.             Marshal.Copy(pkt, 0, buf, pkt.Length);
  207.             Marshal.WriteIntPtr(param, 0, buf);
  208.             Marshal.WriteInt32(param, 4, pkt.Length);
  209.             uint r = dev.ExecuteCommand(CMD_WRITE_PASSTHROUGH, param, 0);
  210.             if (r != 0) throw new IOException(string.Format("WritePassthroughData failed: 0x{0:X8}", r));
  211.         }
  212.         finally { Marshal.FreeHGlobal(param); Marshal.FreeHGlobal(buf); }
  213.     }
  214.  
  215.     static byte[] TryRead(ICTCDC dev)
  216.     {
  217.         const int size = 1024;
  218.         IntPtr buf = Marshal.AllocHGlobal(size);
  219.         IntPtr param = Marshal.AllocHGlobal(12);
  220.         try
  221.         {
  222.             Marshal.WriteIntPtr(param, 0, buf);
  223.             Marshal.WriteInt32(param, 4, size);
  224.             Marshal.WriteInt32(param, 8, 0);
  225.             try { if (dev.ExecuteCommand(CMD_READ_PASSTHROUGH, param, 0) != 0) return null; }
  226.             catch (COMException) { return null; }
  227.             int n = Marshal.ReadInt32(param, 8);
  228.             if (n <= 0) return null;
  229.             byte[] r = new byte[Math.Min(n, size)];
  230.             Marshal.Copy(buf, r, 0, r.Length);
  231.             Log("rx " + Hex(r));
  232.             return r;
  233.         }
  234.         finally { Marshal.FreeHGlobal(param); Marshal.FreeHGlobal(buf); }
  235.     }
  236.  
  237.     static int OnNotify(uint msg, IntPtr data, IntPtr user)
  238.     {
  239.         if (msg == NOTIFY_PASSTHROUGH && data != IntPtr.Zero)
  240.         {
  241.             IntPtr p = Marshal.ReadIntPtr(data, 0);
  242.             int n = Marshal.ReadInt32(data, 4);
  243.             if (p != IntPtr.Zero && n > 0)
  244.             {
  245.                 byte[] r = new byte[n];
  246.                 Marshal.Copy(p, r, 0, n);
  247.                 Log("notify " + Hex(r));
  248.                 lock (notified) notified.Add(r);
  249.             }
  250.         }
  251.         else Log("notify msg=" + msg);
  252.         return 0;
  253.     }
  254.  
  255.     // ---- Creative.App resync ----
  256.  
  257.     // Posts WM_POWERBROADCAST/PBT_APMRESUMEAUTOMATIC to every other process's hidden
  258.     // CTCDC window (class "CTCDC_{...}"). Returns the number of windows notified.
  259.     static int ResyncCreativeApp()
  260.     {
  261.         const uint WM_POWERBROADCAST = 0x0218;
  262.         var PBT_APMRESUMEAUTOMATIC = new IntPtr(0x12);
  263.         uint self = (uint)System.Diagnostics.Process.GetCurrentProcess().Id;
  264.         int count = 0;
  265.         EnumWindows((h, l) =>
  266.         {
  267.             var cls = new System.Text.StringBuilder(128);
  268.             GetClassName(h, cls, cls.Capacity);
  269.             uint pid;
  270.             GetWindowThreadProcessId(h, out pid);
  271.             if (pid != self && cls.ToString().StartsWith("CTCDC_{") &&
  272.                 PostMessage(h, WM_POWERBROADCAST, PBT_APMRESUMEAUTOMATIC, IntPtr.Zero))
  273.                 count++;
  274.             return true;
  275.         }, IntPtr.Zero);
  276.         return count;
  277.     }
  278.  
  279.     // ---- CTCDC.dll loading (registration-free, via DllGetClassObject) ----
  280.  
  281.     static ICTCDC OpenDevice(string dir)
  282.     {
  283.         string dll = Path.Combine(dir, "CTCDC.dll");
  284.         if (!File.Exists(dll)) throw new FileNotFoundException("CTCDC.dll not found", dll);
  285.         SetDllDirectory(dir);
  286.         IntPtr h = LoadLibraryEx(dll, IntPtr.Zero, 0x8 /* LOAD_WITH_ALTERED_SEARCH_PATH */);
  287.         if (h == IntPtr.Zero) throw new IOException("LoadLibrary failed: " + Marshal.GetLastWin32Error());
  288.         var getClassObject = (DllGetClassObjectFn)Marshal.GetDelegateForFunctionPointer(
  289.             GetProcAddress(h, "DllGetClassObject"), typeof(DllGetClassObjectFn));
  290.  
  291.         Guid clsid = CLSID_CTCDC, iidFactory = IID_IClassFactory, iid = IID_ICTCDC;
  292.         object factoryObj;
  293.         Check(getClassObject(ref clsid, ref iidFactory, out factoryObj), "DllGetClassObject");
  294.         object obj;
  295.         Check(((IClassFactory)factoryObj).CreateInstance(null, ref iid, out obj), "CreateInstance");
  296.         var dev = (ICTCDC)obj;
  297.  
  298.         Check((int)dev.Initialize(1, 0), "Initialize");
  299.         keepAlive = OnNotify;
  300.         uint err = 0xFFFFFFFF;
  301.         uint hr;
  302.         try { hr = dev.Open(VID, PID_X4, null, null, ref err, 0); }
  303.         catch (COMException e) { hr = (uint)e.ErrorCode; }
  304.         if (hr != 0)
  305.             throw new IOException(string.Format(
  306.                 "Open failed: hr=0x{0:X8} err={1}. Is Creative.App running? It holds the COM port exclusively.", hr, err));
  307.         dev.RegisterNotificationCallback(keepAlive, IntPtr.Zero, 0);
  308.         Log("device open");
  309.         return dev;
  310.     }
  311.  
  312.     static void Check(int hr, string what)
  313.     {
  314.         if (hr != 0) throw new IOException(string.Format("{0} failed: 0x{1:X8}", what, hr));
  315.     }
  316.  
  317.     static void Log(string s) { if (verbose) Console.Error.WriteLine("[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + s); }
  318.     static string Hex(byte[] b) { return BitConverter.ToString(b).Replace("-", " "); }
  319.  
  320.     [UnmanagedFunctionPointer(CallingConvention.StdCall)]
  321.     delegate int NotifyProc(uint msg, IntPtr data, IntPtr user);
  322.  
  323.     [UnmanagedFunctionPointer(CallingConvention.StdCall)]
  324.     delegate int DllGetClassObjectFn(ref Guid clsid, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
  325.  
  326.     [ComImport, Guid("00000001-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  327.     interface IClassFactory
  328.     {
  329.         [PreserveSig] int CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object outer, ref Guid iid,
  330.                                          [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
  331.         [PreserveSig] int LockServer(bool fLock);
  332.     }
  333.  
  334.     // Declared exactly like Creative's interop (no PreserveSig): the native methods
  335.     // return HRESULT and take a trailing [out, retval] DWORD that becomes the return value.
  336.     [ComImport, Guid("669E9C0E-AD66-48C3-8228-29A55C2E9977"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  337.     interface ICTCDC
  338.     {
  339.         uint Initialize(uint version, uint flag);
  340.         uint Open(ushort vid, ushort pid, [MarshalAs(UnmanagedType.BStr)] string instance,
  341.                   [MarshalAs(UnmanagedType.BStr)] string serial, ref uint err, uint flag);
  342.         uint RegisterNotificationCallback([MarshalAs(UnmanagedType.FunctionPtr)] NotifyProc proc, IntPtr user, uint flag);
  343.         uint ExecuteCommand(uint cmd, IntPtr param, uint flag);
  344.         uint UnregisterNotificationCallback(uint flag);
  345.         uint Close(uint flag);
  346.         uint Shutdown(uint flag);
  347.     }
  348.  
  349.     delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  350.     [DllImport("user32")]
  351.     static extern bool EnumWindows(EnumWindowsProc proc, IntPtr l);
  352.     [DllImport("user32", CharSet = CharSet.Unicode)]
  353.     static extern int GetClassName(IntPtr h, System.Text.StringBuilder s, int n);
  354.     [DllImport("user32")]
  355.     static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  356.     [DllImport("user32")]
  357.     static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l);
  358.  
  359.     [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
  360.     static extern IntPtr LoadLibraryEx(string path, IntPtr file, uint flags);
  361.     [DllImport("kernel32", CharSet = CharSet.Ansi)]
  362.     static extern IntPtr GetProcAddress(IntPtr h, string name);
  363.     [DllImport("kernel32", CharSet = CharSet.Unicode)]
  364.     static extern bool SetDllDirectory(string dir);
  365. }
  366.  
Advertisement
Add Comment
Please, Sign In to add comment