coreaudio 0.2.25

A safe and simple wrapper around the CoreAudio HAL
docs.rs failed to build coreaudio-0.2.25
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

coreaudio

A safe, idiomatic Rust wrapper around the macOS CoreAudio Hardware Abstraction Layer (HAL).

This crate provides typed access to audio devices, streams, and system-level audio objects, with compile-time guarantees around property access permissions and listener support.

Features

  • Type-safe object model — AudioObject<System>, AudioObject<Device>, and AudioObject<Stream> expose only the operations valid for each object type.
  • Compile-time property safety — Properties carry phantom types encoding their value type, owning object, read/write access, and listenability. Attempting to write a read-only property or listen to a non-listenable one is a compile error.
  • Property builder methods — Properties that require an element (channel) or qualifier data expose .for_element(n) and .with_qualifier(value) builder methods. Forgetting to call them is a compile error.
  • Property listeners — Subscribe to property changes with add_listener, then poll with latest(), drain with all_since_last_check(), or block with block_until_change() / block_for_duration().
  • Callback listeners — add_listener_with hands every change straight to a closure on CoreAudio's notification thread, in order, with nothing blocking a thread to wait for it.
  • IO Procs — Register audio render callbacks on devices with add_io_proc and control playback with play() / pause().
  • Channel layouts — Read, write and listen to a device's speaker layout (DEVICE_PREFERRED_CHANNEL_LAYOUT_OUTPUT), expand predefined layouts with layout_for_tag, and get macOS's names for layouts and speakers.
  • Structured error handling — All CoreAudio OSStatus codes are mapped to a typed ErrorKind enum with human-readable four-character-code formatting.
  • Format support — Rich enums for audio format IDs (Linear PCM, AAC variants, ALAC, AC3, Opus, MP3, etc.), format flags, sample formats, transport types, terminal types, and sample resampling utilities.

Requirements

Quick start

use coreaudio::{AudioObject, System, Scope, DEVICE_NAME};

fn main() -> Result<(), coreaudio::CoreAudioError> {
    let system = AudioObject::<System>::default();

    // List all output devices
    let devices = system.devices_with_scope(Scope::Output)?;

    for device in &devices {
        let name: String = device.get_property(DEVICE_NAME)?;
        println!("{}", name);
    }

    Ok(())
}

Usage

Querying device properties

use coreaudio::{
    AudioObject, System, Scope,
    DEVICE_NAME, DEVICE_UID, DEVICE_NOMINAL_SAMPLE_RATE, DEVICE_IS_ALIVE,
};

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;

let name: String = device.get_property(DEVICE_NAME)?;
let uid: String = device.get_property(DEVICE_UID)?;
let sample_rate: f64 = device.get_property(DEVICE_NOMINAL_SAMPLE_RATE)?;
let alive: bool = device.get_property(DEVICE_IS_ALIVE)?;

println!("{name} ({uid}) — {sample_rate} Hz, alive: {alive}");

Setting writable properties

use coreaudio::{AudioObject, System, Scope, DEVICE_NOMINAL_SAMPLE_RATE, DEVICE_BUFFER_FRAME_SIZE};

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;

device.set_property(DEVICE_NOMINAL_SAMPLE_RATE, 48000.0)?;
device.set_property(DEVICE_BUFFER_FRAME_SIZE, 512u32)?;

Listening for property changes

use coreaudio::{AudioObject, System, Scope, DEVICE_NOMINAL_SAMPLE_RATE};
use std::time::Duration;

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;

let listener = device.add_listener(DEVICE_NOMINAL_SAMPLE_RATE)?;

// Non-blocking — returns the most recent change, or None
if let Some(new_rate) = listener.latest() {
    println!("Sample rate changed to {new_rate}");
}

// Blocking with timeout
match listener.block_for_duration(Duration::from_secs(5)) {
    Ok(rate) => println!("Changed to {rate}"),
    Err(e) => println!("Timed out or error: {e}"),
}

Calling a closure on every change

add_listener_with skips the channel: every change is handed straight to a closure, in the order CoreAudio reports it, along with any error reading the new value. The closure runs on CoreAudio's notification thread, so keep it short and hand the value on to wherever the work happens. The returned CallbackListener can be moved between threads, and dropping it unregisters the listener.

use coreaudio::{AudioObject, System, Scope, DEVICE_NOMINAL_SAMPLE_RATE};
use std::sync::mpsc;

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;

let (tx, rx) = mpsc::channel();
let listener = device.add_listener_with(DEVICE_NOMINAL_SAMPLE_RATE, move |rate| {
    // `rate` is `Result<f64, CoreAudioError>`
    let _ = tx.send(rate);
})?;

// ... later
drop(listener);

Properties that require an element or qualifier

Some properties target a specific channel (element) or need a qualifier value before they can be used. Call .for_element(), .with_qualifier(), or both — in either order — to complete the property. Forgetting is a compile error.

Channel names are usually per direction, so read them with DEVICE_INPUT_ELEMENT_NAME or DEVICE_OUTPUT_ELEMENT_NAME; the global OBJECT_ELEMENT_NAME often reads empty on the same device.

use coreaudio::{AudioObject, System, Scope, DEVICE_INPUT_ELEMENT_NAME, MissingElement};

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Input)?;

// e.g. "Channel 1" on an iPhone, "Mic/Line 1" on an audio interface
let name: String = device.get_property(DEVICE_INPUT_ELEMENT_NAME.for_element(1))?;
use coreaudio::{
    AudioObject, System, Scope,
    DEVICE_OUTPUT_VOLUME_SCALAR, DEVICE_OUTPUT_MUTE,
    DEVICE_DATA_SOURCE, DEVICE_DATA_SOURCE_NAME,
    MissingElement, MissingQualifier,
};

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;

// Element-only: read the output volume of channel 1
let volume: f32 = device.get_property(DEVICE_OUTPUT_VOLUME_SCALAR.for_element(1))?;

// Element-only: mute output channel 1
device.set_property(DEVICE_OUTPUT_MUTE.for_element(1), true)?;

// Element-only: read the active data source on channel 1
let source_id: u32 = device.get_property(DEVICE_DATA_SOURCE.for_element(1))?;

// Element + qualifier: look up the name of that source
let source_name: String = device.get_property(
    DEVICE_DATA_SOURCE_NAME
        .for_element(1)
        .with_qualifier(source_id)
)?;
println!("Active source: {source_name}");

Working with streams

use coreaudio::{AudioObject, System, Scope, STREAM_VIRTUAL_FORMAT, STREAM_NAME};

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;
let streams = device.streams_with_scope(Scope::Output)?;

for stream in &streams {
    let name: String = stream.get_property(STREAM_NAME)?;
    let format = stream.get_property(STREAM_VIRTUAL_FORMAT)?;
    println!(
        "{name}: {:?}, {} Hz, {} ch",
        format.format_id(),
        format.sample_rate(),
        format.channels_per_frame(),
    );
}

Registering an IO proc (audio callback)

use coreaudio::{AudioObject, System, Scope};

let system = AudioObject::<System>::default();
let device = system.current_device(Scope::Output)?;

let mut io_proc = device.add_io_proc(|buffers| {
    for buffer in buffers {
        buffer.data.fill(0.0); // silence
    }
})?;

io_proc.play()?;
// ... render audio ...
io_proc.pause()?;
io_proc.remove();

Available buffer sizes and sample rates

use coreaudio::{DEVICE_BUFFER_FRAME_SIZE_RANGE, DEVICE_AVAILABLE_SAMPLE_RATES};

let buffer_range = device.get_property(DEVICE_BUFFER_FRAME_SIZE_RANGE)?;
println!("Valid buffer sizes: {:?}", buffer_range.valid_sizes());

let sample_rates = device.get_property(DEVICE_AVAILABLE_SAMPLE_RATES)?;
println!("Valid sample rates: {:?}", sample_rates.valid_rates());

Property reference

Properties marked element require .for_element(channel) before use.
Properties marked qualifier require .with_qualifier(value) before use.
Properties marked both require both calls (in either order).

Object properties (all object types)

Constant Type Access Listenable Extra
OBJECT_BASE_CLASS u32 Read No —
OBJECT_CLASS u32 Read No —
OBJECT_OWNER u32 Read No —
OBJECT_MODEL_NAME String Read No —
OBJECT_MANUFACTURER String Read No —
OBJECT_CREATOR String Read No —
OBJECT_ELEMENT_NAME String Read No element
OBJECT_ELEMENT_CATEGORY_NAME String Read No element
OBJECT_ELEMENT_NUMBER_NAME String Read No element
OBJECT_OWNED_OBJECTS Vec<u32> Read No qualifier: Vec<u32>

Device properties

Constant Type Access Listenable Extra
DEVICE_NAME String Read Yes —
DEVICE_UID String Read No —
DEVICE_MODEL_UID String Read No —
DEVICE_CONFIGURATION_APPLICATION String Read No —
DEVICE_TRANSPORT_TYPE TransportType Read No —
DEVICE_IS_ALIVE bool Read Yes —
DEVICE_IS_RUNNING bool Read Yes —
DEVICE_IS_HIDDEN bool Read No —
DEVICE_CAN_BE_DEFAULT bool Read No —
DEVICE_CAN_BE_DEFAULT_SYSTEM bool Read No —
DEVICE_NOMINAL_SAMPLE_RATE f64 Read/Write Yes —
DEVICE_AVAILABLE_SAMPLE_RATES Vec<SampleRateRange> Read Yes —
DEVICE_BUFFER_FRAME_SIZE u32 Read/Write Yes —
DEVICE_BUFFER_FRAME_SIZE_RANGE BufferFrameSizeRange Read No —
DEVICE_USES_VARIABLE_BUFFER_FRAME_SIZES u32 Read No —
DEVICE_INPUT_LATENCY u32 Read No —
DEVICE_OUTPUT_LATENCY u32 Read No —
DEVICE_SAFETY_OFFSET u32 Read No —
DEVICE_CLOCK_DOMAIN u32 Read No —
DEVICE_HOG_MODE HogMode Read/Write Yes —
DEVICE_RELATED_DEVICES Vec<u32> Read No —
DEVICE_PREFERRED_CHANNELS_FOR_STEREO ChannelPair Read/Write No —
DEVICE_PREFERRED_CHANNEL_LAYOUT_INPUT ChannelLayout Read/Write Yes —
DEVICE_PREFERRED_CHANNEL_LAYOUT_OUTPUT ChannelLayout Read/Write Yes —
DEVICE_PROCESSOR_OVERLOAD u32 Read Yes —
DEVICE_IO_STOPPED_ABNORMALLY u32 Read Yes —
DEVICE_IO_CYCLE_USAGE f32 Read/Write No —
DEVICE_CLOCK_SOURCE u32 Read/Write Yes —
DEVICE_CLOCK_SOURCES Vec<u32> Read No —
DEVICE_CLOCK_SOURCE_NAME String Read No qualifier: u32
DEVICE_PLAY_THRU_DESTINATION u32 Read/Write Yes —
DEVICE_PLAY_THRU_DESTINATIONS Vec<u32> Read No —
DEVICE_PLAY_THRU_DESTINATION_NAME String Read No qualifier: u32
DEVICE_INPUT_ELEMENT_NAME String Read No element
DEVICE_OUTPUT_ELEMENT_NAME String Read No element
DEVICE_GLOBAL_VOLUME_SCALAR f32 Read/Write Yes element
DEVICE_INPUT_VOLUME_SCALAR f32 Read/Write Yes element
DEVICE_OUTPUT_VOLUME_SCALAR f32 Read/Write Yes element
DEVICE_VOLUME_DECIBELS f32 Read/Write Yes element
DEVICE_VOLUME_RANGE_DECIBELS DBRange Read No element
DEVICE_VOLUME_SCALAR_TO_DECIBELS f32 Read No element
DEVICE_VOLUME_DECIBELS_TO_SCALAR f32 Read No element
DEVICE_SUB_VOLUME_SCALAR f32 Read/Write Yes element
DEVICE_SUB_VOLUME_DECIBELS f32 Read/Write Yes element
DEVICE_SUB_VOLUME_RANGE_DECIBELS DBRange Read No element
DEVICE_SUB_VOLUME_SCALAR_TO_DECIBELS f32 Read No element
DEVICE_SUB_VOLUME_DECIBELS_TO_SCALAR f32 Read No element
DEVICE_STEREO_PAN f32 Read/Write Yes element
DEVICE_STEREO_PAN_CHANNELS ChannelPair Read No element
DEVICE_GLOBAL_MUTE bool Read/Write Yes element
DEVICE_INPUT_MUTE bool Read/Write Yes element
DEVICE_OUTPUT_MUTE bool Read/Write Yes element
DEVICE_SUB_MUTE bool Read/Write Yes element
DEVICE_SOLO bool Read/Write Yes element
DEVICE_PHANTOM_POWER bool Read/Write Yes element
DEVICE_PHASE_INVERT bool Read/Write Yes element
DEVICE_CLIP_LIGHT bool Read/Write Yes element
DEVICE_TALKBACK bool Read/Write Yes element
DEVICE_LISTENBACK bool Read/Write Yes element
DEVICE_JACK_IS_CONNECTED bool Read Yes element
DEVICE_DATA_SOURCE u32 Read/Write Yes element
DEVICE_DATA_SOURCES Vec<u32> Read No element
DEVICE_DATA_SOURCE_NAME String Read No element + qualifier: u32
DEVICE_CHANNEL_NOMINAL_LINE_LEVEL u32 Read/Write Yes element
DEVICE_CHANNEL_NOMINAL_LINE_LEVELS Vec<u32> Read No element
DEVICE_CHANNEL_NOMINAL_LINE_LEVEL_NAME String Read No element + qualifier: u32
DEVICE_HIGH_PASS_FILTER_SETTING u32 Read/Write Yes element
DEVICE_HIGH_PASS_FILTER_SETTINGS Vec<u32> Read No element
DEVICE_HIGH_PASS_FILTER_SETTING_NAME String Read No element + qualifier: u32

Stream properties

Constant Type Access Listenable Extra
STREAM_NAME String Read Yes —
STREAM_IS_ACTIVE bool Read Yes —
STREAM_DIRECTION Scope Read No —
STREAM_LATENCY u32 Read Yes —
STREAM_VIRTUAL_FORMAT StreamDescription Read/Write Yes —
STREAM_PHYSICAL_FORMAT StreamDescription Read/Write Yes —
STREAM_AVAILABLE_VIRTUAL_FORMATS Vec<StreamRangedDescription> Read Yes —
STREAM_AVAILABLE_PHYSICAL_FORMATS Vec<StreamRangedDescription> Read Yes —
TERMINAL_TYPE TerminalType Read No —
STARTING_CHANNEL u32 Read No —

System properties

Constant Type Access Listenable Extra
SYSTEM_NAME String Read No —
SYSTEM_DEVICES Vec<u32> Read Yes —
SYSTEM_DEFAULT_INPUT u32 Read/Write Yes —
SYSTEM_DEFAULT_OUTPUT u32 Read/Write Yes —
SYSTEM_IS_INITING_OR_EXITING bool Read No —
SYSTEM_SLEEPING_IS_ALLOWED bool Read/Write Yes —
SYSTEM_UNLOADING_IS_ALLOWED bool Read/Write No —
SYSTEM_HOG_MODE_IS_ALLOWED bool Read/Write No —
SYSTEM_MIX_STEREO_TO_MONO bool Read/Write No —
SYSTEM_POWER_HINT PowerHint Read/Write No —
SYSTEM_PROCESS_IS_AUDIBLE bool Read/Write Yes —
SYSTEM_PROCESS_IS_MASTER bool Read No —
SYSTEM_USER_SESSION_IS_ACTIVE_OR_HEADLESS bool Read Yes —
SYSTEM_USER_ID_CHANGED u32 Read/Write Yes —
SYSTEM_SERVICE_RESTARTED u32 Read Yes —
SYSTEM_DEFAULT_SYSTEM_OUTPUT u32 Read/Write Yes —
SYSTEM_BOX_LIST Vec<u32> Read Yes —
SYSTEM_CLOCK_DEVICE_LIST Vec<u32> Read Yes —
SYSTEM_PLUGIN_LIST Vec<u32> Read Yes —
SYSTEM_TAP_LIST Vec<u32> Read Yes —
SYSTEM_TRANSPORT_MANAGER_LIST Vec<u32> Read No —
SYSTEM_TRANSLATE_UID_TO_DEVICE u32 Read No qualifier: String
SYSTEM_TRANSLATE_UID_TO_BOX u32 Read No qualifier: String
SYSTEM_TRANSLATE_UID_TO_CLOCK_DEVICE u32 Read No qualifier: String
SYSTEM_TRANSLATE_BUNDLE_ID_TO_PLUGIN u32 Read No qualifier: String
SYSTEM_TRANSLATE_BUNDLE_ID_TO_TRANSPORT_MANAGER u32 Read No qualifier: String

Error handling

All fallible operations return Result<T, CoreAudioError>. The error type wraps a typed ErrorKind enum and the raw OSStatus code. You can match on the kind or inspect the four-character code string:

use coreaudio::ErrorKind;

match device.get_property(DEVICE_NAME) {
    Ok(name) => println!("{name}"),
    Err(e) => match e.kind() {
        ErrorKind::BadDevice => println!("Invalid device"),
        ErrorKind::Permissions => println!("Device is hogged by another process"),
        _ => println!("Error: {} ('{}')", e, e.stringify_code()),
    }
}

Supported audio formats

The FormatId enum covers Linear PCM, AAC (Standard, HE, HEv2, LD, ELD, ELDv2, ELD+SBR, Spatial), Apple Lossless, AC-3, Enhanced AC-3, APAC, AES3, A-Law, AMR, AMR-WB, Opus, and MP3. Unrecognised format IDs are preserved as FormatId::Unknown(u32).

Roadmap

  • Dedicated AudioObject<Clock>, AudioObject<Box>, and AudioObject<Tap> with unique methods
  • Add new wrappers and 'multi-properties' that combine multiple properties into one for things that shouldn't have to be seperate calls
  • Properties for device sample format

Disclaimer

Apple's documentation on what properties can be listened to is pretty much non existant. Because of this, almost all writable properties have been made listenable and it will return an error if it turns out not to be.

If you know of any documentation or if a specific property is incorrectly set, please make an issue in the repository and I will fix it at my earliest convenience.

License

See LICENSE file for details.