Create audio drivers with DriverKit
System & Services 高级 24m

使用 DriverKit 创建音频驱动

Create audio drivers with DriverKit

2021年6月10日

在 Apple 官方观看视频

一句话判断

DriverKit 终于扩展到了音频设备驱动——如果你在做专业音频硬件(USB 音频接口、MIDI 控制器),现在可以在用户态写驱动了,不需要再面对内核态驱动的崩溃风险和漫长的审批流程。

这场 Session 讲了什么

Session 介绍了 macOS Monterey 中 DriverKit 对音频设备驱动的支持。DriverKit 是 Apple 推动的用户态驱动框架,之前已经覆盖了 USB、HID、网络和存储设备。今年新增了 Audio DriverKit,允许开发者在用户空间为 USB 音频设备和蓝牙音频设备编写驱动。

传统内核扩展(Kernel Extension,KEXT)驱动有几个致命问题:KEXT 运行在内核态,一个 bug 就会导致整个系统崩溃;KEXT 需要禁用 SIP(System Integrity Protection)才能安装;Apple 正在逐步淘汰 KEXT。DriverKit 把驱动运行在用户态的一个独立进程中——驱动崩溃只会影响你的设备,不会拖垮整个系统。Session 覆盖了 Audio DriverKit 的架构(IOUserAudioDeviceIOUserAudioStreamIOUserAudioControl)、如何处理音频缓冲区的数据传输、以及如何将驱动打包为 System Extension 分发给用户。

值得深挖的点

IOUserAudioStream 的缓冲区管理

音频驱动的核心是缓冲区管理。IOUserAudioStream 使用 ring buffer(环形缓冲区)在驱动进程和音频硬件之间传递 PCM 数据。你需要指定缓冲区大小、采样率和位深度。Session 建议缓冲区大小设为 256 或 512 帧——太小会增加 CPU 中断频率,太大会增加延迟。对于专业音频场景(低延迟录音),256 帧 @48kHz 约等于 5.3ms 的缓冲延迟,这是大多数专业音乐人可以接受的值。

从 KEXT 迁移到 DriverKit 的路径

如果你的现有音频驱动是 KEXT,迁移到 DriverKit 不是简单的 API 替换。KEXT 使用 IOKit 框架的 C++ API,DriverKit 使用 Objective-C 子类的 API。但数据通路的核心逻辑可以复用:USB 传输描述符的配置、音频格式的协商、音量/静音控制的结构都可以迁移。Session 建议先实现一个最小可行的 DriverKit 驱动(只支持基本的播放功能),验证硬件通信正常后再逐步添加功能。

代码片段

// 定义 Audio DriverKit 驱动的基本结构
class MyAudioDevice: IOUserAudioDevice {
    
    override func start() -> Bool {
        guard super.start() else { return false }
        
        // 创建音频流(输出方向)
        let outputStream = IOUserAudioStream(
            createAudioStreamWithDescription: AudioStreamBasicDescription(
                mSampleRate: 48000.0,
                mFormatID: kAudioFormatLinearPCM,
                mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked,
                mBytesPerPacket: 8,
                mFramesPerPacket: 1,
                mBytesPerFrame: 8,
                mChannelsPerFrame: 2,
                mBitsPerChannel: 32
            ),
            maxBufferSize: 2048,
            direction: .output
        )
        
        // 添加流到设备
        addStream(outputStream)
        
        // 创建音量控制
        let volumeControl = IOUserAudioLevelControl(
            createVolumeControlWithValue: 0.8,
            minValue: 0.0,
            maxValue: 1.0,
            inChannel: 0,
            inChannelName: "Master Volume",
            inScope: .output,
            inElement: .master
        )
        
        addControl(volumeControl)
        
        return true
    }
}
// 处理音频数据传输
class MyAudioStream: IOUserAudioStream {
    
    private var ringBuffer: UnsafeMutableRawPointer?
    private var ringBufferSize: Int = 0
    
    override func performAudioIOStart(
        _ timestamp: UnsafePointer<AudioTimeStamp>,
        frameCount: UInt32) -> IOReturn {
        
        // 从 ring buffer 读取音频数据并写入 USB 端点
        let framesToWrite = Int(frameCount)
        var frameBuffer = [Float](repeating: 0.0, count: framesToWrite * 2)
        
        // 从环形缓冲区拷贝数据
        copyFromRingBuffer(&frameBuffer, frames: framesToWrite)
        
        // 通过 USB 发送音频数据到硬件
        sendUSBAudioData(frameBuffer)
        
        return kIOReturnSuccess
    }
    
    private func copyFromRingBuffer(_ buffer: inout [Float], frames: Int) {
        // 从环形缓冲区读取指定帧数的数据
        // 使用 lock-free 方式避免阻塞音频线程
        guard let ringPtr = ringBuffer else { return }
        
        let bytesToCopy = frames * 2 * MemoryLayout<Float>.size
        memcpy(&buffer, ringPtr, bytesToCopy)
    }
}
// USB 音频设备的配置
class MyUSBAudioDriver: IOUserAudioDevice {
    
    private var usbInterface: IOUSBHostInterface?
    
    override func start() -> Bool {
        guard super.start() else { return false }
        
        // 获取 USB 音频接口
        guard let matching = getUSBMatchingDictionary(
            vendorID: 0x1234,
            productID: 0x5678
        ) else { return false }
        
        // 初始化 USB 通信
        // 设置 isochronous 传输用于音频数据
        // 设置 control 传输用于音量/静音控制
        
        setupAudioStreams()
        setupControls()
        
        return true
    }
    
    private func setupAudioStreams() {
        // 根据设备支持的格式创建输入和输出流
        let formats = queryDeviceSupportedFormats()
        
        for format in formats {
            let stream = IOUserAudioStream(
                createAudioStreamWithDescription: format,
                maxBufferSize: 2048,
                direction: format.isInput ? .input : .output
            )
            addStream(stream)
        }
    }
}

最佳实践

  • 音频缓冲区大小提供多个选项(128、256、512、1024 帧),让用户在延迟和稳定性之间做选择。不要只提供一个固定值——不同使用场景需求不同。
  • 在驱动中实现 suspendresume 方法,处理系统休眠和唤醒。音频设备在唤醒后需要重新初始化 USB 连接和缓冲区。
  • 使用 IOUserAudioClockDevice 为音频流提供精确的时钟源。专业音频设备对时钟精度的要求在微秒级别,系统默认时钟不够精确。

还有什么值得关注

  • DriverKit 的音频驱动以 System Extension(.systemextension)的形式分发,不需要用户禁用 SIP。
  • Core Audio 的 AudioDeviceID 在 DriverKit 驱动中自动生成,你的驱动不需要手动注册。
  • Apple 提供了一个 Audio DriverKit 的示例项目(USBAudioDriver),涵盖了基本的 USB 音频设备驱动实现。
WWDC 2021