//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 }) }