diff --git a/internal/viz/capture.go b/internal/viz/capture.go index d7c2fcd..935e1f3 100644 --- a/internal/viz/capture.go +++ b/internal/viz/capture.go @@ -10,6 +10,7 @@ import ( "log" "math" "math/cmplx" + "runtime" "syscall" "time" "unsafe" @@ -137,7 +138,15 @@ func (c *Capturer) Start(ctx context.Context) { } func (c *Capturer) run(ctx context.Context) error { - coInitializeEx.Call(0, 0) // COINIT_MULTITHREADED + // Pin this goroutine to its OS thread — WASAPI COM objects are + // thread-affine; the scheduler must not migrate us mid-call. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + hr, _, _ := coInitializeEx.Call(0, 0) // COINIT_MULTITHREADED + if hr > 1 { // S_OK=0, S_FALSE=1 are both success + return fmt.Errorf("CoInitializeEx: HRESULT 0x%08X", hr) + } defer coUninitialize.Call() // ── IMMDeviceEnumerator ────────────────────────────────────────────────── diff --git a/internal/volume/volume.go b/internal/volume/volume.go index 9d13a65..d91e279 100644 --- a/internal/volume/volume.go +++ b/internal/volume/volume.go @@ -7,6 +7,7 @@ package volume import ( "fmt" + "runtime" "syscall" "unsafe" @@ -91,13 +92,26 @@ var ( // withEndpointVolume acquires an IAudioEndpointVolume for the default render // device, calls fn with its pointer, and releases all COM objects. +// +// COM apartments are thread-affine on Windows. LockOSThread pins this +// goroutine to its current OS thread for the duration of the call so that +// every COM call runs on the thread that initialized COM. func withEndpointVolume(fn func(vol uintptr) error) error { - coInitializeEx.Call(0, 0) // COINIT_MULTITHREADED + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // CoInitializeEx returns S_OK (0) on first init, S_FALSE (1) if already + // initialized on this thread. Both are success. Anything ≥ 2 is an error + // (HRESULT error codes are 0x8xxxxxxx, always > 1 as unsigned). + hr, _, _ := coInitializeEx.Call(0, 0) // COINIT_MULTITHREADED + if hr > 1 { + return fmt.Errorf("CoInitializeEx: HRESULT 0x%08X", hr) + } defer coUninitialize.Call() // CoCreateInstance(CLSID_MMDeviceEnumerator) → IMMDeviceEnumerator var enumerator uintptr - hr, _, _ := coCreateInstance.Call( + hr, _, _ = coCreateInstance.Call( uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)), 0, clsctxAll,