Web-based Winamp controller for CarPC � Go backend, mobile-first UI
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

254 linhas
8.1KB

  1. //go:build windows
  2. // Package volume controls the Windows system master volume via the
  3. // Core Audio API (IAudioEndpointVolume COM interface).
  4. // This is the modern replacement for the deprecated MMSystem mixer API.
  5. package volume
  6. import (
  7. "fmt"
  8. "runtime"
  9. "syscall"
  10. "unsafe"
  11. "golang.org/x/sys/windows"
  12. )
  13. // ── COM GUIDs ─────────────────────────────────────────────────────────────────
  14. var (
  15. clsidMMDeviceEnumerator = windows.GUID{
  16. Data1: 0xBCDE0395, Data2: 0xE52F, Data3: 0x467C,
  17. Data4: [8]byte{0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E},
  18. }
  19. iidIMMDeviceEnumerator = windows.GUID{
  20. Data1: 0xA95664D2, Data2: 0x9614, Data3: 0x4F35,
  21. Data4: [8]byte{0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6},
  22. }
  23. iidIAudioEndpointVolume = windows.GUID{
  24. Data1: 0x5CDF2C82, Data2: 0x841E, Data3: 0x4546,
  25. Data4: [8]byte{0x97, 0x22, 0x0C, 0xF7, 0x40, 0x78, 0x22, 0x9A},
  26. }
  27. )
  28. const (
  29. eRender uintptr = 0
  30. eConsole uintptr = 0
  31. clsctxAll uintptr = 0x17
  32. )
  33. // ── Helper: call a COM vtable method ─────────────────────────────────────────
  34. // vtbl is a pointer to the vtable struct (first field of the COM object).
  35. // idx is the method index (0 = QueryInterface, 1 = AddRef, 2 = Release, …).
  36. func comCall3(vtbl uintptr, idx int, a1, a2, a3 uintptr) (uintptr, error) {
  37. proc := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
  38. r, _, _ := syscall.Syscall6(proc, 4, a1, a2, a3, 0, 0, 0)
  39. if r != 0 {
  40. return 0, fmt.Errorf("COM call [%d]: HRESULT 0x%08X", idx, r)
  41. }
  42. return r, nil
  43. }
  44. func comCall4(vtbl uintptr, idx int, a1, a2, a3, a4 uintptr) (uintptr, error) {
  45. proc := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
  46. r, _, _ := syscall.Syscall6(proc, 5, a1, a2, a3, a4, 0, 0)
  47. if r != 0 {
  48. return 0, fmt.Errorf("COM call [%d]: HRESULT 0x%08X", idx, r)
  49. }
  50. return r, nil
  51. }
  52. func comCall5(vtbl uintptr, idx int, a1, a2, a3, a4, a5 uintptr) (uintptr, error) {
  53. proc := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
  54. r, _, _ := syscall.Syscall6(proc, 6, a1, a2, a3, a4, a5, 0)
  55. if r != 0 {
  56. return 0, fmt.Errorf("COM call [%d]: HRESULT 0x%08X", idx, r)
  57. }
  58. return r, nil
  59. }
  60. // vtblOf returns the vtable pointer for a COM object pointer.
  61. func vtblOf(p uintptr) uintptr {
  62. return *(*uintptr)(unsafe.Pointer(p))
  63. }
  64. // release calls IUnknown::Release (vtable index 2).
  65. func release(p uintptr) {
  66. proc := *(*uintptr)(unsafe.Pointer(vtblOf(p) + 2*unsafe.Sizeof(uintptr(0))))
  67. syscall.Syscall(proc, 1, p, 0, 0)
  68. }
  69. // ── DLL procedures ────────────────────────────────────────────────────────────
  70. var (
  71. ole32 = windows.NewLazySystemDLL("ole32.dll")
  72. coInitializeEx = ole32.NewProc("CoInitializeEx")
  73. coUninitialize = ole32.NewProc("CoUninitialize")
  74. coCreateInstance = ole32.NewProc("CoCreateInstance")
  75. )
  76. // ── withEndpointVolume ────────────────────────────────────────────────────────
  77. // withEndpointVolume acquires an IAudioEndpointVolume for the default render
  78. // device, calls fn with its pointer, and releases all COM objects.
  79. //
  80. // COM apartments are thread-affine on Windows. LockOSThread pins this
  81. // goroutine to its current OS thread for the duration of the call so that
  82. // every COM call runs on the thread that initialized COM.
  83. func withEndpointVolume(fn func(vol uintptr) error) error {
  84. runtime.LockOSThread()
  85. defer runtime.UnlockOSThread()
  86. // CoInitializeEx returns S_OK (0) on first init, S_FALSE (1) if already
  87. // initialized on this thread. Both are success. Anything ≥ 2 is an error
  88. // (HRESULT error codes are 0x8xxxxxxx, always > 1 as unsigned).
  89. hr, _, _ := coInitializeEx.Call(0, 0) // COINIT_MULTITHREADED
  90. if hr > 1 {
  91. return fmt.Errorf("CoInitializeEx: HRESULT 0x%08X", hr)
  92. }
  93. defer coUninitialize.Call()
  94. // CoCreateInstance(CLSID_MMDeviceEnumerator) → IMMDeviceEnumerator
  95. var enumerator uintptr
  96. hr, _, _ = coCreateInstance.Call(
  97. uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)),
  98. 0,
  99. clsctxAll,
  100. uintptr(unsafe.Pointer(&iidIMMDeviceEnumerator)),
  101. uintptr(unsafe.Pointer(&enumerator)),
  102. )
  103. if hr != 0 {
  104. return fmt.Errorf("CoCreateInstance: HRESULT 0x%08X", hr)
  105. }
  106. defer release(enumerator)
  107. // IMMDeviceEnumerator::GetDefaultAudioEndpoint (vtable index 4)
  108. // (0=QI, 1=AddRef, 2=Release, 3=EnumAudioEndpoints, 4=GetDefaultAudioEndpoint)
  109. var device uintptr
  110. if _, err := comCall5(vtblOf(enumerator), 4,
  111. enumerator, eRender, eConsole,
  112. uintptr(unsafe.Pointer(&device)), 0); err != nil {
  113. return fmt.Errorf("GetDefaultAudioEndpoint: %w", err)
  114. }
  115. defer release(device)
  116. // IMMDevice::Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, nil, &vol)
  117. // (0=QI, 1=AddRef, 2=Release, 3=Activate, …)
  118. var vol uintptr
  119. if _, err := comCall5(vtblOf(device), 3,
  120. device,
  121. uintptr(unsafe.Pointer(&iidIAudioEndpointVolume)),
  122. clsctxAll,
  123. 0,
  124. uintptr(unsafe.Pointer(&vol))); err != nil {
  125. return fmt.Errorf("IMMDevice::Activate: %w", err)
  126. }
  127. defer release(vol)
  128. return fn(vol)
  129. }
  130. // ── IAudioEndpointVolume vtable indices ───────────────────────────────────────
  131. //
  132. // 0 QueryInterface
  133. // 1 AddRef
  134. // 2 Release
  135. // 3 RegisterControlChangeNotify
  136. // 4 UnregisterControlChangeNotify
  137. // 5 GetChannelCount
  138. // 6 SetMasterVolumeLevel
  139. // 7 SetMasterVolumeLevelScalar
  140. // 8 GetMasterVolumeLevel
  141. // 9 GetMasterVolumeLevelScalar
  142. // 14 SetMute
  143. // 15 GetMute
  144. const (
  145. idxSetMasterScalar = 7
  146. idxGetMasterScalar = 9
  147. idxSetMute = 14
  148. idxGetMute = 15
  149. )
  150. // ── Public API ────────────────────────────────────────────────────────────────
  151. // Get returns the current Windows master volume as a percentage (0–100).
  152. func Get() (int, error) {
  153. var pct int
  154. err := withEndpointVolume(func(vol uintptr) error {
  155. var level float32
  156. proc := *(*uintptr)(unsafe.Pointer(
  157. vtblOf(vol) + idxGetMasterScalar*unsafe.Sizeof(uintptr(0)),
  158. ))
  159. r, _, _ := syscall.Syscall(proc, 2, vol, uintptr(unsafe.Pointer(&level)), 0)
  160. if r != 0 {
  161. return fmt.Errorf("GetMasterVolumeLevelScalar: 0x%08X", r)
  162. }
  163. pct = int(level * 100)
  164. return nil
  165. })
  166. return pct, err
  167. }
  168. // Set sets the Windows master volume to pct percent (0–100).
  169. func Set(pct int) error {
  170. if pct < 0 {
  171. pct = 0
  172. }
  173. if pct > 100 {
  174. pct = 100
  175. }
  176. level := float32(pct) / 100.0
  177. return withEndpointVolume(func(vol uintptr) error {
  178. var nullGUID windows.GUID
  179. proc := *(*uintptr)(unsafe.Pointer(
  180. vtblOf(vol) + idxSetMasterScalar*unsafe.Sizeof(uintptr(0)),
  181. ))
  182. // float32 must be widened to uintptr via bit-reinterpretation
  183. bits := *(*uint32)(unsafe.Pointer(&level))
  184. r, _, _ := syscall.Syscall(proc, 3, vol, uintptr(bits), uintptr(unsafe.Pointer(&nullGUID)))
  185. if r != 0 {
  186. return fmt.Errorf("SetMasterVolumeLevelScalar: 0x%08X", r)
  187. }
  188. return nil
  189. })
  190. }
  191. // GetMute reports whether the system audio is currently muted.
  192. func GetMute() (bool, error) {
  193. var muted bool
  194. err := withEndpointVolume(func(vol uintptr) error {
  195. var val int32
  196. proc := *(*uintptr)(unsafe.Pointer(
  197. vtblOf(vol) + idxGetMute*unsafe.Sizeof(uintptr(0)),
  198. ))
  199. r, _, _ := syscall.Syscall(proc, 2, vol, uintptr(unsafe.Pointer(&val)), 0)
  200. if r != 0 {
  201. return fmt.Errorf("GetMute: 0x%08X", r)
  202. }
  203. muted = val != 0
  204. return nil
  205. })
  206. return muted, err
  207. }
  208. // SetMute mutes or unmutes the system audio.
  209. func SetMute(muted bool) error {
  210. return withEndpointVolume(func(vol uintptr) error {
  211. var nullGUID windows.GUID
  212. val := uintptr(0)
  213. if muted {
  214. val = 1
  215. }
  216. proc := *(*uintptr)(unsafe.Pointer(
  217. vtblOf(vol) + idxSetMute*unsafe.Sizeof(uintptr(0)),
  218. ))
  219. r, _, _ := syscall.Syscall(proc, 3, vol, val, uintptr(unsafe.Pointer(&nullGUID)))
  220. if r != 0 {
  221. return fmt.Errorf("SetMute: 0x%08X", r)
  222. }
  223. return nil
  224. })
  225. }