Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // sbx4out - switch Sound Blaster X4 output between speakers and headphones.
- //
- // Reuses Creative's own CTCDC.dll (which handles the serial port + firmware
- // unlock) and sends the same raw "5A" passthrough packets Creative.App sends:
- // get current : 5A 2C 01 01
- // get support : 5A 2C 01 02
- // set target : 5A 2C 05 00 <uint32 LE mask> (Lineout=1, Headphone=4, Toggle=0xFF000000)
- //
- // Must be built x86 (CTCDC.dll is 32-bit). CTCDC instances in different processes
- // hand the COM port over via a global mutex: Creative.App releases it to us and
- // does not see our changes. Afterwards we post a fake WM_POWERBROADCAST resume to
- // Creative.App's hidden CTCDC window, which makes it reconnect and call
- // RefreshAllFeatures() so its UI/tray matches the device again.
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Runtime.InteropServices;
- using System.Threading;
- static class Program
- {
- const ushort VID = 0x041E, PID_X4 = 0x3278;
- const byte CMD_OUTPUT_TARGET = 0x2C;
- const uint CMD_READ_PASSTHROUGH = 1000, CMD_WRITE_PASSTHROUGH = 1001;
- const uint NOTIFY_PASSTHROUGH = 0xFFFFFFFE;
- const uint MASK_LINEOUT = 1, MASK_HEADPHONE = 4, MASK_TOGGLE = 0xFF000000;
- static readonly Guid CLSID_CTCDC = new Guid("66FC4CF0-56C8-4523-A92B-CE69FCD7556A");
- static readonly Guid IID_ICTCDC = new Guid("669E9C0E-AD66-48C3-8228-29A55C2E9977");
- static readonly Guid IID_IClassFactory = new Guid("00000001-0000-0000-C000-000000000046");
- static bool verbose;
- static readonly List<byte[]> notified = new List<byte[]>();
- static NotifyProc keepAlive;
- static int Main(string[] args)
- {
- verbose = args.Contains("-v");
- string cmd = args.FirstOrDefault(a => !a.StartsWith("-")) ?? "get";
- bool resync = !args.Contains("--no-resync");
- if (cmd == "resync")
- {
- Console.WriteLine("notified " + ResyncCreativeApp() + " Creative.App window(s)");
- return 0;
- }
- string dir = Environment.GetEnvironmentVariable("CREATIVE_APP_DIR")
- ?? @"C:\Program Files (x86)\Creative\Creative App";
- try
- {
- ICTCDC dev = OpenDevice(dir);
- try { return Run(dev, cmd); }
- finally
- {
- dev.Close(0);
- dev.Shutdown(0);
- if (resync) Log("resync notified " + ResyncCreativeApp() + " window(s)");
- }
- }
- catch (Exception e)
- {
- Console.Error.WriteLine("error: " + e.Message);
- return 1;
- }
- }
- static int Run(ICTCDC dev, string cmd)
- {
- uint mask;
- switch (cmd)
- {
- case "get":
- Console.WriteLine(Describe(GetTarget(dev)));
- return 0;
- case "supported":
- Console.WriteLine(string.Join(", ", GetSupported(dev).Select(Describe)));
- return 0;
- case "speakers":
- mask = SpeakerMask(dev);
- break;
- case "headphones":
- mask = MASK_HEADPHONE;
- break;
- case "toggle":
- mask = (GetTarget(dev) & MASK_HEADPHONE) != 0 ? SpeakerMask(dev) : MASK_HEADPHONE;
- break;
- case "toggle-native":
- SetTarget(dev, MASK_TOGGLE);
- Thread.Sleep(150);
- Console.WriteLine(Describe(GetTarget(dev)));
- return 0;
- default:
- Console.Error.WriteLine("usage: sbx4out [get|supported|speakers|headphones|toggle|toggle-native|resync] [-v] [--no-resync]");
- return 2;
- }
- uint now = SwitchTo(dev, mask);
- Console.WriteLine(Describe(now));
- if (now == mask) return 0;
- Console.Error.WriteLine("error: device did not switch to " + Describe(mask));
- return 3;
- }
- // Sets the target and polls until the device reports it. The X4 echoes a new target
- // immediately but keeps reporting the old one until the switch has physically happened:
- // ~0.5s for line out, or until ~5s after the last switch to headphones (it holds
- // headphones that long). A line-out request sent while a headphone switch is still
- // reconfiguring is dropped outright, so re-send it until it sticks. Returning early
- // would print a stale state and make the next toggle go the wrong way.
- static uint SwitchTo(ICTCDC dev, uint mask)
- {
- DateTime start = DateTime.UtcNow, sent = start;
- SetTarget(dev, mask);
- uint now;
- while ((now = GetTarget(dev)) != mask && DateTime.UtcNow - start < TimeSpan.FromSeconds(8))
- {
- if (DateTime.UtcNow - sent >= TimeSpan.FromSeconds(1))
- {
- Log("not switched yet, re-sending");
- SetTarget(dev, mask);
- sent = DateTime.UtcNow;
- }
- Thread.Sleep(50);
- }
- Log("settled after " + (int)(DateTime.UtcNow - start).TotalMilliseconds + " ms");
- return now;
- }
- // ---- protocol ----
- static uint GetTarget(ICTCDC dev)
- {
- byte[] res = Transact(dev, new byte[] { 0x5A, CMD_OUTPUT_TARGET, 0x01, 0x01 }, 0x01);
- return BitConverter.ToUInt32(res, 4); // 5A 2C len op <u32>
- }
- static uint[] GetSupported(ICTCDC dev)
- {
- byte[] res = Transact(dev, new byte[] { 0x5A, CMD_OUTPUT_TARGET, 0x01, 0x02 }, 0x02);
- int count = res[2] >= 2 ? (res[2] - 2) / 4 : 0; // 5A 2C len op last/num <u32...>
- return Enumerable.Range(0, count).Select(i => BitConverter.ToUInt32(res, 5 + i * 4)).ToArray();
- }
- static uint SpeakerMask(ICTCDC dev)
- {
- try
- {
- uint[] s = GetSupported(dev);
- foreach (uint m in s) if ((m & MASK_HEADPHONE) == 0 && m != 0) return m;
- }
- catch (Exception e) { Log("supported query failed: " + e.Message); }
- return MASK_LINEOUT;
- }
- static void SetTarget(ICTCDC dev, uint mask)
- {
- byte[] pkt = new byte[] { 0x5A, CMD_OUTPUT_TARGET, 0x05, 0x00 }.Concat(BitConverter.GetBytes(mask)).ToArray();
- Write(dev, pkt);
- }
- static string Describe(uint m)
- {
- var names = new List<string>();
- if ((m & 1) != 0) names.Add("lineout");
- if ((m & 2) != 0) names.Add("amp");
- if ((m & 4) != 0) names.Add("headphones");
- if ((m & 8) != 0) names.Add("rca");
- if ((m & 16) != 0) names.Add("bt");
- if ((m & 32) != 0) names.Add("usbhost");
- if ((m & 64) != 0) names.Add("spdif");
- if ((m & 128) != 0) names.Add("headphone-mirror");
- if ((m & 256) != 0) names.Add("lineout-mirror");
- return string.Format("0x{0:X} ({1})", m, names.Count > 0 ? string.Join("+", names) : "none");
- }
- // ---- transport via CTCDC.dll passthrough ----
- static byte[] Transact(ICTCDC dev, byte[] pkt, byte op)
- {
- lock (notified) notified.Clear();
- Write(dev, pkt);
- DateTime deadline = DateTime.UtcNow.AddMilliseconds(1500);
- while (DateTime.UtcNow < deadline)
- {
- byte[] r = TryRead(dev);
- if (r == null) lock (notified) { r = notified.FirstOrDefault(IsMatch(pkt[1], op)); }
- if (r != null && IsMatch(pkt[1], op)(r)) return r;
- if (r != null) Log("ignored: " + Hex(r));
- Thread.Sleep(20);
- }
- throw new TimeoutException("no response to " + Hex(pkt));
- }
- static Func<byte[], bool> IsMatch(byte cmd, byte op)
- {
- return r => r.Length >= 4 && r[0] == 0x5A && r[1] == cmd && r[3] == op;
- }
- static void Write(ICTCDC dev, byte[] pkt)
- {
- Log("tx " + Hex(pkt));
- IntPtr buf = Marshal.AllocHGlobal(pkt.Length);
- IntPtr param = Marshal.AllocHGlobal(8);
- try
- {
- Marshal.Copy(pkt, 0, buf, pkt.Length);
- Marshal.WriteIntPtr(param, 0, buf);
- Marshal.WriteInt32(param, 4, pkt.Length);
- uint r = dev.ExecuteCommand(CMD_WRITE_PASSTHROUGH, param, 0);
- if (r != 0) throw new IOException(string.Format("WritePassthroughData failed: 0x{0:X8}", r));
- }
- finally { Marshal.FreeHGlobal(param); Marshal.FreeHGlobal(buf); }
- }
- static byte[] TryRead(ICTCDC dev)
- {
- const int size = 1024;
- IntPtr buf = Marshal.AllocHGlobal(size);
- IntPtr param = Marshal.AllocHGlobal(12);
- try
- {
- Marshal.WriteIntPtr(param, 0, buf);
- Marshal.WriteInt32(param, 4, size);
- Marshal.WriteInt32(param, 8, 0);
- try { if (dev.ExecuteCommand(CMD_READ_PASSTHROUGH, param, 0) != 0) return null; }
- catch (COMException) { return null; }
- int n = Marshal.ReadInt32(param, 8);
- if (n <= 0) return null;
- byte[] r = new byte[Math.Min(n, size)];
- Marshal.Copy(buf, r, 0, r.Length);
- Log("rx " + Hex(r));
- return r;
- }
- finally { Marshal.FreeHGlobal(param); Marshal.FreeHGlobal(buf); }
- }
- static int OnNotify(uint msg, IntPtr data, IntPtr user)
- {
- if (msg == NOTIFY_PASSTHROUGH && data != IntPtr.Zero)
- {
- IntPtr p = Marshal.ReadIntPtr(data, 0);
- int n = Marshal.ReadInt32(data, 4);
- if (p != IntPtr.Zero && n > 0)
- {
- byte[] r = new byte[n];
- Marshal.Copy(p, r, 0, n);
- Log("notify " + Hex(r));
- lock (notified) notified.Add(r);
- }
- }
- else Log("notify msg=" + msg);
- return 0;
- }
- // ---- Creative.App resync ----
- // Posts WM_POWERBROADCAST/PBT_APMRESUMEAUTOMATIC to every other process's hidden
- // CTCDC window (class "CTCDC_{...}"). Returns the number of windows notified.
- static int ResyncCreativeApp()
- {
- const uint WM_POWERBROADCAST = 0x0218;
- var PBT_APMRESUMEAUTOMATIC = new IntPtr(0x12);
- uint self = (uint)System.Diagnostics.Process.GetCurrentProcess().Id;
- int count = 0;
- EnumWindows((h, l) =>
- {
- var cls = new System.Text.StringBuilder(128);
- GetClassName(h, cls, cls.Capacity);
- uint pid;
- GetWindowThreadProcessId(h, out pid);
- if (pid != self && cls.ToString().StartsWith("CTCDC_{") &&
- PostMessage(h, WM_POWERBROADCAST, PBT_APMRESUMEAUTOMATIC, IntPtr.Zero))
- count++;
- return true;
- }, IntPtr.Zero);
- return count;
- }
- // ---- CTCDC.dll loading (registration-free, via DllGetClassObject) ----
- static ICTCDC OpenDevice(string dir)
- {
- string dll = Path.Combine(dir, "CTCDC.dll");
- if (!File.Exists(dll)) throw new FileNotFoundException("CTCDC.dll not found", dll);
- SetDllDirectory(dir);
- IntPtr h = LoadLibraryEx(dll, IntPtr.Zero, 0x8 /* LOAD_WITH_ALTERED_SEARCH_PATH */);
- if (h == IntPtr.Zero) throw new IOException("LoadLibrary failed: " + Marshal.GetLastWin32Error());
- var getClassObject = (DllGetClassObjectFn)Marshal.GetDelegateForFunctionPointer(
- GetProcAddress(h, "DllGetClassObject"), typeof(DllGetClassObjectFn));
- Guid clsid = CLSID_CTCDC, iidFactory = IID_IClassFactory, iid = IID_ICTCDC;
- object factoryObj;
- Check(getClassObject(ref clsid, ref iidFactory, out factoryObj), "DllGetClassObject");
- object obj;
- Check(((IClassFactory)factoryObj).CreateInstance(null, ref iid, out obj), "CreateInstance");
- var dev = (ICTCDC)obj;
- Check((int)dev.Initialize(1, 0), "Initialize");
- keepAlive = OnNotify;
- uint err = 0xFFFFFFFF;
- uint hr;
- try { hr = dev.Open(VID, PID_X4, null, null, ref err, 0); }
- catch (COMException e) { hr = (uint)e.ErrorCode; }
- if (hr != 0)
- throw new IOException(string.Format(
- "Open failed: hr=0x{0:X8} err={1}. Is Creative.App running? It holds the COM port exclusively.", hr, err));
- dev.RegisterNotificationCallback(keepAlive, IntPtr.Zero, 0);
- Log("device open");
- return dev;
- }
- static void Check(int hr, string what)
- {
- if (hr != 0) throw new IOException(string.Format("{0} failed: 0x{1:X8}", what, hr));
- }
- static void Log(string s) { if (verbose) Console.Error.WriteLine("[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + s); }
- static string Hex(byte[] b) { return BitConverter.ToString(b).Replace("-", " "); }
- [UnmanagedFunctionPointer(CallingConvention.StdCall)]
- delegate int NotifyProc(uint msg, IntPtr data, IntPtr user);
- [UnmanagedFunctionPointer(CallingConvention.StdCall)]
- delegate int DllGetClassObjectFn(ref Guid clsid, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
- [ComImport, Guid("00000001-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- interface IClassFactory
- {
- [PreserveSig] int CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object outer, ref Guid iid,
- [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
- [PreserveSig] int LockServer(bool fLock);
- }
- // Declared exactly like Creative's interop (no PreserveSig): the native methods
- // return HRESULT and take a trailing [out, retval] DWORD that becomes the return value.
- [ComImport, Guid("669E9C0E-AD66-48C3-8228-29A55C2E9977"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- interface ICTCDC
- {
- uint Initialize(uint version, uint flag);
- uint Open(ushort vid, ushort pid, [MarshalAs(UnmanagedType.BStr)] string instance,
- [MarshalAs(UnmanagedType.BStr)] string serial, ref uint err, uint flag);
- uint RegisterNotificationCallback([MarshalAs(UnmanagedType.FunctionPtr)] NotifyProc proc, IntPtr user, uint flag);
- uint ExecuteCommand(uint cmd, IntPtr param, uint flag);
- uint UnregisterNotificationCallback(uint flag);
- uint Close(uint flag);
- uint Shutdown(uint flag);
- }
- delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
- [DllImport("user32")]
- static extern bool EnumWindows(EnumWindowsProc proc, IntPtr l);
- [DllImport("user32", CharSet = CharSet.Unicode)]
- static extern int GetClassName(IntPtr h, System.Text.StringBuilder s, int n);
- [DllImport("user32")]
- static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
- [DllImport("user32")]
- static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l);
- [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
- static extern IntPtr LoadLibraryEx(string path, IntPtr file, uint flags);
- [DllImport("kernel32", CharSet = CharSet.Ansi)]
- static extern IntPtr GetProcAddress(IntPtr h, string name);
- [DllImport("kernel32", CharSet = CharSet.Unicode)]
- static extern bool SetDllDirectory(string dir);
- }
Advertisement
Add Comment
Please, Sign In to add comment