104 lines
3.4 KiB
Go
104 lines
3.4 KiB
Go
//go:build windows
|
|
|
|
// Package audio drives Windows audio endpoints via WASAPI (through go-ole /
|
|
// go-wca) — pure Go, no CGO, the same COM stack OmniRig already uses. It
|
|
// powers the Digital Voice Keyer (record/play voice messages to the rig) and
|
|
// the QSO recorder (rolling-buffer capture saved as WAV).
|
|
package audio
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"sort"
|
|
|
|
"github.com/go-ole/go-ole"
|
|
"github.com/moutend/go-wca/pkg/wca"
|
|
)
|
|
|
|
// Device is one audio endpoint (a capture input or a render output).
|
|
type Device struct {
|
|
ID string `json:"id"` // stable WASAPI endpoint id (persisted)
|
|
Name string `json:"name"` // friendly name shown in dropdowns
|
|
Default bool `json:"default"` // is this the system default endpoint
|
|
}
|
|
|
|
// ListInputDevices returns the active capture endpoints — microphones,
|
|
// line-in, and the soundcard input wired to the rig's audio out ("From Radio").
|
|
func ListInputDevices() ([]Device, error) { return listEndpoints(wca.ECapture) }
|
|
|
|
// ListOutputDevices returns the active render endpoints — speakers and the
|
|
// soundcard output wired to the rig's mic/data input ("To Radio").
|
|
func ListOutputDevices() ([]Device, error) { return listEndpoints(wca.ERender) }
|
|
|
|
// listEndpoints enumerates active endpoints for a data-flow direction. COM is
|
|
// thread-affine, so we lock the OS thread and Co(Un)Initialize around the work
|
|
// — this is a one-shot call from a Wails binding, not a long-lived session.
|
|
func listEndpoints(flow uint32) (out []Device, err error) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
if e := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); e != nil {
|
|
// 0x1 = S_FALSE → already initialised on this thread, fine.
|
|
if oe, ok := e.(*ole.OleError); !ok || oe.Code() != 0x00000001 {
|
|
return nil, fmt.Errorf("CoInitializeEx: %w", e)
|
|
}
|
|
}
|
|
defer ole.CoUninitialize()
|
|
|
|
var mmde *wca.IMMDeviceEnumerator
|
|
if e := wca.CoCreateInstance(wca.CLSID_MMDeviceEnumerator, 0, wca.CLSCTX_ALL,
|
|
wca.IID_IMMDeviceEnumerator, &mmde); e != nil {
|
|
return nil, fmt.Errorf("create MMDeviceEnumerator: %w", e)
|
|
}
|
|
defer mmde.Release()
|
|
|
|
// Record the default endpoint id so the UI can flag it.
|
|
var defID string
|
|
var defDev *wca.IMMDevice
|
|
if e := mmde.GetDefaultAudioEndpoint(flow, wca.EConsole, &defDev); e == nil && defDev != nil {
|
|
_ = defDev.GetId(&defID)
|
|
defDev.Release()
|
|
}
|
|
|
|
var coll *wca.IMMDeviceCollection
|
|
if e := mmde.EnumAudioEndpoints(flow, wca.DEVICE_STATE_ACTIVE, &coll); e != nil {
|
|
return nil, fmt.Errorf("enum endpoints: %w", e)
|
|
}
|
|
defer coll.Release()
|
|
|
|
var count uint32
|
|
if e := coll.GetCount(&count); e != nil {
|
|
return nil, fmt.Errorf("count endpoints: %w", e)
|
|
}
|
|
for i := uint32(0); i < count; i++ {
|
|
var dev *wca.IMMDevice
|
|
if coll.Item(i, &dev) != nil || dev == nil {
|
|
continue
|
|
}
|
|
var id string
|
|
_ = dev.GetId(&id)
|
|
name := endpointName(dev, id)
|
|
dev.Release()
|
|
out = append(out, Device{ID: id, Name: name, Default: id != "" && id == defID})
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|
|
|
|
// endpointName reads PKEY_Device_FriendlyName, falling back to the raw id.
|
|
func endpointName(dev *wca.IMMDevice, fallback string) string {
|
|
var ps *wca.IPropertyStore
|
|
if dev.OpenPropertyStore(wca.STGM_READ, &ps) != nil || ps == nil {
|
|
return fallback
|
|
}
|
|
defer ps.Release()
|
|
var pv wca.PROPVARIANT
|
|
if ps.GetValue(&wca.PKEY_Device_FriendlyName, &pv) != nil {
|
|
return fallback
|
|
}
|
|
if s := pv.String(); s != "" {
|
|
return s
|
|
}
|
|
return fallback
|
|
}
|