|
- //go:build windows
-
- // Package volume controls the Windows system master volume via the
- // Core Audio API (IAudioEndpointVolume COM interface).
- // This is the modern replacement for the deprecated MMSystem mixer API.
- package volume
-
- import (
- "fmt"
- "syscall"
- "unsafe"
-
- "golang.org/x/sys/windows"
- )
-
- // ── COM GUIDs ─────────────────────────────────────────────────────────────────
-
- var (
- clsidMMDeviceEnumerator = windows.GUID{
- Data1: 0xBCDE0395, Data2: 0xE52F, Data3: 0x467C,
- Data4: [8]byte{0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E},
- }
- iidIMMDeviceEnumerator = windows.GUID{
- Data1: 0xA95664D2, Data2: 0x9614, Data3: 0x4F35,
- Data4: [8]byte{0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6},
- }
- iidIAudioEndpointVolume = windows.GUID{
- Data1: 0x5CDF2C82, Data2: 0x841E, Data3: 0x4546,
- Data4: [8]byte{0x97, 0x22, 0x0C, 0xF7, 0x40, 0x78, 0x22, 0x9A},
- }
- )
-
- const (
- eRender uintptr = 0
- eConsole uintptr = 0
- clsctxAll uintptr = 0x17
- )
-
- // ── Helper: call a COM vtable method ─────────────────────────────────────────
- // vtbl is a pointer to the vtable struct (first field of the COM object).
- // idx is the method index (0 = QueryInterface, 1 = AddRef, 2 = Release, …).
-
- func comCall3(vtbl uintptr, idx int, a1, a2, a3 uintptr) (uintptr, error) {
- proc := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
- r, _, _ := syscall.Syscall6(proc, 4, a1, a2, a3, 0, 0, 0)
- if r != 0 {
- return 0, fmt.Errorf("COM call [%d]: HRESULT 0x%08X", idx, r)
- }
- return r, nil
- }
-
- func comCall4(vtbl uintptr, idx int, a1, a2, a3, a4 uintptr) (uintptr, error) {
- proc := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
- r, _, _ := syscall.Syscall6(proc, 5, a1, a2, a3, a4, 0, 0)
- if r != 0 {
- return 0, fmt.Errorf("COM call [%d]: HRESULT 0x%08X", idx, r)
- }
- return r, nil
- }
-
- func comCall5(vtbl uintptr, idx int, a1, a2, a3, a4, a5 uintptr) (uintptr, error) {
- proc := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
- r, _, _ := syscall.Syscall6(proc, 6, a1, a2, a3, a4, a5, 0)
- if r != 0 {
- return 0, fmt.Errorf("COM call [%d]: HRESULT 0x%08X", idx, r)
- }
- return r, nil
- }
-
- // vtblOf returns the vtable pointer for a COM object pointer.
- func vtblOf(p uintptr) uintptr {
- return *(*uintptr)(unsafe.Pointer(p))
- }
-
- // release calls IUnknown::Release (vtable index 2).
- func release(p uintptr) {
- proc := *(*uintptr)(unsafe.Pointer(vtblOf(p) + 2*unsafe.Sizeof(uintptr(0))))
- syscall.Syscall(proc, 1, p, 0, 0)
- }
-
- // ── DLL procedures ────────────────────────────────────────────────────────────
-
- var (
- ole32 = windows.NewLazySystemDLL("ole32.dll")
- coInitializeEx = ole32.NewProc("CoInitializeEx")
- coUninitialize = ole32.NewProc("CoUninitialize")
- coCreateInstance = ole32.NewProc("CoCreateInstance")
- )
-
- // ── withEndpointVolume ────────────────────────────────────────────────────────
-
- // withEndpointVolume acquires an IAudioEndpointVolume for the default render
- // device, calls fn with its pointer, and releases all COM objects.
- func withEndpointVolume(fn func(vol uintptr) error) error {
- coInitializeEx.Call(0, 0) // COINIT_MULTITHREADED
- defer coUninitialize.Call()
-
- // CoCreateInstance(CLSID_MMDeviceEnumerator) → IMMDeviceEnumerator
- var enumerator uintptr
- hr, _, _ := coCreateInstance.Call(
- uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)),
- 0,
- clsctxAll,
- uintptr(unsafe.Pointer(&iidIMMDeviceEnumerator)),
- uintptr(unsafe.Pointer(&enumerator)),
- )
- if hr != 0 {
- return fmt.Errorf("CoCreateInstance: HRESULT 0x%08X", hr)
- }
- defer release(enumerator)
-
- // IMMDeviceEnumerator::GetDefaultAudioEndpoint (vtable index 4)
- // (0=QI, 1=AddRef, 2=Release, 3=EnumAudioEndpoints, 4=GetDefaultAudioEndpoint)
- var device uintptr
- if _, err := comCall5(vtblOf(enumerator), 4,
- enumerator, eRender, eConsole,
- uintptr(unsafe.Pointer(&device)), 0); err != nil {
- return fmt.Errorf("GetDefaultAudioEndpoint: %w", err)
- }
- defer release(device)
-
- // IMMDevice::Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, nil, &vol)
- // (0=QI, 1=AddRef, 2=Release, 3=Activate, …)
- var vol uintptr
- if _, err := comCall5(vtblOf(device), 3,
- device,
- uintptr(unsafe.Pointer(&iidIAudioEndpointVolume)),
- clsctxAll,
- 0,
- uintptr(unsafe.Pointer(&vol))); err != nil {
- return fmt.Errorf("IMMDevice::Activate: %w", err)
- }
- defer release(vol)
-
- return fn(vol)
- }
-
- // ── IAudioEndpointVolume vtable indices ───────────────────────────────────────
- //
- // 0 QueryInterface
- // 1 AddRef
- // 2 Release
- // 3 RegisterControlChangeNotify
- // 4 UnregisterControlChangeNotify
- // 5 GetChannelCount
- // 6 SetMasterVolumeLevel
- // 7 SetMasterVolumeLevelScalar
- // 8 GetMasterVolumeLevel
- // 9 GetMasterVolumeLevelScalar
- // 14 SetMute
- // 15 GetMute
-
- const (
- idxSetMasterScalar = 7
- idxGetMasterScalar = 9
- idxSetMute = 14
- idxGetMute = 15
- )
-
- // ── Public API ────────────────────────────────────────────────────────────────
-
- // Get returns the current Windows master volume as a percentage (0–100).
- func Get() (int, error) {
- var pct int
- err := withEndpointVolume(func(vol uintptr) error {
- var level float32
- proc := *(*uintptr)(unsafe.Pointer(
- vtblOf(vol) + idxGetMasterScalar*unsafe.Sizeof(uintptr(0)),
- ))
- r, _, _ := syscall.Syscall(proc, 2, vol, uintptr(unsafe.Pointer(&level)), 0)
- if r != 0 {
- return fmt.Errorf("GetMasterVolumeLevelScalar: 0x%08X", r)
- }
- pct = int(level * 100)
- return nil
- })
- return pct, err
- }
-
- // Set sets the Windows master volume to pct percent (0–100).
- func Set(pct int) error {
- if pct < 0 {
- pct = 0
- }
- if pct > 100 {
- pct = 100
- }
- level := float32(pct) / 100.0
- return withEndpointVolume(func(vol uintptr) error {
- var nullGUID windows.GUID
- proc := *(*uintptr)(unsafe.Pointer(
- vtblOf(vol) + idxSetMasterScalar*unsafe.Sizeof(uintptr(0)),
- ))
- // float32 must be widened to uintptr via bit-reinterpretation
- bits := *(*uint32)(unsafe.Pointer(&level))
- r, _, _ := syscall.Syscall(proc, 3, vol, uintptr(bits), uintptr(unsafe.Pointer(&nullGUID)))
- if r != 0 {
- return fmt.Errorf("SetMasterVolumeLevelScalar: 0x%08X", r)
- }
- return nil
- })
- }
-
- // GetMute reports whether the system audio is currently muted.
- func GetMute() (bool, error) {
- var muted bool
- err := withEndpointVolume(func(vol uintptr) error {
- var val int32
- proc := *(*uintptr)(unsafe.Pointer(
- vtblOf(vol) + idxGetMute*unsafe.Sizeof(uintptr(0)),
- ))
- r, _, _ := syscall.Syscall(proc, 2, vol, uintptr(unsafe.Pointer(&val)), 0)
- if r != 0 {
- return fmt.Errorf("GetMute: 0x%08X", r)
- }
- muted = val != 0
- return nil
- })
- return muted, err
- }
-
- // SetMute mutes or unmutes the system audio.
- func SetMute(muted bool) error {
- return withEndpointVolume(func(vol uintptr) error {
- var nullGUID windows.GUID
- val := uintptr(0)
- if muted {
- val = 1
- }
- proc := *(*uintptr)(unsafe.Pointer(
- vtblOf(vol) + idxSetMute*unsafe.Sizeof(uintptr(0)),
- ))
- r, _, _ := syscall.Syscall(proc, 3, vol, val, uintptr(unsafe.Pointer(&nullGUID)))
- if r != 0 {
- return fmt.Errorf("SetMute: 0x%08X", r)
- }
- return nil
- })
- }
|