大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 DriverKit 创建音频驱动程序
敬请探索如何使用 AudioDriverKit API 将您的音频服务器插件和 DriverKit 扩展整合到一个包中。了解如何使用 app 而不是安装程序包来简化音频驱动程序的安装,并通过 Mac App Store 分发驱动程序。我们将带您了解 Core Audio HAL 如何与 AudioDriverKit 交互并探索音频设备驱动程序的最佳实践。
资源
相关视频
WWDC22
WWDC20
WWDC19
-
下载
大家好 我叫卞善成 我的工作项目是Core Audio 今天我会介绍创建音频驱动的新方法 使用Driver Kit 首先来看音频驱动是怎么运作的 在macOS Big Sur之前 一个音频服务器插件 要跟硬件设备通信的话 得通过用户客户端连到内核扩展 在macOS Big Sur中 CoreAudio HAL提供支持 在DriverKit Extension上 创建音频服务器插件 插件和dext之间的层 与插件和kext之间的层相同 但通过移出内核并移入用户空间 安全性得到提高 关于DriverKit的更多信息 请查看先前的WWDC DriverKit视频 现行的解法下 允许音频驱动发展移出内核 不过我们仍然需要两个单独的组件 来执行具功能性的硬件音频驱动 那就是音频服务器插件以及驱动扩展 这会使开发复杂化 资源增加 并可能增加额外开销和迟延 在macOS Monterey启动的话 你只需要dext 不再需要插件 AudioDriverKit是DriverKit的新框架 用于和USBDriverKit或PCIDriverKit 一同写音频驱动扩展 这个新框架 处理所有往CoreAudio HAL的 进程间通信 由于你只有一个dext 你现在不用在 dext跟音频服务器插件之间沟通 你可以专注在DriverKit里
由于AudioDriverKit扩展 捆绑在Mac应用程序里 因此不再需要单独的安装程序
现在你可以立即加载驱动 不需重新启动
知道了AudioDriverKit的好处 我们来深入看看 如何打造新的音频驱动 我先做个简短介绍 聊聊音频驱动里相关的组件 接着看写dext要些什么
我们准备好写代码后 我会详细解说如何配置 如何初始化 如何创建设备、流、其他音频对象 还有如何处理IO路径和时间戳 最后我会谈谈如何处理配置更动 结尾时给大家展示
先从架构开始 这个图表展示 用AudioDriverKit框架时 HAL 如何与Driver Extension通信 AudioDriverKit框架 会创建一个私有用户客户端 这将用于Core Audio 和你的音频dext之间 所有的通信 我们不打算直接使用这个用户客户端 也不会暴露给你的dext 注意 应用程序和dext的通信 不需要插件或自定义用户客户端 或者有必要的话 应用程序可以开启自定义用户客户端 跟你的dext直接通信 现在来看看你会需要什么权限 所有的DriverKit驱动扩展 需DriverKit权限 AudioDriverKit dexts 也要有权限 这样用户客户端才能有取得路径 这适用于 已全获DriverKit权限的开发人员 此外 应根据需求 来加入通讯系列权限 如果你还没请求U盘 或PCI运输权限 可以前往Apple的开发部网站 然后提出请求 记住 你看到的示例代码纯为展示 并且这示例代码创了个虚拟音频驱动 这虚拟音频驱动跟硬件设备并无关联 因此在这种使用情况下 无法给予权限 如果只需要虚拟音频驱动或设备 那音频服务器插件驱动模型 应该被持续使用 现在来看看dext的info.plist 这些设定需要被加进 dext的IOKitPersonalities AudioDriverKit会处理 创建HAL所需的 IOUserAudioDriverUserClient 能跟用户连线接通的所需权限 HAL已经具备 这里展示了 SimpleAudioDriverUserClient 自定义用户客户端的例子 查看头文件 AudioDriverKitTypes.h 以获取更多信息 接着来聊聊配置 以及初始化 音频dext配置的第一步 为子类化IOUserAudioDriver 并且覆写虚拟方法 IOUserAudioDriver是IOService的子类 执行自定义行为所需的 任何IOUserAudio对象 我们把它子类化 配置后并加至IOUserAudioDriver 这个图表展示的 IOUserAudio对象概览 你之后会需要创建 SimpleAudioDriver 是IOUserAudioDriver的子类 也是dext的入口 SimpleAudioDriver 会创个SimpleAudioDevice 也就是IOUserAudioDevice的子类别 音频设备处理所有的起止IO相关消息 时间戳 以及配置更动 SimpleAudioDevice 会创多种IOUserAudioObjects 设备对象也会创建 OSTimerDispatchSources OSActions 并且实作音调发生器 来模拟硬件干扰以及IO IOUserAudioStream为设备所有 流会为了IO使用IOMemoryDescriptor 这会映射给HAL IOUserAudioVolumeLevelControl 是一个控制对象 使用标量或是分贝值 我们会使用控制值 在输入音频缓冲区应用增益 所有的IOUserAudioObjects 皆可有IOUserAudioCustomProperties SimpleAudioDevice 会创建一个自定义属性的例子 还有一个字符串 作为它的限定符以及数据值 我们来看看代码 SimpleAudioDriver 是IOUserAudioDriver的子类 Start、Stop、与NewUserClient 这些IOService类虚拟方法 驱动需要去进行覆写 StopDevice以及StopDevice 为IO相关的虚拟方法 来自IOUserAudioDriver HAL为了音频设备启动或停止IO时 这些都会被调用 我讲完设备、串流 以及其他音频对象后 会聊聊IO路径 这些例子显示 如何覆写NewUserClient 以建立用户客户端连结 委托进程想要跟dext连结时 NewUserClient会被调用 AudioDriverKit框架 会调用NewUserClient 在IOUserAudioDriver基类上 来处理创建HAL所需的用户客户端 CoreAudio HAL所需的 IOUserAudioDriverUserClient 将在此步骤创建 通过调用IOService Create 自定义用户客户端也会创建 IOService会从 加入的驱动扩展信息plist入口 建立用户客户对象 我们来看看如何覆写Start 并创建自定义IOUserAudioDevice对象 首先 调用超类的Start 接着分配SimpleAudioDevice 然后初始化一些必须的参数 初始化的设备 需要通过调用AddObject 来加入音频驱动 登录服务之后 驱动就准备就绪了 现在你的驱动已经初始化 接着来创建设备、流 以及其他音频对象 将IOUserAudioDevice子类化 以取得自定义行为
我们来创建输入流、音量控制 和自定义属性对象 SimpleAudioDevice的初始化方法 显示了如何配置设备 与创建多种音频对象 通过调用设备上的 SetAvailableSampleRates 还有SetSampleRate 设备的采样率相关信息得以被配置 创建一个 会被传给IOUserAudioStream的 IOBufferMemoryDescriptor 内存会被映射到CoreAudio HAL 以及被音频IO所使用 内存与DMA到硬件使用的IO内存 理想上应当相同 IOUserAudioStream 是通过指定输入流方向 传入上面创的IO内存描述符来创建的 流在发挥功能之前 需要在流上配置额外的东西 定义流格式的方式 是通过创建IOUserAudio StreamBasicDescriptions的格式列表 指定说明采样率、格式ID 以及其他所需的格式属性 通过传递上方声明的流格式列表 来设置可用格式 接着设置流的当前格式 最后 通过调用AddStream 将配置的流加至设备 现在来详细走一次 创建音量级别控制的流程 要创建音量控制对象 我们要调用以下这个方法 IOUserAudioLevelControl::Create 该控制是一个可设置的音量控制 初始电平设置为-6dB 范围为96dB 元素、作用域 还有控制类别 都需要指定说明 最后 将控制对象加至设备 通过将增益应用于输入流IO缓冲区 音量控制增益值将在IO路径中使用 现在来看如何创建自定义属性对象 以供设备使用 属性的地址 需提供给每一个自定义属性对象 以全球规格以及主要元素 来定义定制选择器的类型 接下来 通过提供属性和定义的地址 创建一个自定义的属性对象 自定义属性可进行设定 限定符以及数据值类型都是字符串 现在给限定符还有数据值 创建OSS字符串 然后设定在自定义属性上 最后给设备加上自定义属性 现在我们建好了音频对象 接着来聊聊IO GetIOMemoryDescriptor方法 会进行返回 创建一个流时 IOUserAudioStream使用的 IOMemoryDescriptor 会被传递给初始化方法 流也可用新的内存描述符来更新 内存会被映射到HAL上 并使用在音频IO上 理想情况下 流使用的内存描述符 应与DMA到硬件设备的 内存描述符相同 IOUserAudioClockDevice 是IOUserAudioDevice的基类 UpdateCurrentZeroTimestamp 和GetCurrentZeroTimestamp 这两个代码应被用于 操作硬件设备的时间戳 时间戳会被原子化操作 HAL会用采样时间-主机时间配对 来运作及同步IO 追踪硬件的时间戳至关重要 追得越精确越好 来看SimpleAudioDevice类 并聚焦在IO相关的方法上 HAL试图运行IO时 会从驱动调用StartIO和StopIO 从这个私有方法的呈现中可见 用IOTimerDispatchSource 和OSAction来模拟硬件中断 将会被用于输入IO缓冲区上 生成零时间戳 以及音频数据 由于这个例子并未抵触硬件设备 因此硬件干扰以及DMA 被计时器跟动作取代 HAL试图在设备上启动IO时 设备对象会调用StartIO 在硬件上启动IO的任何必要调用 都必须在这里完成 这之后StartIO必须在基类上进行调用 接着取得输入流的 IOMemoryDescriptor IOMemoryMap可通过调用 CreateMapping来创建 缓冲地址、长度、偏移量 会用于动作发生的handler中 以在IO缓冲区上生成音调 我们会调用StartTimers以进行配置 以及允许时间资源及动作生成时间戳 并填充输入音频缓冲 为了IOUserAudioDevice UpdateCurrentZeroTimestamp被调用 目的是原子化更新 采样时间-主机时间配对 根据mach_absolute_time 和设备配置的主机节拍 计时器源被允许使用 并设定醒来时间 ZtsTimerOccurred动作 会根据醒来时间被调用 因此新的时间戳可在设备上更新 虽然没有显示在这儿 但是示例代码也以类似的方式 更新了音调生成计时器以及动作 零时间戳动作启用时 通过调用GetCurrentZeroTimestamp 可从设备取得最后一个零时间戳值 如果这是第一个时间戳 则使用mach_absolute_time 来传递至计时器作为锚定时间 否则的话 会用每次缓冲的零时间戳期间 还有主机节拍来更新时间戳 调用UpdateCurrentZeroTimestamp 会更新设备的时间戳 如此HAL就能使用新的值 给下一个零时间戳 设定ZTS计时器之后的醒来时间
要模拟DMA 计时器动作进行时 音频数据会被写至输入IO缓冲区 首先 在startIO被调用时 检查被指定的输入内存映射是否有效 使用内存映射缓冲区长度以及流格式 来获得IO缓冲区样本的长度 由于流只支持 signed 16 bit pcm样本格式 我们要拿到缓冲住址以及偏移值 指定其为int16_t缓冲区指针 现在我们可以通过正弦波音的生成 填写输入IO缓冲区 首先将输入音量控制增益作为标量值 接着将所需样本数量放入loop循环 生成正弦波音 应用音量控制增益 接着循环遍历缓冲区 并根据信道数 将正弦波音填入IO缓冲区 同时也将绕回处理考虑进来 现在音频dext已进行配置 也可运作IO 下一步是处理配置更动 以更新设备以及IO相关状态 此处显示的设备方法 可用于请求及执行配置更动 对于IO 或是对于 IO结构音频设备状态更动的影响 驱动需要请求配置更动通过调动 RequestDeviceConfigurationChange HAL会停止任何运行中的IO PerformDeviceConfigurationChange 会在驱动上调动 只有在那个时候 音频设备可更新其IO相关状态 常见的情况 会是更新音频设备的现行采样率 或是更改现行的流格式 以响应硬件设备的更动 请见图表 图表显示了设备配置更动的系列事件 驱动应先请求配置更动 听众会收到HAL通知 设备即将开始更动配置 如IO仍在运作 会在设备上停下 设备当下的状态会被获取 PerformDeviceConfigurationChange 会被调用在驱动上 此时设备与硬件上的任何状态 皆允许驱动进行任何更动 一旦执行配置更动 设备的新状态会被获取 所有IO相关的状态 如IO缓冲区或采样率 都会更新 所有设备状态的更动 都会通知全部的客户听众 如果IO在配置更动前正在运作 那么IO会在设备上重新启动 最后 HAL会通知所有听众 告知配置更动已经结束 要模拟硬件自下而上的配置更动请求 就使用自定义用户客户端命令 来诱发dext上的采样率更动 RequestDeviceConfigurationChange 会通知HAL 关于音频设备上的配置更动请求 注意 更动信息可以是 任何一种OSObject 这个例子提供了自定义配置更动动作 以及作为Osstring的更动信息 要处理执行配置更动 SimpleAudioDevice类需要的是 覆写PerformDeviceConfigurationChange 的方法 在PerformDeviceConfigurationChange里 在switch语句中处理配置更改动作 在请求配置更改时 记录作为更改信息所提供的 相同的OSString对象 接着我们要拿到现行采样率 然后在设备上设定新的数字 要确保的是 音频流更新自己当下的流格式 在流对象上 通过调用DeviceSampleRateChanged 来处理采样率更动 其他设备间接处理的配置更改动作 可以被传递至基类 我们在Mac上看一下 SimpleAudio 是捆绑驱动扩展的示例代码应用 要安装音频驱动扩展 只要点击安装驱动 就会跳出安全性偏好设置的选项 点击允许的话 就会自动载入音频驱动扩展 先前在dext上行不通 因为需要进行重启 SimpleAudioDevice 有采样率格式可用 还有音调选择数据源 还有我们加进示例代码的音量控制 现在我们可以打开QuickTime 在音频设备上进行音频录制
要从下而上测试配置更动的话 我们可以直接跟dext通讯 来开关切换音调频率或是采样率 这些更动 也应在Audio MIDI设置里反映出来 不用驱动扩展时 只要删除应用
即从Audio MIDI里消失 总结一下 我们重点回顾了音频服务器插件 跟DriverKit扩展 这些会持续被支持 AudioServerPugIn驱动界面仍可使用 以上介绍新的AudioDriverKit框架 并讨论了新版驱动模型带来的好处 我举了例子深入介绍 关于如何采用AudioDriverKit框架 也展示了示例代码 来创建以音频dext为基础的 IOUserService 这些都在用户空间里运作 下载最新的Xcode以及DriverKit SDK 对于具DriverKit支持的 硬件设备系列的音频设备 采用AudioDriverKit 如有关于AudioDriverKit的反馈 请提供给Apple的反馈助理 谢谢 [欢快音乐]
-
-
5:58 - Subclass of IOUserAudioDriver
// 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;
-
6:27 - Override of IOService::NewUserClient
// 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; }
-
7:04 - SimpleAudioDevice::init
// 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()); ...
-
8:19 - SimpleAudioDevice::init continued
// 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());
-
8:50 - Create a input volume level control object
// 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());
-
9:22 - Create custom property
// 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());
-
10:47 - Subclass of IOUserAudioDevice
// SimpleAudioDevice example subclass of IOUserAudioDevice class SimpleAudioDevice: public IOUserAudioDevice { ... virtual kern_return_t StartIO(IOUserAudioStartStopFlags in_flags) final LOCALONLY; virtual kern_return_t StopIO(IOUserAudioStartStopFlags in_flags) final LOCALONLY; private: kern_return_t StartTimers() LOCALONLY; void StopTimers() LOCALONLY; void UpdateTimers() LOCALONLY; virtual void ZtsTimerOccurred(OSAction* action, uint64_t time) TYPE(IOTimerDispatchSource::TimerOccurred); virtual void ToneTimerOccurred(OSAction* action, uint64_t time) TYPE(IOTimerDispatchSource::TimerOccurred); void GenerateToneForInput(size_t in_frame_size) LOCALONLY; }
-
11:18 - StartIO
// 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; }
-
11:57 - StartTimers
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); ... }
-
12:27 - ZtsTimerOccurred
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); }
-
13:03 - GenerateToneForInput
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()); ... }
-
13:30 - GenerateToneForInput continued
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; } }
-
14:02 - IOUserAudioClockDevice.h and IOUserAudioDevice.h
// 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);
-
15:32 - HandleTestConfigChange
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; }
-
16:05 - PerformDeviceConfigurationChange
// 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; }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。