MacOS 音频驱动开发二:SoundFlower源码学习

543 阅读17分钟

前言

SoundFlower目前虽然无法在最新系统使用,但是音频驱动的实现还是值得学习。它主要是通过IOAudioFamily实现一个内核扩展。然后在应用程序中使用Core Audio实现对音量的调节及对采样率的切换。下面根据实现虚拟声卡的原理并根据SoundFlower工程进行解析。(学习资料来源网络、苹果开发者网站和OSX and iOS kernel Programming 一书)(有些地方理解有问题,后续还需要修正)

一、SoundFlower内核工程结构(虚拟声卡实现)

截屏2024-02-14 16.24.14.png 这里先大概讲一下整个工程的结构及主要作用(部分基本术语可参考翻译不同可以基本理解)

  1. 工程目录,包含4个文件。主要就是基于 IOAudioDevice 和 IOAudioEngine 的实现。这个是整个音频驱动的实现。(下面音频驱动实现过程会详细讲解)
  2. info.这个是整个音频驱动的一些配置信息。其中OSBundleLibraries和IOKitPersonalities这2个字段是固定且必须的。
  • IOKitPersonalities
    • PhantomAudioDriver - 这只是一个驱动的命名,保持有意义就行
    • AudioEngines - 这是自定义的音频引擎一些参数(SoundFlower 有2个audioengine 一个2通道一个64通道,内部是它的相关配置参数,这里挑几个重要的)
      • BlockSize 采样缓存区可存储数量(每个缓存区可以存多少个采样组)
      • Description 引擎描述(相当于引擎的名字)
      • Formats 相关音频格式信息
      • NumBlocks 采样块数量
      • NumStreams 有多少个Audio Stream
      • SampleRates 支持的采样率
    • CFBundleIdentifier - 唯一标识符 一般就是bundle ID
    • IOClass - 这个就是实现 IOAudioDevice 的实例类名(一定要保持唯一)
    • IOMatchCategory - 匹配实现的类名 (一般和IOClass一致)
    • IOProviderClass - 这里我们是虚拟设备没有实体声卡所以用 IOResources
    • IOResourceMatch - 这里依赖IOKit 实现
  • OSBundleLibraries (和一般驱动开发需要配置差不多)
    • com.apple.iokit.IOAudioFamily 使用IOAudioFamily 的版本号(可以参考这篇文章
    • com.apple.kpi.iokit、com.apple.kpi.libkern、com.apple.kpi.mach(通过 uname -r 查看自己的内核版本进行替换)

二、音频驱动(虚拟声卡的)实现过程(结合SoundFlower 内核扩展部分源码)

为了和使用户空间Core Audio进行功能上的交互,这里需要实现一个IOAudioDevice 的实例。首先自定义一个SoundflowerDevice类继承IOAudioDevice。然后再定义一个类SoundflowerEngine继承IOAudioEngine。

2.1、 SoundflowerDevice(IOAudioDevice),这个主要是提供和core audio进行交互的方法。同时也提供硬件初始化相关方法,以及实例化IOAudioEngine.通常IOAudioDevice 执行以下相关操作:

  • 配置硬件设备的提供者,枚举任何需要的资源。对于PCI或Thunderbolt来说,是映射设备内存或I/O区。对于USB来说,则是枚举接口 和管道。
  • 配置设备,以进行操作。例如,通过访问设备的奇存器或发送控制请求,消除设备的重置/睡眠模式。
  • 如果你的驱动程序支持多个音频芯片,或带有各种DMA信道、输入或输出的芯片,驱动程序将需要询问设备,以得知其具体功能。
  • 设置音频设备的名称及描述,以便Core Audio和用户空间应用程序能够识别。
  • 根据从设备提取出来的信息,创建适当数量的IOAudioEngine实例,这些实例接着分配一个或多个IOAudiostream实例以及相关的采样缓冲区。

因为篇幅有限这里只对SoundflowerDevice重要部分源码进行解释。

    virtual bool initHardware(IOService *provider);
    virtual bool createAudioEngines();
    virtual bool initControls(SoundflowerEngine *audioEngine);
    static  IOReturn volumeChangeHandler(IOService *target, IOAudioControl *volumeControl, SInt32 oldValue, SInt32 newValue);
    virtual IOReturn volumeChanged(IOAudioControl *volumeControl, SInt32 oldValue, SInt32 newValue);
    static  IOReturn outputMuteChangeHandler(IOService *target, IOAudioControl *muteControl, SInt32 oldValue, SInt32 newValue);
    virtual IOReturn outputMuteChanged(IOAudioControl *muteControl, SInt32 oldValue, SInt32 newValue);
    static  IOReturn gainChangeHandler(IOService *target, IOAudioControl *gainControl, SInt32 oldValue, SInt32 newValue);
    virtual IOReturn gainChanged(IOAudioControl *gainControl, SInt32 oldValue, SInt32 newValue);
    static  IOReturn inputMuteChangeHandler(IOService *target, IOAudioControl *muteControl, SInt32 oldValue, SInt32 newValue);
    virtual IOReturn inputMuteChanged(IOAudioControl *muteControl, SInt32 oldValue, SInt32 newValue);

首先,以上代码明显缺少了常见I/OKit相关生命周期Start(),Stop()方法。其实这主要是因为它的父类IOAudioDevice 中已经实现了。Start()负责注册电源管理然后调用initHardware()方法。

  • initHardware:它执行硬件相关的初始化方法,在它返回之前会通过调用 createAudioEngines方法创建所需IOAudioEngine实例。(SHB)因为这里没有实际硬件,所以这个方法里面并没有处理过多任务。这里只有设置设备名称简称和制造商名称。
  • createAudioEngines:这个是创建IOAudioEngine实例并激活它们.
bool SoundflowerDevice::createAudioEngines()
{
    //从info.plist中读取相关引擎配置
    OSArray* audioEngineArray = OSDynamicCast(OSArray, getProperty(AUDIO_ENGINES_KEY));
    //遍历器
    OSCollectionIterator* audioEngineIterator;
    //引擎字典,存放实例化相关AudioEngine
    OSDictionary* audioEngineDict;
    if (!audioEngineArray) {
        IOLog("SoundflowerDevice[%p]::createAudioEngine() - Error: no AudioEngine array in personality.\n", this);
        return false;
    }
audioEngineIterator = OSCollectionIterator::withCollection(audioEngineArray);
    if (!audioEngineIterator) {
IOLog("SoundflowerDevice: no audio engines available.\n");
return true;
}
    //遍历AudioEngines
    while ((audioEngineDict = (OSDictionary*)audioEngineIterator->getNextObject())) {
SoundflowerEngine* audioEngine = NULL;
        if (OSDynamicCast(OSDictionary, audioEngineDict) == NULL)
            continue;
audioEngine = new SoundflowerEngine;
        if (!audioEngine)
continue;   
        if (!audioEngine->init(audioEngineDict))
continue;
        //初始化注册对应audioEngine 相关声道,音量控制等功能和数据
initControls(audioEngine);
        //激活对应audioEngine
        activateAudioEngine(audioEngine); // increments refcount and manages the object
        audioEngine->release(); // decrement refcount so object is released when the manager eventually releases it
    }
    audioEngineIterator->release();
    return true;
}
  • initControls:注册音频控制,音频设备一般会有几个常用的方法,例如调整音量,静音及其他调整。为了使的客户端可以控制这些方法,这里有一个IOAudioControl的类来描述对应方法属性。它有3个子类分别是

1、IOAudioLevelControl,主要用于控制音量(Volume)和增益(Gain)

音量控制通过使用IOAudioLevelControl专用的工厂方法createvolumecontrol()创建。该方法的前3个参数分别表示初始音量值、最小值和最大值。可以指定不同的值,以匹配你的硬件寄存器规范,或者转换回调中的值, 以匹配硬件的音量控制奇存器预期的范围。接下来的两个参数设置最小值和最大值对应的dB值。音量标度通常从0.0 dB(表示最大音量)到某一 负的dB值。该音量的默认值 为0.0 dB,降低信号的音量时减小。dB值存储为定点值。下一个参数是声道ID。例如设置kIOAudioControlChannelIDDefaultleft,表示该控制用于左立体声道。其他声道对应声明在IOKit/audio/AudioDefines.h中。下-个参数是声道的描述性名称宇符串。类似于声道1D,我们使用一个预定义的常数。下个参数是一个标识符,驱动程序可以用来传递一个值。最后一个参数指定控制的用途。例如kIOAudioControlUsageOutput,对Core Audio来说,这表示一个输出音量控制。其他可能的值有kIOAudiocontrolUsageInput等。成功构建了一个控制之后,需要设置回调函数,它会在用户空间操作该控制时对其进行调用。该回调两数必须是一个静态成员函数。其实现可以看对应源码 volumeChangeHandler实现。

SoundFlower代码中对应channel,channelNameMap[channel] 是自己做了映射对应值。可以看源码前面的遍历查看对应值
核心代码(调节音量):
control = IOAudioLevelControl::createVolumeControl(SoundflowerDevice::kVolumeMax, // Initial value

                                                           0, // min value

                                                           SoundflowerDevice::kVolumeMax, // max value

                                                           (-40 << 16) + (32768), // -72 in IOFixed (16.16)

                                                           0, // max 0.0 in IOFixed

                                                           channel, // kIOAudioControlChannelIDDefaultLeft,

                                                           channelNameMap[channel], // kIOAudioControlChannelNameLeft,

                                                           channel, // control ID - driver-defined

                                                           kIOAudioControlUsageOutput);

        addControl(control, (IOAudioControl::IntValueChangeHandler)volumeChangeHandler);

2、IOAudioToggleControl,主要控制静音

类似上面音量控制
// Create an input mute control

    control = IOAudioToggleControl::createMuteControl(false, // initial state - unmuted

                                                      kIOAudioControlChannelIDAll, // Affects all channels

                                                      kIOAudioControlChannelNameAll,

                                                      0, // control ID - driver-defined

                                                      kIOAudioControlUsageInput);

    addControl(control, (IOAudioControl::IntValueChangeHandler)inputMuteChangeHandler);

3、IOAudioSelectorControl(完全废弃).

2.2、SoundflowerEngine(IOAudioEngine),在音频驱动程序中,音频引擎执行实际的I/0操作。音频引擎实现为抽象的IOAudioEngine类的子类。它控制I/O行为,处理一个或多个相关采样缓存区的传输。许多音频设备可以在同一时刻驱动多个独立的输入和输出,在这种情况下,建议创建多个IOAudioEngine实例,每个实例对应一个I/O通道。IOAudioEngine子类的实现,分为如下几个步骤。

  • 重载initHardware()方法,执行所需的硬件相关初始化。
  • 分配采样缓存区及实例化相关的IOAudioStream。
  • 实现performAudioEngineStart()和performAudioEngineStop()方法,启动或停止I/O。
  • 实现free()方法,回收资源释放相关内存。
  • 实现getCurrentSampleFrame()方法。
  • 实现performFormatChange()方法,响应Core Audio改变格式的请求。
  • 实现一种机制,通知采样缓存区置回开头的时间截。
  • 实现用于输出流的clipOutputSamples()方法和用于输入流的convertInputSamples()方法。

IOAudioEngine子类由Core Audio通过IOAudioEngineUserClient直接启动和停止。启动之后, 该引擎将通过采样缓存区连续运行。IOAudioEngine子类通过时间戳,告知超类该缓冲区何时重 置到缓存区的开头。Core Audio框架使用时间戳精确预测采样缓冲区的位置。音频引擎还可以确 保删除采样缓存区中已播放的采样。

这里是SoundflowerEngine.h中的实现,我们可以看到它多了一些方法,作用下面会解析
virtual bool init(OSDictionary *properties);

    virtual void free();    

    virtual bool initHardware(IOService *provider);

    virtual bool createAudioStreams(IOAudioSampleRate *initialSampleRate);

    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);

    static void ourTimerFired(OSObject *target, IOTimerEventSource *sender);
  • init(OSDictionary *properties),通过从info.plist 中读取一些值,初始化相关参数。
  • initHardware(IOService *provider),初始化硬件相关。为了便于理解源码下面先做部分讲解: initHardware中主要需要实现以下几个功能:
  1. 设置引擎相关采样率。采样率是IOAudioEngine的一个属性。因此,如果引擎处理多个流,则它们一定具有相同的采样率。
  2. 因为虚拟声卡没有实体设备,这里分配一个IOTimerEventsource,用来代替硬件模拟中断。使用setDescription()方法设置描述字符串。该字符串在某些地方对用户可见,包括在SystemPreferences的声音面板中。

在实现代码之前这里有几个概念需要先知道便于理解:

  • 缓存区中的采样数(使用setNumSampleFramesPerBuffer):例如SoundFlower 设置每个采样组长blockSize,采样缓存区总块数 numBlocks。这样计算就是 blockSize * numBlocks 这个数量的采样(采样缓存区可以设置为一块,这里SoundFlower设置为2)
  • 采样缓存区长度(使用setSampleBuffer):本虚拟声卡没有实体设备所以可以比较随意选择。但是在实际中如果有实际设备,则需要查看硬件的实际功能参数,通常情况下采样缓存区长度是可以配置的。根据上条采样数计算,通道数为channel.那么采样缓存区长度就是 blockSize * numBlocks * channel * maxBitWidth /8 。(maxBitWidth-采样Bit,一般是16 、24、 32 还有其他的可以看上篇基础。除8是每字节占8位 )
  • 设置输入/输出延迟(setInputSampleLatency,setOutputSampleLatency)或者统一设置(setSampleLatency), 音频在传输的时候,某些硬件设备可能会有其他缓存或延迟。
  • 设备中断率和中断时间(SoundFlower 设置是 kAudioInterruptHZ = 100, kAudioInterruptInterval 10 纳秒)这个如果在实际中就要由音频硬件决定。中断就是每隔一个固定时间会处理一次数据。
  • 为采样缓存区分配内存,虚拟声卡并不执行到硬件设备的DMA,因此只是使用IOMalloc()分配输入和输出缓存区。对基于硬件的驱动程序,需要为该缓存区分配IOBufferMemoryDescriptor,或创建一个独立的IOMemoryDescriptor,优先采用前者。然后,需要为DMA或I/O传输准备缓存区。对于DMA,需要将该缓存区的地址转换为物理地址,以便硬件读取或设置分散/聚集表,这些都可以使用IODMACommand 类实现。每个缓存区都需要关联到一个IOAudiostream,IOAudiostream负责协调客户端访问该缓存区。IOAudiostream实例通过createAudioStreams() 方法分配。
  • 采样缓存区的格式 - IOAudioStreamFormat。SoundFlower支持多个格式和采样率。
  • createAudioStreams,这段代码实在太长,根据上面描述核心的已经概括,可以自行查看源码进行理解。这段主要就是给输入输出IOAudioStream,设置采样缓存区、采样率、缓存区大小、音频格式等。
  • performFormatChange,IOAudioEngine 需要响应Core Audio的请求,改变音频流格式的方法。
  • getCurrentSampleFrame, 返回当前采样缓存区的具体位置(获取当前读到哪一个采样组(帧))。

注意getCurrentSampleFrame 这个方法也比较重要,计算错误会出现爆音,咔哒声,或声音失真等问题。因为返回缓存区当前位置,IOAudioEngine会根据这个数据删除已经播放的采样,如果错误会覆盖尚未播放的采样数据。

  • performAudioEngineStart,performAudioEngineStop,这2个方法是响应Core Audio 的指令启动和停止audioEngine。(注意这2个方法和IOService生命周期start()和stop()无关,这2个生命周期方法只在驱动程序初次加载和驱动程序卸载时调用。)performAudioEnginestart()方法应该执行两方面的工作,一方面,确保设备在硬件中开始播放或捕获,另一方面,确保采样缓冲区的初始时间戳通过takeTimestamp()参数数进行设置。performAudioEnginestop()将反向执行引擎启动时的操作,并禁用设备终端,以便设备不再从采样缓存区执行I/O,并将设备重置为准备再次运行的状态。
IOReturn SoundflowerEngine::performAudioEngineStart()

{
    //采样缓存区初始时间戳,因为是开始不需要存储循环计数fCurrentLoopCount
    takeTimeStamp(false);
    //采样缓存区计数(这里有一共2个),这里设置开始当前为第0个
    currentBlock = 0;
    //设置中断时间
    timerEventSource->setTimeout(blockTimeoutNS);
    uint64_t time;
    //计算出下一次的中断时间,并把它和这次时间进行比较
    clock_get_uptime(&time);
    absolutetime_to_nanoseconds(time, &nextTime);
    //下一次时间为当前时间+中断时间 (纳秒)
    nextTime += blockTimeoutNS;
    return kIOReturnSuccess;

}
IOReturn SoundflowerEngine::performAudioEngineStop()
{
    //IOLog("SoundflowerEngine[%p]::performAudioEngineStop()\n", this) 
    //取消计时,使设备不再从缓存区执行I/O
    timerEventSource->cancelTimeout();
    return kIOReturnSuccess;
}

剪裁和转换采样: 因为Core Audio(音频HAL)只能处理高精度32位浮点采样,所以在输出音频时,必须将音频采 样从浮点格式转换为硬件可以理解的格式(除非硬件本身支持),大多数音频硬件只处理整形采样。如果引擎有一个输出 IOAudiostream,则IOAudioEngine子类应该覆写 IOAudioEngine ::clipOutputSamples()方法。类似地,如果有一个输入IOAudiostream,则需要覆写IOAudioEngine::convertInputsamples()方法。这些方法负责将音频数据转换成本地格式,或从本地格式转换成其他格式并剪裁采样。剪裁是检查每个采样,确保其处于有效的范围内。例如,浮点采样的范围必须是-1.0~1.0. 较低或较高的值必领剪裁为最接近的有效值。 注意:如果你需要对音频数据进行某些处理,比如过滤某些声音,改变某些音频数据,这就是你最好的切入点。

  • clipOutputSamples,裁剪输出的音频采样
IOReturn SoundflowerEngine::clipOutputSamples(const void *mixBuf, void *sampleBuf, UInt32 firstSampleFrame, UInt32 numSampleFrames, const IOAudioStreamFormat *streamFormat, IOAudioStream *audioStream)
{
    // 声道数量
    UInt32 channelCount = streamFormat->fNumChannels;
    // 第一组也就是缓存区开始的偏移量
    UInt32 offset = firstSampleFrame * channelCount;
    //偏移的字节
    UInt32 byteOffset = offset * sizeof(float);
    //本次应该裁剪的采样总字节大小(计算参看前文)
    UInt32 numBytes = numSampleFrames * channelCount * sizeof(float);
SoundflowerDevice* device = (SoundflowerDevice*)audioDevice;
    //采样缓存区最后一组采样组数据的index
mLastValidSampleFrame = firstSampleFrame+numSampleFrames;
if (device->mMuteIn[0]) {
        //如果当前设备静音,则将需要转换的样本数据分配numBytes大小内存,数据用0填充
memset((UInt8*)mThruBuffer + byteOffset, 0, numBytes);
}
else {
        //当前设备正常播放,则将需要转换的样本数据,全部分配到新的numBytes大小内存中
memcpy((UInt8*)mThruBuffer + byteOffset, (UInt8 *)mixBuf + byteOffset, numBytes);
float masterGain = logTable[ device->mGain[0] ];
float masterVolume = logTable[ device->mVolume[0] ];
        //这个 logTable 我个人认为是声音0-100每个对应的一个数字值(有待考证)
//这里是遍历所有声道,将对应音量和声音增益,按照对应剪裁调整放入 mThruBuffer(传入传出硬件的数据缓存区),因为mThruBuffer是个一维数组,所以要将所有声道值的数据进行遍历传入
        //其中如果声道channelMute 对应值就为0      
for (UInt32 channel = 0; channel < channelCount; channel++) {
SInt32 channelMute = device->mMuteIn[channel+1];
float channelGain = logTable[ device->mGain[channel+1] ];
float channelVolume = logTable[ device->mVolume[channel+1] ];
float adjustment = masterVolume * channelVolume * masterGain * channelGain;
for (UInt32 channelBufferIterator = 0; channelBufferIterator < numSampleFrames; channelBufferIterator++) {
if (channelMute)
mThruBuffer[offset + channelBufferIterator*channelCount + channel] = 0;
else
mThruBuffer[offset + channelBufferIterator*channelCount + channel] *= adjustment;
}
}
}
return kIOReturnSuccess;
}
  • convertInputSamples,和裁剪相似,只是反向执行,注意转换输入采样,用UInt8 整型数据。原因上文已写
IOReturn SoundflowerEngine::convertInputSamples(const void *sampleBuf, void *destBuf, UInt32 firstSampleFrame, UInt32 numSampleFrames, const IOAudioStreamFormat *streamFormat, IOAudioStream *audioStream)
{
    //获取每组(帧)大小
    UInt32 frameSize = streamFormat->fNumChannels * sizeof(float);
    //采样缓存区开始的偏移
    UInt32 offset = firstSampleFrame * frameSize;
SoundflowerDevice* device = (SoundflowerDevice*)audioDevice;
//如果是静音就用0填充,如果正常播放就用 mThruBuffer 中数据进行填充(注意转换成UInt8 整型)
    if (device->mMuteOut[0])
        memset((UInt8*)destBuf, 0, numSampleFrames * frameSize);
    else
        memcpy((UInt8*)destBuf, (UInt8*)mThruBuffer + offset, numSampleFrames * frameSize);
    return kIOReturnSuccess;
}
  • 中断处理(SoundFlower方法 ourTimerFired)

在基于DMA设备的音频引擎中。设备将连续按间隔从缓存区中读取音频输出流,并向缓存区中写入音频输入流。DMA引擎启动之后,运行起来几乎不受任何干扰。但需要执行一个非常重要的任务,即通知IOAudioEngine采样缓冲区回绕到开始时间,并跟踪回绕次数。关键是时间戳要尽可能精确。音频HAL将使用该信息来跟踪采样缓冲区在任意给定时刻的位置。这非常重要,因为Core Audio不同于其他的音频体系结构,在一个I/O循环完成之后,它接收不到驱动程序的直接通知。Core Audio是依赖驱动程序的时间戳,预测采样缓冲区当前的位置。处理时间戳通过调用takeTimestamp()方法实现,这会把当前时间(单位是纳秒)存储至IOAudioEngine类的内部实例变量(fLastloopTime)中,并存储循环计数 (fCurrentloopcount )。在performAudioEnginestart(方法中,I/O开始之后便处理初始时间戳。你会发现,该方法传递了一个false参数,这会确保循环计数不增加,因为我们尚末完成某些循环。

void SoundflowerEngine::ourTimerFired(OSObject *target, IOTimerEventSource *sender)

{

    if (target) {

        SoundflowerEngine *audioEngine = OSDynamicCast(SoundflowerEngine, target);

UInt64 thisTimeNS;

uint64_t time;

SInt64 diff;

        

        if (audioEngine) {

// make sure we have a client, and thus new data so we don't keep on 

// just looping around the last client's last buffer!

            //获取当前audioEngine的输出流

            IOAudioStream *outStream = audioEngine->getAudioStream(kIOAudioStreamDirectionOutput, 1);

            if (outStream->numClients == 0) {

                // it has, so clean the buffer

                //当前输出流缓存区没有数据读取,将当前输出缓存区全部置0

                memset((UInt8*)audioEngine->mThruBuffer, 0, audioEngine->mBufferSize);

            }

            //当前已经完成的计数块+1

audioEngine->currentBlock++;

            //当前计数块数量>= 计数总块,说明这次中断已经完成

            if (audioEngine->currentBlock >= audioEngine->numBlocks) {

                //重制当前计数块 = 0

                audioEngine->currentBlock = 0;

                //把当前时间(单位纳秒)存储至IOAudioEngine类的内部实例变量(fLastLoopTime)中,并存储循环计数fCurrentLoopCount

                audioEngine->takeTimeStamp();

            }

            

            // calculate next time to fire, by taking the time and comparing it to the time we requested.

            //计算出下一次的中断时间,并把它和这次事件进行比较

            clock_get_uptime(&time);

            absolutetime_to_nanoseconds(time, &thisTimeNS);

// this next calculation must be signed or we will introduce distortion after only a couple of vectors

            //计算出两者的差值

diff = ((SInt64)audioEngine->nextTime - (SInt64)thisTimeNS);

            //中断设置,下一次中断的时间就是 blockTimeoutNS(中断常量 纳秒)+ 差值

            //为什么要加diff,是因为目前是在这个中断的开始所以要加上

            sender->setTimeout(audioEngine->blockTimeoutNS + diff);

            //预估audioEngine 下一次中断的时间

            audioEngine->nextTime += audioEngine->blockTimeoutNS;

        }

    }

}