Just a modern wrapper of the windows core audio api
Find a file
Callial 063b5045b3
Improve device ID handling and prevent invalid pointer access in callbacks
Enhanced logic to treat empty device IDs as wildcard matches for self-set events. Updated notifications to avoid dereferencing potentially invalid pointers, mitigating access violations in specific driver scenarios.
2025-12-17 12:22:13 -07:00
examples Migrate from manual COM initialization to an internal service model. Replace InitCOM() and UninitCOM() with EnsureStarted() and event subscription. Simplify library API and improve encapsulation of COM threading. Update examples and documentation. 2025-12-17 10:11:29 -07:00
internal/winmm Improve device ID handling and prevent invalid pointer access in callbacks 2025-12-17 12:22:13 -07:00
.gitignore Initial scaffolding for Windows Core Audio library in Go. Includes public API definitions, COM initialization support, placeholder implementations, and examples. 2025-12-07 11:42:45 -07:00
caerror.go Add internal CoreAudio service with event-driven COM thread management 2025-12-17 10:12:40 -07:00
com.go Migrate from manual COM initialization to an internal service model. Replace InitCOM() and UninitCOM() with EnsureStarted() and event subscription. Simplify library API and improve encapsulation of COM threading. Update examples and documentation. 2025-12-17 10:11:29 -07:00
coreaudio.go Initial scaffolding for Windows Core Audio library in Go. Includes public API definitions, COM initialization support, placeholder implementations, and examples. 2025-12-07 11:42:45 -07:00
device.go Initial scaffolding for Windows Core Audio library in Go. Includes public API definitions, COM initialization support, placeholder implementations, and examples. 2025-12-07 11:42:45 -07:00
enumerator.go Migrate from manual COM initialization to an internal service model. Replace InitCOM() and UninitCOM() with EnsureStarted() and event subscription. Simplify library API and improve encapsulation of COM threading. Update examples and documentation. 2025-12-17 10:11:29 -07:00
go.mod Add property change events and watcher singleton management. 2025-12-07 12:49:47 -07:00
go.sum Add property change events and watcher singleton management. 2025-12-07 12:49:47 -07:00
LICENSE Initial scaffolding for Windows Core Audio library in Go. Includes public API definitions, COM initialization support, placeholder implementations, and examples. 2025-12-07 11:42:45 -07:00
README.md Migrate from manual COM initialization to an internal service model. Replace InitCOM() and UninitCOM() with EnsureStarted() and event subscription. Simplify library API and improve encapsulation of COM threading. Update examples and documentation. 2025-12-17 10:11:29 -07:00
selfset.go Improve device ID handling and prevent invalid pointer access in callbacks 2025-12-17 12:22:13 -07:00
service.go Add internal CoreAudio service with event-driven COM thread management 2025-12-17 10:12:40 -07:00
types.go Add property change events and watcher singleton management. 2025-12-07 12:49:47 -07:00
watcher.go Migrate from manual COM initialization to an internal service model. Replace InitCOM() and UninitCOM() with EnsureStarted() and event subscription. Simplify library API and improve encapsulation of COM threading. Update examples and documentation. 2025-12-17 10:11:29 -07:00

coreaudio (Windows; Forgejo hosted)

Windows Core Audio device management in Go with minimal COM interop.

Repository/module path: git.callial.com/Callial/coreaudio

Status: Pre-release (breaking changes expected). Phase 1 usable. Device enumeration, default get/set, and event subscription for device/default changes are implemented for Windows.

Current capabilities

  • Enumerate audio endpoint devices (Render/Capture), including ID, friendly Name, Flow, and State with default flags
  • Get the current default device for Console or Communications roles
  • Set the default device for a role (via the undocumented IPolicyConfig interface)
  • Subscribe to device events via IMMNotificationClient:
    • Default-device changes (Render/Capture; Console/Multimedia/Communications)
    • Device added/removed
    • Device state changes
    • Property changes (key exposed)

Out of scope for now: per-device volume, per-session control.

Requirements

  • Windows 10/11
  • Go 1.25+

Install

go get git.callial.com/Callial/coreaudio

Quick start

All Windows-specific files use //go:build windows. Build on Windows or with the -tags windows flag.

Service model (library owns COM)

Core Audio is COM-based, but this library now owns COM for you. All Core Audio work is performed on a single dedicated OS thread inside the package. Callers must not initialize COM or pin threads.

Quick start:

package main

import (
    "fmt"
    "log"
    ca "git.callial.com/Callial/coreaudio"
)

func main() {
    // Start the internal service (idempotent). Optional but recommended at app init.
    if err := ca.EnsureStarted(ca.Options{IgnoreSelfSets: true}); err != nil { log.Fatal(err) }

    // Enumerate active render devices
    devs, err := ca.ListDevices(ca.Render, ca.Active)
    if err != nil { log.Fatal(err) }
    fmt.Println("Devices:", devs)
}

Core Audio COM threading and lifetime laws

These rules are mandatory for correctness and stability when using Windows Core Audio (COM):

  • Every OS thread that touches COM must call CoInitializeEx first, and must call CoUninitialize exactly once when done.
  • Pick an apartment model and stick to it per thread. Do not mix STA/MTA on the same thread or re-initialize differently later.
  • A COM interface pointer is owned by the OS thread/apartment that obtained it. Treat it as thread-affine unless you explicitly marshal it.
  • Never use a COM pointer from a goroutine that might run on a different OS thread. In Go, assume goroutines can hop threads unless you prevent it.
  • The safest architecture: one dedicated COM thread for all audio work.
    • Start a goroutine.
    • runtime.LockOSThread().
    • CoInitializeEx(...) (use coreaudio.InitCOM() / coreaudio.UninitCOM() here).
    • Create/hold/use/release all Core Audio objects on that thread.
    • Expose a channel/RPC API for other goroutines to request work.
  • Callbacks/notifications must not escape onto random goroutines and then call COM. If you receive a device-change callback, funnel it back into the same COM thread before touching COM again.
  • Release is not optional. For every interface you acquire: exactly one Release() on the same COM thread, including on error paths.
  • Dont cache COM pointers across “world changes” unless you refresh safely. Default-device changes, device removal, and policy changes can invalidate assumptions. Prefer re-resolving devices by ID when acting.
  • No COM in finalizers for correctness. Finalizers run on arbitrary goroutines/threads. Only use them as a last-ditch leak guard, never as primary cleanup.
  • Never let the UI thread call Core Audio directly. UI → send request to COM thread → COM thread does work → send back result.

Examples

See examples/:

  • list_devices: shows enumeration and default lookup.
  • watch_defaults: prints default change events as they happen.

Run on Windows:

go run ./examples/list_devices
go run ./examples/watch_defaults

Public API

// Device listing
func ListDevices(flow DataFlow, stateMask DeviceState) ([]DeviceInfo, error)

// Default device
func GetDefaultDevice(flow DataFlow, role Role) (*DeviceInfo, error)
func SetDefaultDevice(deviceID string, role Role) error

// Events (subscription)
// Start the internal Core Audio service (idempotent).
func EnsureStarted(opts Options) error
// Subscribe to device/default change events. Returns the channel and a cancel function.
func Subscribe(buf int) (<-chan Event, func())
// Close the internal service (optional; idempotent).
func Close() error

Types and enums are in types.go; devices are described by DeviceInfo.

Event subscription

  • Call EnsureStarted once at startup to configure Options like IgnoreSelfSets and the self-suppression window.
  • Use Subscribe(buf) to receive events. It returns a buffered channel and a cancel function; call cancel() to unsubscribe (this closes the channel). Multiple independent subscribers are supported. Self-triggered default-change events (those caused by SetDefaultDevice) can be suppressed by enabling IgnoreSelfSets via EnsureStarted.

Implementation notes (Phase 1)

  • Minimal COM wrappers live under internal/winmm/ (IMMDeviceEnumerator, IMMDevice, IPropertyStore)
  • Default get/set uses IMMDeviceEnumerator.GetDefaultAudioEndpoint and IPolicyConfig.SetDefaultEndpoint
  • An internal service owns a dedicated COM thread, registers an IMMNotificationClient callback, and forwards events to subscribers via a non-blocking event bus.

Next steps

  • Expand event coverage and metadata (e.g., surface parent IDs on add/remove, expose richer property-change metadata)
  • Improve HRESULT-to-error translation with richer context
  • Add integration tests on a Windows CI runner and harden edge cases

License

MIT-style license. See LICENSE for full text. Copyright (c) 2025 Callial.