Highlight
macOS Monterey 引入 AudioDriverKit,硬件音频驱动可以用一个运行在用户空间的 DriverKit extension 直接连接 CoreAudio HAL,省掉 audio server plug-in、独立安装包和重启流程。
核心内容
做 macOS 音频硬件驱动,以前要维护两层代码。
一层是 audio server plug-in。它负责跟 CoreAudio HAL(Hardware Abstraction Layer,硬件抽象层)对接。另一层是 kernel extension(内核扩展,kext)或 DriverKit extension(驱动扩展,dext)。它负责跟硬件通信。
这条链路能工作,但开发者要处理 plug-in 和 dext 之间的通信。组件变多,资源占用变多,开销和延迟也可能增加。
macOS Big Sur 已经把驱动开发从内核搬到用户空间。安全性变好了,但音频驱动仍然需要 audio server plug-in 加 dext 两个组件。
macOS Monterey 给了新的选择:AudioDriverKit。它是一个 DriverKit 框架,音频驱动只需要一个 dext。框架负责处理 dext 和 CoreAudio HAL 之间的进程间通信。
安装方式也变了。AudioDriverKit extension 打包在 Mac App 里。用户安装 App 后允许驱动,系统就能动态加载 dext,不需要独立 installer package,也不需要重启。
这场 session 的重点很明确:如果你在做 USB 或 PCI 音频硬件驱动,可以把驱动模型收敛到一个用户空间 dext;如果你只做虚拟音频设备,Apple 仍然建议继续使用 audio server plug-in 模型。
详细内容
AudioDriverKit 的基本架构
(02:30)CoreAudio HAL 通过 AudioDriverKit 和 driver extension 通信。AudioDriverKit 会创建一个私有 user client,供 HAL 使用。这个 user client 不暴露给你的 dext。
如果 App 需要直接和 dext 通信,可以额外打开自定义 user client。这个路径适合做厂商控制面板,例如切换设备模式或读取硬件状态。
// SimpleAudioDriver example override of IOService::NewUserClient
kern_return_t SimpleAudioDriver::NewUserClient_Impl(uint32_t in_type,
IOUserClient** out_user_client)
{
kern_return_t error = kIOReturnSuccess;
// Have the super class create the IOUserAudioDriverUserClient object if the type is
// kIOUserAudioDriverUserClientType
if (in_type == kIOUserAudioDriverUserClientType)
{
error = super::NewUserClient(in_type, out_user_client, SUPERDISPATCH);
}
else
{
IOService* user_client_service = nullptr;
error = Create(this, "SimpleAudioDriverUserClientProperties", &user_client_service);
FailIfError(error, , Failure, "failed to create the SimpleAudioDriver user-client");
*out_user_client = OSDynamicCast(IOUserClient, user_client_service);
}
return error;
}
关键点:
NewUserClient_Impl在客户端进程连接 dext 时被调用。kIOUserAudioDriverUserClientType是 HAL 需要的类型,交给IOUserAudioDriver基类创建。super::NewUserClient(...)创建IOUserAudioDriverUserClient,AudioDriverKit 用它处理 HAL 通信。- 其他类型走
IOService::Create,从 dext 的info.plist条目创建自定义 user client。 OSDynamicCast(IOUserClient, user_client_service)把创建出来的服务对象转成IOUserClient返回。
驱动入口:继承 IOUserAudioDriver
(05:58)音频 dext 的入口是 IOUserAudioDriver 子类。它本身继承自 IOService,所以需要处理 Start、Stop、NewUserClient 这些服务生命周期方法。
// SimpleAudioDriver example, subclass of IOUserAudioDriver
class SimpleAudioDriver: public IOUserAudioDriver
{
public:
virtual bool init() override;
virtual void free() override;
virtual kern_return_t Start(IOService* provider) override;
virtual kern_return_t Stop(IOService* provider) override;
virtual kern_return_t NewUserClient(uint32_t in_type,
IOUserClient** out_user_client) override;
virtual kern_return_t StartDevice(IOUserAudioObjectID in_object_id,
IOUserAudioStartStopFlags in_flags) override;
virtual kern_return_t StopDevice(IOUserAudioObjectID in_object_id,
IOUserAudioStartStopFlags in_flags) override;
关键点:
init()和free()管理驱动对象自身的初始化与释放。Start(IOService* provider)在 dext 启动时进入,session 中用它创建SimpleAudioDevice,再用AddObject加到 driver。Stop(IOService* provider)在 dext 停止时进入,用于释放设备相关资源。NewUserClient(...)负责 HAL user client 和自定义 user client 的连接入口。StartDevice(...)和StopDevice(...)会在 HAL 启停某个音频设备的 IO 时被调用。
创建设备、流和音频格式
(07:04)IOUserAudioDevice 表示一个音频设备。设备下面挂 IOUserAudioStream。stream 使用 IOMemoryDescriptor 做音频 IO,这块内存会映射给 CoreAudio HAL。
session 的示例创建的是输入流。真实硬件驱动中,Apple 建议这块内存尽量就是硬件 DMA 使用的同一块 IO memory。
// SimpleAudioDevice::init, set device sample rates and create IOUserAudioStream object
...
SetAvailableSampleRates(sample_rates, 2);
SetSampleRate(kSampleRate_1);
// Create the IOBufferMemoryDescriptor ring buffer for the input stream
OSSharedPtr<IOBufferMemoryDescriptor> io_ring_buffer;
const auto buffer_size_bytes = static_cast<uint32_t>(in_zero_timestamp_period *
sizeof(uint16_t) * input_channels_per_frame);
IOBufferMemoryDescriptor::Create(kIOMemoryDirectionInOut, buffer_size_bytes, 0,
io_ring_buffer.attach());
// Create input stream object and pass in the IO ring buffer memory descriptor
ivars->m_input_stream = IOUserAudioStream::Create(in_driver,
IOUserAudioStreamDirection::Input,
io_ring_buffer.get());
...
关键点:
SetAvailableSampleRates(sample_rates, 2)声明设备支持的采样率列表。SetSampleRate(kSampleRate_1)设置当前采样率。IOBufferMemoryDescriptor::Create(...)创建输入流使用的 ring buffer。buffer_size_bytes按 zero timestamp period、16 位采样大小、输入声道数计算。IOUserAudioStream::Create(...)创建输入方向的音频流,并把内存描述符交给 stream。
(08:19)stream 创建后还不能直接工作。你还要声明它支持的格式列表,设置当前格式,再把 stream 加到设备上。
// SimpleAudioDevice::init continued
IOUserAudioStreamBasicDescription input_stream_formats[2] = {
kSampleRate_1, IOUserAudioFormatID::LinearPCM,
static_cast<IOUserAudioFormatFlags>(
IOUserAudioFormatFlags::FormatFlagIsSignedInteger |
IOUserAudioFormatFlags::FormatFlagsNativeEndian),
static_cast<uint32_t>(sizeof(int16_t)*input_channels_per_frame),
1,
static_cast<uint32_t>(sizeof(int16_t)*input_channels_per_frame),
static_cast<uint32_t>(input_channels_per_frame),
16
},
...
}
ivars->m_input_stream->SetAvailableStreamFormats(input_stream_formats, 2);
ivars->m_input_stream_format = input_stream_formats[0];
ivars->m_input_stream->SetCurrentStreamFormat(&ivars->m_input_stream_format);
error = AddStream(ivars->m_input_stream.get());
关键点:
IOUserAudioStreamBasicDescription描述 stream 的音频格式。IOUserAudioFormatID::LinearPCM表示线性 PCM。FormatFlagIsSignedInteger表示采样值是有符号整数。FormatFlagsNativeEndian表示使用平台原生字节序。SetAvailableStreamFormats(...)注册 stream 支持的格式列表。SetCurrentStreamFormat(...)设置当前格式。AddStream(...)把 stream 加入设备,HAL 才能看到它。
音量控制和自定义属性
(08:50)AudioDriverKit 把音量控制也建模成音频对象。示例用 IOUserAudioLevelControl 创建一个输入音量控制,初始值是 -6 dB,范围是 -96 dB 到 0 dB。
// Create volume control object for the input stream.
ivars->m_input_volume_control = IOUserAudioLevelControl::Create(in_driver,
true, -6.0, {-96.0, 0.0},
IOUserAudioObjectPropertyElementMain,
IOUserAudioObjectPropertyScope::Input,
IOUserAudioClassID::VolumeControl);
// Add volume control to device
error = AddControl(ivars->m_input_volume_control.get());
关键点:
IOUserAudioLevelControl::Create(...)创建音量控制对象。- 第二个参数
true表示这个控制可设置。 -6.0是初始音量值。{-96.0, 0.0}是 dB 范围。IOUserAudioObjectPropertyScope::Input表示这个控制作用于输入 scope。AddControl(...)把控制加入设备。
(09:22)每个 IOUserAudioObject 都可以带自定义属性。自定义属性需要一个 property address,里面包含 selector、scope 和 element。
// SimpleAudioDevice::init, Create custom property
IOUserAudioObjectPropertyAddress prop_addr = {
kSimpleAudioDriverCustomPropertySelector,
IOUserAudioObjectPropertyScope::Global,
IOUserAudioObjectPropertyElementMain
};
custom_property = IOUserAudioCustomProperty::Create(in_driver, prop_addr, true,
IOUserAudioCustomPropertyDataType::String,
IOUserAudioCustomPropertyDataType::String);
qualifier = OSSharedPtr(
OSString::withCString(kSimpleAudioDriverCustomPropertyQualifier0), OSNoRetain);
data = OSSharedPtr(
OSString::withCString(kSimpleAudioDriverCustomPropertyDataValue0), OSNoRetain);
custom_property->SetQualifierAndDataValue(qualifier.get(), data.get());
AddCustomProperty(custom_property.get());
关键点:
IOUserAudioObjectPropertyAddress定义属性地址。kSimpleAudioDriverCustomPropertySelector是自定义 selector。IOUserAudioObjectPropertyScope::Global表示全局 scope。IOUserAudioCustomProperty::Create(...)创建自定义属性对象。- 两个
IOUserAudioCustomPropertyDataType::String分别指定 qualifier 和 data value 的类型。 SetQualifierAndDataValue(...)写入 qualifier 和 data value。AddCustomProperty(...)把属性挂到设备上。
启动 IO,映射 stream 内存
(11:18)当 HAL 要运行设备 IO 时,driver 会调用设备的 StartIO。真实硬件驱动应该在这里启动硬件 IO。示例没有硬件,所以用 timer 和 action 模拟中断与 DMA。
// StartIO
kern_return_t SimpleAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags)
{
__block kern_return_t error = kIOReturnSuccess;
__block OSSharedPtr<IOMemoryDescriptor> input_iomd;
ivars->m_work_queue->DispatchSync(^(){
// Tell IOUserAudioObject base class to start IO for the device
error = super::StartIO(in_flags);
if (error == kIOReturnSuccess)
{
// Get stream IOMemoryDescriptor, create mapping and store to ivars
input_iomd = ivars->m_input_stream->GetIOMemoryDescriptor();
input_iomd->CreateMapping(0, 0, 0, 0, 0, ivars->m_input_memory_map.attach());
// Start timers to send timestamps and generate sine tone on the stream buffer
StartTimers();
}
});
return error;
}
关键点:
DispatchSync(...)把启动流程放到 work queue 上同步执行。super::StartIO(in_flags)通知基类设备 IO 已启动。GetIOMemoryDescriptor()取出输入 stream 的内存描述符。CreateMapping(...)创建IOMemoryMap,后续可以拿到 buffer 地址、长度和 offset。StartTimers()启动示例中的时间源,用来生成 zero timestamp 和测试音频数据。
更新 zero timestamp
(11:57)HAL 需要 sample time 和 host time 的配对来同步 IO。IOUserAudioClockDevice 提供 UpdateCurrentZeroTimestamp 和 GetCurrentZeroTimestamp,用于原子更新和读取这个配对。
kern_return_t SimpleAudioDevice::StartTimers()
{
...
// clear the device's timestamps
UpdateCurrentZeroTimestamp(0, 0);
auto current_time = mach_absolute_time();
auto wake_time = current_time + ivars->m_zts_host_ticks_per_buffer;
// start the timer, the first time stamp will be taken when it goes off
ivars->m_zts_timer_event_source->WakeAtTime(kIOTimerClockMachAbsoluteTime,
wake_time,
0);
ivars->m_zts_timer_event_source->SetEnable(true);
...
}
关键点:
UpdateCurrentZeroTimestamp(0, 0)先清空设备当前 timestamp。mach_absolute_time()读取当前 host time。m_zts_host_ticks_per_buffer表示一个 buffer 对应的 host ticks。WakeAtTime(...)安排 zero timestamp timer 的下一次触发。SetEnable(true)启用 timer event source。
(12:27)timer 触发后,示例读取当前 timestamp,按一个 buffer 的步长推进 sample time 和 host time,再写回设备。
void SimpleAudioDevice::ZtsTimerOccurred_Impl(OSAction* action, uint64_t time)
{
...
GetCurrentZeroTimestamp(¤t_sample_time, ¤t_host_time);
auto host_ticks_per_buffer = ivars->m_zts_host_ticks_per_buffer;
if (current_host_time != 0) {
current_sample_time += GetZeroTimestampPeriod();
current_host_time += host_ticks_per_buffer;
}
else {
current_sample_time = 0;
current_host_time = time;
}
// Update the device with the current timestamp
UpdateCurrentZeroTimestamp(current_sample_time, current_host_time);
// set the timer to go off in one buffer
ivars->m_zts_timer_event_source->WakeAtTime(kIOTimerClockMachAbsoluteTime,
current_host_time + host_ticks_per_buffer, 0);
}
关键点:
GetCurrentZeroTimestamp(...)读取上一次 sample time 和 host time。current_host_time != 0表示已经有锚点 timestamp。GetZeroTimestampPeriod()返回每次推进的 sample frame 数。- 第一次触发时,
time被用作 host time 锚点。 UpdateCurrentZeroTimestamp(...)把新配对写回设备,HAL 用它运行和同步 IO。- 最后一行把 timer 安排到下一个 buffer 的时间点。
写入输入 buffer
(13:03)示例用正弦波模拟输入音频。真实硬件驱动中,这一步通常对应 DMA 写入的音频数据。
void SimpleAudioDevice::GenerateToneForInput(size_t in_frame_size)
{
// Fill out the input buffer with a sine tone
if (ivars->m_input_memory_map)
{
// Get the pointer to the IO buffer and use stream format information
// to get buffer length
const auto& format = ivars->m_input_stream_format;
auto buffer_length = ivars->m_input_memory_map->GetLength() /
(format.mBytesPerFrame / format.mChannelsPerFrame);
auto num_samples = in_frame_size;
auto buffer = reinterpret_cast<int16_t*>(ivars->m_input_memory_map->GetAddress() +
ivars->m_input_memory_map->GetOffset());
...
}
关键点:
ivars->m_input_memory_map存在时,说明StartIO已经创建了内存映射。m_input_stream_format提供当前 stream 格式。GetLength()返回映射内存长度。format.mBytesPerFrame / format.mChannelsPerFrame得到单个采样的字节数。GetAddress() + GetOffset()得到可写的 buffer 起始地址。reinterpret_cast<int16_t*>对应示例中的 signed 16-bit PCM 格式。
(13:30)写 buffer 时,示例读取音量控制的 scalar value,把它乘到正弦波上,再按声道数写入 ring buffer。
void SimpleAudioDevice::GenerateToneForInput(size_t in_frame_size)
{
...
auto input_volume_level = ivars->m_input_volume_control->GetScalarValue();
for (size_t i = 0; i < num_samples; i++)
{
float float_value = input_volume_level *
sin(2.0 * M_PI * frequency *
static_cast<double>(ivars->m_tone_sample_index) / format.mSampleRate);
int16_t integer_value = FloatToInt16(float_value);
for (auto ch_index = 0; ch_index < format.mChannelsPerFrame; ch_index++)
{
auto buffer_index =
(format.mChannelsPerFrame * ivars->m_tone_sample_index + ch_index) %
buffer_length;
buffer[buffer_index] = integer_value;
}
ivars->m_tone_sample_index += 1;
}
}
关键点:
GetScalarValue()读取输入音量控制的增益。sin(...)生成当前 sample index 对应的正弦波采样。FloatToInt16(float_value)把浮点采样转为 16 位整数采样。- 内层循环按
mChannelsPerFrame给每个声道写同一个采样值。 % buffer_length处理 ring buffer 回绕。m_tone_sample_index += 1推进下一次生成的位置。
配置变更:让 HAL 先停 IO
(14:02)如果要改采样率或 stream 格式,driver 不能直接改设备状态。正确流程是调用 RequestDeviceConfigurationChange。
HAL 收到请求后,会通知监听者配置变更将开始。如果设备正在跑 IO,HAL 会先停掉 IO,捕获旧状态,然后调用 driver 的 PerformDeviceConfigurationChange。变更完成后,HAL 更新 IO 相关状态,通知客户端,再按需重启 IO。
// IOUserAudioClockDevice.h and IOUserAudioDevice.h
kern_return_t RequestDeviceConfigurationChange(uint64_t in_change_action,
OSObject* in_change_info);
virtual kern_return_t PerformDeviceConfigurationChange(uint64_t in_change_action,
OSObject* in_change_info);
virtual kern_return_t AbortDeviceConfigurationChange(uint64_t change_action,
OSObject* in_change_info);
关键点:
RequestDeviceConfigurationChange(...)发起配置变更请求。in_change_action标识本次变更类型。in_change_info可以是任意OSObject,用于传递变更上下文。PerformDeviceConfigurationChange(...)是真正允许修改设备状态的位置。AbortDeviceConfigurationChange(...)用于中止配置变更。
(15:32)示例用自定义 user client 命令模拟硬件自下而上的配置变更,请求切换采样率。
kern_return_t SimpleAudioDriver::HandleTestConfigChange()
{
auto change_info = OSSharedPtr(OSString::withCString("Toggle Sample Rate"), OSNoRetain);
return ivars->m_simple_audio_device->RequestDeviceConfigurationChange(
k_custom_config_change_action, change_info.get());
}
class SimpleAudioDevice: public IOUserAudioDevice
{
...
virtual kern_return_t PerformDeviceConfigurationChange(uint64_t change_action,
OSObject* in_change_info) final LOCALONLY;
}
关键点:
OSString::withCString("Toggle Sample Rate")创建一段变更说明。OSSharedPtr(..., OSNoRetain)管理这个OSString。RequestDeviceConfigurationChange(...)把自定义 action 和 change info 交给设备。PerformDeviceConfigurationChange(...)需要在设备子类中覆盖。
(16:05)执行变更时,示例读取当前采样率,在两个采样率之间切换,然后通知 stream 设备采样率已变化。
// In SimpleAudioDevice::PerformDeviceConfigurationChange
kern_return_t ret = kIOReturnSuccess;
switch (change_action) {
case k_custom_config_change_action: {
if (in_change_info) {
auto change_info_string = OSDynamicCast(OSString, in_change_info);
DebugMsg("%s", change_info_string->getCStringNoCopy());
}
double rate_to_set = static_cast<uint64_t>(GetSampleRate()) !=
static_cast<uint64_t>(kSampleRate_1) ? kSampleRate_1 : kSampleRate_2;
ret = SetSampleRate(rate_to_set);
if (ret == kIOReturnSuccess) {
// Update stream formats with the new rate
ret = ivars->m_input_stream->DeviceSampleRateChanged(rate_to_set);
}
}
break;
default:
ret = super::PerformDeviceConfigurationChange(change_action, in_change_info);
break;
}
// Update the cached format:
ivars->m_input_stream_format = ivars->m_input_stream->GetCurrentStreamFormat();
return ret;
}
关键点:
switch (change_action)按变更类型分发。OSDynamicCast(OSString, in_change_info)取回请求阶段传入的说明字符串。GetSampleRate()读取当前设备采样率。SetSampleRate(rate_to_set)修改设备采样率。DeviceSampleRateChanged(rate_to_set)让 stream 更新当前 stream format。- 未处理的 action 交给
super::PerformDeviceConfigurationChange(...)。 - 最后用
GetCurrentStreamFormat()更新 driver 缓存的格式。
核心启发
-
做什么:为 USB 音频接口做一个伴随控制 App。 为什么值得做:AudioDriverKit extension 可以打包在 Mac App 里,用户安装 App 后加载驱动,不需要独立安装器。 怎么开始:用
IOUserAudioDriver作为 dext 入口,把设备控制命令放在自定义 user client 中,必要时用RequestDeviceConfigurationChange更新采样率或 stream 格式。 -
做什么:给录音设备增加硬件增益和输入电平控制。 为什么值得做:session 展示了
IOUserAudioLevelControl,控制值可以在 IO path 中用于处理输入 buffer。 怎么开始:在IOUserAudioDevice初始化时创建IOUserAudioLevelControl,用AddControl加到设备;在写入或读取 buffer 时调用GetScalarValue()应用增益。 -
做什么:为多采样率硬件做稳定的采样率切换。 为什么值得做:采样率变化会影响 IO 状态,AudioDriverKit 提供了 HAL 协调的配置变更流程。 怎么开始:先调用
RequestDeviceConfigurationChange,在PerformDeviceConfigurationChange中执行SetSampleRate,再调用 stream 的DeviceSampleRateChanged。 -
做什么:把硬件厂商的诊断信息暴露给控制面板。 为什么值得做:AudioDriverKit 支持自定义 user client,也支持
IOUserAudioCustomProperty。 怎么开始:用IOUserAudioCustomProperty::Create定义设备属性;App 侧通过自定义 user client 和 dext 通信,读取硬件状态或触发测试命令。 -
做什么:构建低延迟输入设备的时间戳验证工具。 为什么值得做:HAL 依赖 sample time 与 host time 配对同步 IO,timestamp 贴近硬件时钟会影响录音稳定性。 怎么开始:在设备中维护
UpdateCurrentZeroTimestamp,记录mach_absolute_time与 sample time 的关系,并在控制 App 中显示 jitter 或 buffer 周期变化。
关联 Session
- What’s new in AVFoundation — 了解 Apple 媒体框架在播放、检查和创作音视频资源上的更新。
- Discover built-in sound classification in SoundAnalysis — 从音频输入中检测特定声音,适合和硬件输入链路一起评估。
- Explore ShazamKit — 使用精确音频匹配构建识别类体验,关注音频数据进入 App 后的处理。
- Immerse your app in Spatial Audio — 理解 Spatial Audio 在播放体验中的位置,补齐音频输出体验的上层视角。
评论
GitHub Issues · utterances