音频驱动开发全解析

1. 虚拟音频设备架构

虚拟音频设备 MyAudioDevice 是 IOAudioDevice 的子类,它会分配一个 MyAudioEngine 类的实例,该类继承自 IOAudioEngine 。同时, MyAudioDevice 还会分配多个 IOAudioControl 实例,用于调整左右声道的输入输出音量、静音输入输出等。由于没有实际的硬件设备,这些控制目前不会执行实际操作,仅用于演示。 MyAudioEngine 类将代替实际硬件来表示I/O引擎,它会分配两个 IOAudioStream 实例,分别用于输出和输入样本缓冲区。当数据进入输出缓冲区时,会直接复制到输入缓冲区。

2. 驱动和硬件初始化

IOAudioEngine 主要负责硬件初始化,其实现通常较为简约,因为音频驱动的大部分复杂性会在 IOAudioEngine 的子类中实现。该类内部会执行一些重要任务,例如提供一个中央 IOWorkLoop 和 IOCommandGate ,供 IOAudioEngine 等从属类共享,用于序列化对驱动和硬件的访问。此外,它还提供共享的定时器服务,对象可以通过 addTimerEvent() 函数注册接收定时器事件:

virtual IOReturn addTimerEvent(OSObject *target, TimerEvent event, AbsoluteTime interval);

参数说明:
- target :指向将接收定时器事件通知的对象的指针。
- interval :以 AbsoluteTime (纳秒)为单位指定定时器事件的频率。
- event :指定回调函数。

音频驱动的 IOAudioDevice 子类通常会执行以下步骤:
1. 配置硬件设备的提供者并枚举所需资源。对于PCI或Thunderbolt设备,需要映射设备内存或I/O区域;对于USB设备,需要枚举接口和/或管道。
2. 配置设备以进行操作,例如通过访问设备寄存器或发送控制请求将设备从复位/睡眠模式中唤醒。
3. 如果驱动支持多个音频芯片或具有不同数量的DMA通道、输入或输出的芯片,需要查询设备以确定其确切功能。
4. 设置音频设备的名称和描述,以便Core Audio和用户空间应用程序识别。
5. 根据从设备提取的信息,创建适当数量的 IOAudioEngine 实例,这些实例将依次分配一个或多个 IOAudioStream 实例以及相关的样本缓冲区。

以下是 MyAudioDevice 类的头文件:

#ifndef _MYAUDIODEVICE_H__
#define _MYAUDIODEVICE_H__

#include <IOKit/audio/IOAudioDevice.h>

#define MyAudioDevice com_osxkernel_MyAudioDevice

class MyAudioDevice : public IOAudioDevice
{     
    OSDeclareDefaultStructors(MyAudioDevice);

    virtual bool initHardware(IOService *provider);
    bool createAudioEngine();

    // Control callbacks
    static IOReturn volumeChangeHandler(OSObject* target, IOAudioControl *volumeControl,  
                                        SInt32 oldValue, SInt32 newValue);
    virtual IOReturn volumeChanged(IOAudioControl *volumeControl, SInt32 oldValue, SInt32  
                                   newValue);

    static IOReturn outputMuteChangeHandler(OSObject* target, IOAudioControl *muteControl,  
                                            SInt32 oldValue, SInt32 newValue);
    virtual IOReturn outputMuteChanged(IOAudioControl* muteControl, SInt32 oldValue, SInt32  
                                       newValue);

    static IOReturn gainChangeHandler(OSObject* target, IOAudioControl* gainControl, SInt32  
                                      oldValue, SInt32 newValue);
    virtual IOReturn gainChanged(IOAudioControl* gainControl, SInt32 oldValue, SInt32  
                                 newValue);

    static IOReturn inputMuteChangeHandler(OSObject* target, IOAudioControl *muteControl,  
                                           SInt32 oldValue, SInt32 newValue);
    virtual IOReturn inputMuteChanged(IOAudioControl* muteControl, SInt32 oldValue, SInt32  
                                      newValue);
};

#endif

MyAudioDevice 类缺少一些常见的I/O Kit生命周期方法,如 start() 和 stop() ,因为这些方法由父类 IOAudioDevice 实现。 start() 方法会处理电源管理注册,然后调用 initHardware() 方法,驱动开发者应实现该方法。 initHardware() 是执行与硬件相关初始化的首选方法,在返回之前,应创建至少一个 IOAudioEngine 实例并通过调用 activateAudioEngine() 方法激活它。以下是 MyAudioDevice 的 initHardware() 方法实现:

bool MyAudioDevice::initHardware(IOService *provider)
{
    bool result = false;

    IOLog("MyAudioDevice[%p]::initHardware(%p)\n", this, provider);

    if (!super::initHardware(provider))
        goto done;

    setDeviceName("My Audio Device");
    setDeviceShortName("MyAudioDevice");
    setManufacturerName("osxkernel.com");

    if (!createAudioEngine())
        goto done;

    result = true;

done:
    return result;
}

由于 MyAudioDevice 没有实际的硬件设备支持,这里只设置了设备名称、简称和制造商名称,这些信息将被Core Audio用于各种目的,设备名称将在OS X系统偏好设置中可见。最后调用 createAudioEngine() 方法来初始化并创建 MyAudioEngine 实例。

3. 注册音频控制

音频设备通常具有一个或多个可控制的属性,如调整音量、静音等。为了让用户空间客户端能够看到这些控制,需要使用 IOAudioControl 来描述每个属性。 IOAudioFamily 提供了三个 IOAudioControl 的子类,其中 IOAudioLevelControl 用于控制音量。以下是创建和注册左声道音量控制的示例:

control = IOAudioLevelControl::createVolumeControl
              (65535,   // Initial value 
               0,       // min value
               65535,   // max value
              (-22 << 16) + (32768), // -22.5 in IOFixed (16.16)
               0,       // max 0.0 in IOFixed
               kIOAudioControlChannelIDDefaultLeft,
               kIOAudioControlChannelNameLeft,
               0,       // control ID - driver-defined
               kIOAudioControlUsageOutput);
if (!control) {
    goto Done;
}

control->setValueChangeHandler(volumeChangeHandler, this);
audioEngine->addDefaultAudioControl(control);
control->release();

创建音量控制使用了特殊的工厂方法 createVolumeControl() ,前三个参数分别表示初始音量值、最小值和最大值,可以根据硬件寄存器规范指定不同的值,也可以在回调中转换这些值以匹配硬件音量控制寄存器的预期范围。接下来的两个参数设置最小值和最大值对应的dB值,音量通常从0.0 dB(表示全音量)到某个负dB值。dB值以定点值形式存储。 kIOAudioControlChannelIDDefaultLeft 指定该控制是用于左声道, IOAudioFamily 还为其他声道定义了常量名称。最后一个参数 kIOAudioControlUsageOutput 表示这是一个输出音量控制。

当控制值发生变化时,会调用回调函数:

IOReturn SampleAudioDevice::volumeChangeHandler(IOService *target, IOAudioControl *volumeControl, SInt32 oldValue, SInt32 newValue)
{
    IOReturn result = kIOReturnBadArgument;
    SampleAudioDevice *audioDevice;

    audioDevice = (SampleAudioDevice *)target;
    if (audioDevice) {
        result = audioDevice->volumeChanged(volumeControl, oldValue, newValue);
    }
    return result;
}

IOReturn SampleAudioDevice::volumeChanged(IOAudioControl *volumeControl, SInt32 oldValue, SInt32 newValue)
{
    IOLog("SampleAudioDevice[%p]::volumeChanged(%p, %ld, %ld)\n",  
           this, volumeControl, oldValue, newValue);
    if (volumeControl) {
        IOLog("\t-> Channel %ld\n", volumeControl->getChannelID());
    }

    // Add hardware volume code change  

    return kIOReturnSuccess;
}

回调函数会提供指向值已更改的控制的指针,并传递旧值和新值。对于大多数硬件驱动,该方法会将新值写入硬件寄存器,以实现音量的增减或其他操作。

静音控制的实现与音量控制类似,但使用 createMuteControl() 工厂方法:

// Create an input mute control
control = IOAudioToggleControl::createMuteControl(false,    // initial state - unmuted
                           AudioControlChannelIDAll,        // Affects all channels
                           kIOAudioControlChannelNameAll,
                           0,                               // control ID - driver-defined
                           kIOAudioControlUsageInput);

这里的静音控制指定影响所有声道。

4. 音频引擎实现流程
graph TD;
    A[开始] --> B[重写initHardware方法];
    B --> C[分配样本缓冲区和IOAudioStream实例];
    C --> D[实现performAudioEngineStart和performAudioEngineStop方法];
    D --> E[实现free方法];
    E --> F[实现getCurrentSampleFrame方法];
    F --> G[实现performFormatChange方法];
    G --> H[实现通知机制];
    H --> I[实现clipOutputSamples和convertInputSamples方法];
    I --> J[结束];
5. 注册音频控制操作步骤
步骤 操作
1 使用 IOAudioLevelControl::createVolumeControl 创建音量控制
2 设置音量控制的回调函数
3 使用 addDefaultAudioControl 方法将控制添加到父对象
4 释放控制的引用
5 使用 IOAudioToggleControl::createMuteControl 创建静音控制
6 重复步骤2 - 4添加静音控制
6. 实现音频引擎

音频引擎在音频驱动中负责实际的I/O操作,它是抽象类 IOAudioEngine 的子类,控制I/O行为并处理一个或多个相关样本缓冲区的传输。对于能同时驱动多个独立输入和输出的音频设备,建议为每个I/O通道创建一个 IOAudioEngine 实例。实现 IOAudioEngine 子类需要完成以下步骤:
1. 重写 initHardware() 方法,执行额外的硬件初始化并分配和初始化其他所需资源。
2. 分配样本缓冲区和相关的 IOAudioStream 实例。
3. 实现 performAudioEngineStart() 和 performAudioEngineStop() 方法,用于启动和停止I/O。
4. 实现 free() 方法,清理使用的资源。
5. 实现 getCurrentSampleFrame() 方法。
6. 实现 performFormatChange() 方法,响应Core Audio的格式更改请求。
7. 实现一种机制,通知父类样本缓冲区何时回到起始位置的时间戳。
8. 为输出流实现 clipOutputSamples() 方法,为输入流实现 convertInputSamples() 方法。

以下是 MyAudioDevice 的 IOAudioEngine 子类的头文件:

#ifndef _MYAUDIOENGINE_H_
#define _MYAUDIOENGINE_H_

#include <IOKit/audio/IOAudioEngine.h>

#include "MyAudioDevice.h"

#define MyAudioEngine com_osxkernel_MyAudioEngine

class MyAudioEngine : public IOAudioEngine
{
    OSDeclareDefaultStructors(MyAudioEngine)

public:
   virtual void free();

   virtual bool initHardware(IOService* provider);
   virtual void stop(IOService *provider);

   virtual IOAudioStream *createNewAudioStream(IOAudioStreamDirection direction,  
                                               void* sampleBuffer, UInt32 sampleBufferSize);

   virtual IOReturn performAudioEngineStart();
   virtual IOReturn performAudioEngineStop();

   virtual UInt32 getCurrentSampleFrame();

   virtual IOReturn performFormatChange(IOAudioStream* audioStream, const IOAudioStreamFormat*  
                                        newFormat, const IOAudioSampleRate* newSampleRate);

   virtual IOReturn clipOutputSamples(const void* mixBuf, void* sampleBuf, UInt32  
                                      firstSampleFrame, UInt32 numSampleFrames,  
                                      const IOAudioStreamFormat* streamFormat,  
                                      IOAudioStream* audioStream);
   virtual IOReturn convertInputSamples(const void* sampleBuf, void* destBuf, UInt32  
                                        firstSampleFrame, UInt32 numSampleFrames,  
                                        const IOAudioStreamFormat* streamFormat,  
                                        IOAudioStream* audioStream);

private:
   IOTimerEventSource*     fAudioInterruptSource;
   SInt16*                 fOutputBuffer;
   SInt16*                 fInputBuffer;
   UInt32                  fInterruptCount;
   SInt64                  fNextTimeout;

   static void             interruptOccured(OSObject* owner, IOTimerEventSource* sender);
   void                    handleAudioInterrupt();
};

#endif
7. I/O引擎初始化

IOAudioEngine 有自己的 initHardware() 方法,需要重写该方法以执行特定于I/O引擎的硬件初始化,以及分配和初始化其他所需资源。方法返回后,引擎应准备好启动I/O,然后可以调用 performAudioEngineStart() 开始实际的I/O操作。在本例中, initHardware() 方法由 IOAudioDevice::activateAudioEngine() 调用。虽然 IOAudioEngine 继承自 IOService ,但这里不重写或调用 start() 方法,因为该类由 MyAudioDevice 分配和初始化,而不是由I/O Kit。不过,声明了 stop() 方法,可用于撤销 initHardware() 中执行的操作。以下是 MyAudioDevice 驱动的 initHardware() 方法实现:

#define kAudioSampleRate                  48000
#define kAudioNumChannels                 2
#define kAudioSampleDepth                 16
#define kAudioSampleWidth                 16
#define kAudioBufferSampleFrames          kAudioSampleRate/2
// Buffer holds half second's worth of audio.
#define kAudioSampleBufferSize           (kAudioBufferSampleFrames * kAudioNumChannels *  
                                         (kAudioSampleDepth / 8))

#define kAudioInterruptInterval           10000000 // nanoseconds (1000 ms / 100 hz = 10ms).
#define kAudioInterruptHZ                 100

bool MyAudioEngine::initHardware(IOService *provider)
{
    bool result = false;
    IOAudioSampleRate initialSampleRate;
    IOAudioStream*    audioStream;
    IOWorkLoop*       workLoop = NULL;

    IOLog("MyAudioEngine[%p]::initHardware(%p)\n", this, provider);

    if (!super::initHardware(provider))
        goto done;

    fAudioInterruptSource = IOTimerEventSource::timerEventSource(this, interruptOccured);
    if (!fAudioInterruptSource)
        return false;

    workLoop = getWorkLoop();
        if (!workLoop)
                return false;

    if (workLoop->addEventSource(fAudioInterruptSource) != kIOReturnSuccess)
        return false;

    // Setup the initial sample rate for the audio engine
    initialSampleRate.whole = kAudioSampleRate;
    initialSampleRate.fraction = 0;

    setDescription("My Audio Device");
    setSampleRate(&initialSampleRate);

    // Set the number of sample frames in each buffer
    setNumSampleFramesPerBuffer(kAudioBufferSampleFrames);
    setInputSampleLatency(kAudioSampleRate / kAudioInterruptHZ);
    setOutputSampleOffset(kAudioSampleRate / kAudioInterruptHZ);

    fOutputBuffer = (SInt16 *)IOMalloc(kAudioSampleBufferSize);
    if (!fOutputBuffer)
        goto done;

    fInputBuffer = (SInt16 *)IOMalloc(kAudioSampleBufferSize);
    if (!fInputBuffer)
        goto done;

    // Create an IOAudioStream for each buffer and add it to this audio engine
    audioStream = createNewAudioStream(kIOAudioStreamDirectionOutput,  
                                       fOutputBuffer, kAudioSampleBufferSize);
    if (!audioStream)
        goto done;

    addAudioStream(audioStream);
    audioStream->release();

    audioStream = createNewAudioStream(kIOAudioStreamDirectionInput,  
                                       fInputBuffer, kAudioSampleBufferSize);
    if (!audioStream)
        goto done;

    addAudioStream(audioStream);
    audioStream->release();

    result = true;
done:
    return result;
}

该方法首先分配一个 IOTimerEventSource ,用于模拟硬件中断,并使用 setDescription() 方法设置描述信息,该信息将在系统偏好设置的声音面板中显示。接着设置引擎的采样率,采样率是 IOAudioEngine 的属性,如果引擎管理多个流,它们必须具有相同的采样率。还需要定义样本缓冲区包含的样本数量,在 MyAudioEngine 中使用两个流(一个输入流和一个输出流),缓冲区大小通过 setNumSampleFramesPerBuffer() 方法设置为采样率的一半,即24000个样本,对应半秒的音频。通过公式 24000 samples * 2 channels * (16 bits / 8 bits = 2 bytes) = 96000 bytes 计算出样本缓冲区的大小。

setInputSampleLatency() 和 setOutputSampleLatency() 方法用于向Core Audio指示从样本计划播放到实际在硬件中开始播放所需的时间。在本例中,由于没有硬件延迟,将延迟设置为一个中断周期(10毫秒),以给Core Audio在读写样本时留出一些空间。

还需要为样本缓冲区分配内存,在 MyAudioDevice 中,由于不执行到硬件设备的DMA操作,直接使用 IOMalloc() 分配输入和输出缓冲区。对于基于硬件的驱动,需要分配 IOBufferMemoryDescriptor 或为缓冲区创建单独的 IOMemoryDescriptor ,并为DMA或I/O传输准备缓冲区。每个缓冲区需要与一个 IOAudioStream 关联,通过 createNewAudioStream() 方法分配 IOAudioStream 实例,并使用 addAudioStream() 方法将其添加到引擎中,添加后可以释放引用,父类会处理最终的释放。

8. 创建和初始化音频流

IOAudioEngine 至少需要一个 IOAudioStream 才能正常工作,一个流与一个样本缓冲区关联,并描述该缓冲区支持的格式和采样率。流可以是输出流或输入流, IOAudioStream 负责处理数据进出样本缓冲区的机制。它内部维护一个混合缓冲区,将多个客户端的音频数据混合成一个流,然后存储到最终的样本缓冲区中。对于大多数情况, IOAudioStream 的默认行为就足够了,但如果驱动需要更高级的功能,可以重写 IOAudioStream 的大多数方法以提供自定义行为。以下是 MyAudioEngine 的 createNewAudioStream() 方法,用于创建输入和输出流:

IOAudioStream *MyAudioEngine::createNewAudioStream(IOAudioStreamDirection direction,  
                                                   void* sampleBuffer, UInt32 sampleBufferSize)
{
    IOAudioStream* audioStream;

    audioStream = new IOAudioStream;
    if (audioStream) {
        if (!audioStream->initWithAudioEngine(this, direction, 1)) {
            audioStream->release();
        } else {
            IOAudioSampleRate rate;
            IOAudioStreamFormat format = {
                2,                                                  // num channels
9. I/O引擎初始化操作步骤
步骤 操作
1 重写 initHardware 方法,调用父类的 initHardware 方法
2 分配 IOTimerEventSource
3 获取工作循环并添加事件源
4 设置音频引擎的初始采样率
5 设置设备描述、采样率、每个缓冲区的样本帧数、输入和输出样本延迟
6 使用 IOMalloc 分配输入和输出缓冲区
7 使用 createNewAudioStream 方法创建输入和输出流
8 使用 addAudioStream 方法将流添加到引擎中并释放引用
10. 创建和初始化音频流操作步骤
graph TD;
    A[开始] --> B[创建IOAudioStream实例];
    B --> C{初始化是否成功};
    C -- 是 --> D[设置采样率和流格式];
    C -- 否 --> E[释放IOAudioStream实例];
    D --> F[结束];
    E --> F;

综上所述,开发音频驱动需要对虚拟音频设备架构、硬件初始化、音频控制注册、音频引擎实现等方面有深入的理解,并按照相应的步骤进行操作,才能实现一个完整的音频驱动程序。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐