目录

Wwise SDK 2018.1.11
Low-Level I/O
Default Streaming Manager Information

Low-Level I/O 是高级 Stream Manager API 默认实现的一个子模块,用于为 I/O 传输提供比实现高级 Stream Manager API 更简单的接口,因此只有在 Stream Manager 默认实现的环境中才适用。

简介

Low-Level I/O 系统是特别针对 Audiokinetic 厂商实现的 Stream Manager,其接口于 <Wwise Installation>/SDK/include/AK/SoundEngine/AkStreamMgrModule.h 中定义。

Low-Level I/O 系统有两个目的:

  • 解析文件位置。
  • 将实际 I/O 传输抽象化。

实现 AK::StreamMgr::IAkFileLocationResolver 的唯一对象被称为 File Location Resolver ,必须将其注册至 Stream Manager (使用 AK::StreamMgr::SetFileLocationResolver())。每次创建标准流或自动流时,Stream Manager 都会调用 AK::StreamMgr::IAkFileLocationResolver::Open()。 此方法必须返回有效的文件描述符(AkFileDesc),其中除文件大小和其他信息外,还包含用于播放此文件的高级流播放设备的 ID。 流播放设备通过 AK::StreamMgr::CreateDevice() 在 Stream Manager 中创建和注册。

必须为每个流播放设备提供一个 Low-Level I/O 挂钩。I/O 挂钩实现下列两个接口之一:AK::StreamMgr::IAkIOHookBlockingAK::StreamMgr::IAkIOHookDeferred。每当流播放设备需要执行 I/O 传输时,将使用 Low-Level I/O 挂钩的 Read() 还是 Write() 方法。 高级 Stream Manager 不会直接调用平台 I/O API。File Location Resolver 和 I/O 挂钩共同构成 Low-Level I/O 系统。与存储设备限制、文件和平台 I/O 相关的一切事务都由 Low-Level I/O 系统处理。

下图从默认 Stream Manager 角度描述了 Low-Level I/O 系统接口。

LowLevelIO.gif

游戏需要实现 Low-Level I/O 接口来解析文件位置,执行实际的 I/O 传输。在游戏中集成 Wwise 声音引擎 I/O 管理的最简单高效的方式,是使用默认 Stream Manager 并实现 Low-Level I/O 系统。 在此,您可以执行自带文件的读取操作,或者使用自己的 I/O 管理技术来处理 I/O 请求。

Wwise SDK 包含默认的 Low-Level I/O。可以直接使用,也可以此为基础来实现您自己的 I/O 管理。请参阅 默认实现示例纵览 一节了解 Low-Level I/O 示例的概述。

文件位置解析

File Location Resolver( File Location Resolver )

File Location Resolver 需要使用 AK::StreamMgr::SetFileLocationResolver() 注册至 Stream Manager。 当 Stream Manager 创建流对象时,会调用此实例的 Open() 方法。 AK::StreamMgr::IAkFileLocationResolver::Open() 必须填充文件描述符结构 (AkFileDesc) 的各字段。 Low-Level I/O 中的文件数量与同一时间 Stream Manager 中流对象数量相同。

游戏必须使用 AK::StreamMgr::CreateDevice() 在 Stream Manager 中至少创建一个流播放设备。 还可以根据需要创建所需数量的设备。每个流播放设备在独立的线程中运行,并将 I/O 请求发送到各自的 I/O 挂钩。通常应为每个物理设备创建一个流播放设备。 在 AkFileDesc 结构中,有 deviceID 字段。File Location Resolver 需要将其设置为已有流播放设备的 deviceID 之一。这会将文件处理操作指定到相应设备。另外,必须返回文件大小和偏置(uSector),并且应创建系统文件句柄(虽然并非必须)。

文件说明

Stream Manager 的客户端使用字符串(const AkOSChar *)、ID(AkFileID,整型)或文件描述符来识别文件。这是 Stream Manager 的流创建方法 (AK::IAkStreamMgr::CreateStd(), AK::IAkStreamMgr::CreateAuto()) 有两个重载的原因。File Location Resolver 负责将文件名称和文件 ID 映射到文件描述符,因此 AK::StreamMgr::IAkFileLocationResolver::Open() 具有两个重载。文件描述符用于标识所有底层传输和信息查询的数据源。

AK::StreamMgr::IAkFileLocationResolver::Open() 必须填充文件描述符。每次调用 Low-Level I/O 挂钩的方法时都会传回此文件描述符。高级 Stream Manager 使用该结构的三个成员:

  • deviceID:须为调用 AK::CreateDevice() 所获得的有效设备 ID。Stream Manager 用其将文件关联到正确的高级设备。一旦建立关联,将通过创建设备时传递的 I/O 挂钩执行 I/O 传输。
  • uSector:所述文件开头的偏置。是相对于 AkFileHandle AkFileDesc::hFile 所标识文件开头的偏置。它采用块(扇区)表示,块大小即 AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() 为此文件描述符所返回的值。 当流播放设备调用 I/O 传输方法时,会将文件开头的偏置(单位:字节)作为 AkTransferInfo 结构的一部分发送给其 Low-Level I/O 挂钩。偏置的计算方式是:Current_Position + ( AkFileDesc::uSector * Block_Size)。注意块大小也通过此挂钩查询,为每个文件描述符各查询一次。
  • iFileSize:Stream Manager 用其来检测文件结尾。然后通过 AK::IAkStdStream::GetPosition(), AK::IAkAutoStream::GetPosition() 向用户报告,并停止自动流的 I/O 传输。

其余的成员归 Low-Level I/O 系统所独自占有。例如,Win32 中类型定义为 HANDLE 的 AkFileHandle 可用于容纳传递到 Win32 ReadFile() 的实际有效文件句柄。它还可以用作 ID 或者指针。高级设备不可修改或读取文件描述符字段。因此,如果冗余文件存在于游戏磁盘中,Low-Level I/O 可以随意地关闭和重新打开文件句柄。

Tip: 传递到 AK::StreamMgr::IAkFileLocationResolver::Open() 的文件描述符结构 in_fileDesc 的地址,在其相关流对象的整个生命期间保持不变。

延迟打开

AK::StreamMgr::IAkFileLocationResolver::Open() 必须填充文件描述符。当 io_bSyncOpen 为 True 时,意味着文件必须立即打开,其描述符必须包含有效的信息。然而,当 io_bSyncOpen 为 False 时, File Location Resolver 可以决定一直等到文件打开。为什么等待? 因为 AK::IAkStreamMgr::CreateStd()AK::IAkStreamMgr::CreateAuto() 在客户端的线程中调用 AK::StreamMgr::IAkFileLocationResolver::Open() 如果文件打开操作需要很长时间,延迟打开 I/O 线程可能是个好主意。例如,声音引擎在音频线程中创建流并阻塞 Open()。如果这需要很长时间,可能会听到音频因 Voice Starvation(声部干涸)而卡顿。

io_bSyncOpen 为 False 时,您可以选择也可以不选择立即打开文件。如果立即打开,需要填充 AkFileDesc 结构并设置标志为 True 来表明已经打开它。如果不立即打开,除设置 AkFileDesc 的 deviceID 字段外,无需执行任何操作。这非常重要:您需要告诉 Stream Manager 哪个设备的线程将处理此文件。文件一旦指派到设备,就不可更改。 对 deviceID 的进一步修改将被忽略。

Caution: 对于请求立即打开的文件,不可以延迟打开。换句话说,您不可以重设 io_bSyncOpen

如果延迟打开,此流对象将归属 AkFileDesc::deviceID 指定的流播放设备所有。 在从其线程中对 I/O 挂钩调用 Read() 前,它将再次调用 AK::StreamMgr::IAkFileLocationResolver::Open(),但这次 io_bSyncOpen 设为 True

Caution: 延迟打开文件有一定的代价。必须储存文件打开数据(标志和文件名),直到再次调用 AK::StreamMgr::IAkFileLocationResolver::Open()。没有必要时,应避免延迟打开文件。

文件系统标志

Stream Manager 的 AK::IAkStreamMgr::CreateStd()AK::IAkStreamMgr::CreateAuto() 方法接受指向 AkFileSystemFlags 结构的指针,此指针传递到 AK::StreamMgr::IAkFileLocationResolver::Open()。此结构是用户直接传递信息到 Low-Level I/O 的一种方式。例如,此信息可以用于完成文件位置逻辑。Stream Manager 的一般用户可以传递 NULL,但声音引擎总是传递含有相关信息的结构,以告知 File Location Resolver 该请求来自声音引擎。

文件系统标志结构包含以下字段:

  • uCompanyID:声音引擎总是设置此字段为 AKCOMPANYID_AUDIOKINETIC,因此 Low-Level I/O 的实现者知道此文件须由声音引擎读取。 在流播放 External Source(外部源)时,声音引擎将传递 AKCOMPANYID_AUDIOKINETIC_EXTERNAL。
  • uCodecID:此字段可用于辨别文件类型。它将告知声音引擎使用的文件类型。声音引擎使用的编解码器 ID 在 AkTypes.h 中定义。主机程序可以使用与其中相同的值来定义它自己的 ID,只要不将 companyID 设为 AKCOMPANYID_AUDIOKINETIC 或 AKCOMPANYID_AUDIOKINETIC_EXTERNAL 就可以。
  • bIsLanguageSpecific:此字段指示查找的文件是否针对当前语言。通常,针对特定语言的文件存储在不同的位置。Low-Level I/O 需要根据当前选择的语言解析路径。请参阅 多语言专用的(“语音”和“混合”) SoundBank 了解更多详情。
  • 自定义参数和大小:用于文件位置机制的、游戏特有的扩展名。例如,在文件描述符中可以存储扩展名。声音引擎总是传递 0。

解析声音引擎文件

声音引擎读取 SoundBank 文件和流播放音频文件。此小节解释 Low-Level I/O 如何能够将其标识符解析为实际文件。

在 Low-Level I/O 中将 AK::StreamMgr::IAkFileLocationResolver::Open() 中收到的 ID 映射至有效文件描述符时,存在多种不同的策略:

  • 实现并使用 ID 到文件名称的映射
  • 使用 ID 创建文件名字符串
  • 获取所有流播放音频文件(如使用 File Packager 应用程序生成的文件包——请参阅 Wwise Help 了解更多详情)拼接所得大文件的系统句柄,实现并使用从 ID 到文件描述符结构的映射。文件描述符结构定义该流播放音频文件在此大文件中的大小和位置偏置。

Low-Level I/O 的 SDK 示例使用两种不同的策略来解析文件位置。策略一(CAkFileLocationBase)将全局设置的路径拼接起来,创建可用于平台 fopen() 方法的完整路径字符串。策略二(CAkFilePackageLowLevelIO)则使用 File Packager 实用程序创建的文件包。文件包是将许多文件拼接在一起的大文件,其文件头指示每个原始文件的相对偏置。

File Location Resolver 的两种实现也被用作 SDK 的示例。它们使用不同的策略管理文件位置。本节末尾的代码示例详解中描述了这些策略。

请参阅 基本文件定位 了解有关 Low-Level I/O 的默认实现所用策略的描述。

请参阅 文件位置 了解有关使用文件包的 Low-Level I/O 实现(CAkFilePackageLowLevelIO)所用策略的描述。

SoundBank

在显式或隐式请求从主要 API(AK::SoundEngine::LoadBank()AK::SoundEngine::PrepareEvent())加载库后, SoundBank 的 Bank Manager 可以调用 AK::IAkStreamMgr::CreateStd() 的 ansi 字符串或 ID 重载。对于选择哪个重载的条件的详细说明,请参阅 从文件系统加载 SoundBank

在两种情况下,AKCOMPANYID_AUDIOKINETIC 都将用作文件系统标志中的公司 ID,而 AKCODECID_BANK 用作编解码器 ID。

声音引擎 API 的 LoadBank() 方法不会提供标志来指示 SoundBank 是否针对特定语言。解决方案要由您的 Low-Level I/O 来实现。 由于声音引擎不知道 SoundBank 的语言特性,因此它将调用 Stream Manager,其中 bIsLanguageSpecific 标志设为 True。如果 Stream Manager( Low-Level I/O)无法打开它,声音引擎将再次尝试,此次 bIsLanguageSpecific 标志设为 False。请参阅 多语言专用的(“语音”和“混合”) SoundBank 了解有关在 Wwise SDK 中使用特定语言的 SoundBank 的更多信息。

Tip: 如果您想避免让 Bank Manager 为非本地化库调用两次 Stream Manager,可以忽略 bIsLanguageSpecific 标志并直接打开正确位置的 SoundBank。只有您知道 SoundBank 的内容和位置。另外,传递给 AK::SoundEngine::LoadBank() 异步版本的 cookie 也将作为 in_pFlags->pCustomData 的值传递给 AK::StreamMgr::IAkFileLocationResolver::Open()。可以使用它来帮您确定是否应从特定语言的目录中打开 SoundBank。

流播放音频文件

流播放文件的参考将以整型 ID 形式存储在 SoundBank 中。对于准备流播放的转码音频文件,其真实文件路径可以在与 SoundBank 一起生成的 SoundBanksInfo.xml 文件中找到(请参阅 SoundBanksInfo.xml 了解详情)。

Tip: 在 Wwise 的 SoundBank 设置中选择 “Copy streamed files” 将自动复制 Generated SoundBanks 特定平台文件夹中的文件,并按照 [ID].[ext] 方案重新命名。 请参阅 流播放音频文件 了解更多信息。 默认文件位置实现(CAkFileLocationBase)将结合 “Copy streamed files” 选项一起使用。

当声音引擎要流播放音频文件时,它会调用 AK::IAkStreamMgr::CreateAuto() 的 ID 重载。由上及下,将使用 AK::StreamMgr::IAkFileLocationResolver::Open() 的 ID 重载。将与 ID 一起传递 AkFileSystemFlags 结构及以下信息:

  • uCompanyID 是 AKCOMPANYID_AUDIOKINETIC
  • uCodecID 是 AkTypes.h (AKCODECID_XXX) 中定义的音频格式之一。
  • bIsLanguageSpecific 标志。当需要在当前游戏语言位置中搜索文件时,此标志为 True,否则为 False。

I/O 传输接口

文件位置一旦被解析,Stream Manager 就会将文件描述符交给适当的流播放设备,然后设备通过其 I/O 挂钩与 Low-Level I/O 系统交互。它将首先调用 AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize(),查询底层块大小限制。 然后各输入数据传输将通过 Read() 方法执行,并通过挂钩的 Write() 方法输出。 流被销毁时,将调用 AK::StreamMgr::IAkLowLevelIOHook::Close()

上述各方法将收到由 File Location Resolver 定义的相同文件描述符。

高级设备特性

Stream Manager 的当前实现定义了两种流播放设备,进而定义了两种 I/O 挂钩。其中之一将使用同步握手( AK::StreamMgr::IAkIOHookBlocking )与 Low-Level I/O 交换信号,另一个将使用异步握手( AK::StreamMgr::IAkIOHookDeferred )。这两个接口都是从 AK::StreamMgr::IAkLowLevelIOHook 继承而来。

AK::StreamMgr::CreateDevice() 收到名为 AkDeviceSettings 的结构,其中包含设备全局初始化设置。其中一个是 AkDeviceSettings::uSchedulerTypeFlags ,用于指定高级 I/O 调度程序的类型。如果您想创建一个阻塞设备来使用 AK::StreamMgr::IAkIOHookBlocking ,可以将其设置为 AK_SCHEDULER_BLOCKING,如果想创建一个延迟设备来使用 AK::StreamMgr::IAkIOHookDeferred ,则可以将其设置为 AK_SCHEDULER_DEFERRED_LINED_UP。适当的 Low-Level I/O 挂钩必须传递给 AK::StreamMgr::CreateDevice()

Caution:AK::StreamMgr::IAkIOHookDeferred 传递给 AK_SCHEDULER_BLOCKING 设备或者反向传递是违法的,创建时将悄无声息地发生故障,并在运行时崩溃。

AK::StreamMgr::CreateDevice() 返回由 File Location Resolver 在文件描述符结构中设置的设备 ID。

下面两节分别介绍阻塞和延迟 I/O 挂钩。

阻塞 I/O 挂钩

当指定 AK_SCHEDULER_BLOCKING 标志时,Stream Manager 创建通过 AK::StreamMgr::IAkIOHookBlocking 接口与 Low-Level I/O 系统交互的流播放设备。

阻塞接口比延迟接口简单。它定义两种方法,分别是 AK::StreamMgr::IAkIOHookBlocking::Read()AK::StreamMgr::IAkIOHookBlocking::Write(),均仅当 I/O 传输结束时才返回。

Caution: 请勿将 Low-Level I/O 级的同步 I/O 传输与 Stream Manager 级(甚至在 AK::SoundEngine::LoadBank() 中的声音引擎级)的同步流存取混为一谈。它们之间毫不相关。如前文所说,流播放设备在其专有的线程中运行。此线程将 Low-Level I/O 传输与高级 IAkStdStream 和 IAkAutoStream 存取隔离。因此阻塞型 Low-Level I/O 挂钩会处理 Stream Manager 及其延迟型挂钩的非阻塞流存取,反之亦然。

AK_SCHEDULER_BLOCKING 设备将根据设备实现中编写的算法来选择应优先服务哪个高级流对象。然后它将使用与此流对象相关的文件描述符来调用 Read() 或 Write()。它还传递一个 AkIOTransferInfo 结构(其中指定启动传输的文件位置和请求的传输大小)和一个指向要写入或从中读取数据的缓冲区位置的指针。另外,它还传递针对此传输的启发式算法。传输启发式算法由结构 AkIoHeuristics 定义,包含截止时间(毫秒)以及 AK_MIN_PRIORITY 和 AK_MAX_PRIORITY 之间的优先值。流播放设备的调度程序会将具有最短截止时间和最高优先级的传输请求发送给 Low-Level I/O。

Note:

在 Read() 或 Write() 内,数据传输必须完整执行。如果传输成功完成,函数应返回 AK_Success。否则应返回 AK_Fail。

延迟型 I/O 挂钩

当指定了 AK_SCHEDULER_DEFERRED_LINED_UP 标志时,Stream Manager 创建的流播放设备将通过 AK::StreamMgr::IAkIOHookDeferred 接口与 Low-Level I/O 系统交互。

延迟型接口比阻塞型接口复杂。它将用于结合 Low-Level I/O 实现一起,同时处理多个传输请求。它定义 3 种方法,分别是 AK::StreamMgr::IAkIOHookDeferred::Read()、AK::StreamMgr::IAkIOHookDeferred::Write() 和 AK::StreamMgr::IAkIOHookDeferred::Cancel()。Read() 和 Write() 应立即返回结果,并通过回调函数通知流播放设备传输已完成。在初始化设置中指定允许流播放设备发送到 Low-Level I/O 的并行 I/O 传输的最大数量(AkDeviceSettings::uMaxConcurrentIO)。

如果您传递 AK_Fail(或除 AK_Success 外的任何内容)给回调函数,或者 AK::StreamMgr::IAkIOHookDeferred::Read()/Write() 返回 AK_Fail,则流将被销毁,且传输日志中将出现 “I/O error” 通知。

Caution: 如果 AK::StreamMgr::IAkIOHookDeferred::Read()(或 Write())返回 AK_Fail,则流播放设备将不会等待回调函数,因此您应调用它。

Tip: 同时处理大量请求时,AK_SCHEDULER_DEFERRED_LINED_UP 设备将帮助您的实现取得更高的性能。如果不是这样,则应该使用 AK_SCHEDULER_BLOCKING 设备。 例如,使用 Win32 OVERLAPPED ReadFile() 代替阻塞 ReadFile 不会提高性能。如前文所说, ReadFile() 已经由 I/O 线程将其与 Stream Manager 接口隔离。

另一方面,延迟型设备也有一些缺点。它们占用的内存更多,I/O 传输被刷新或取消的可能性更大,因为它们发布的时间比阻塞型设备早。例如,调度程序可以同时为流发布多个传输,如果声音引擎调用 SetPosition(),所有未处理的传输在完成时都将被刷新。

Note: AkDeviceSettings::uMaxConcurrentIO 代表设备可能向 Low-Level I/O 发布的传输请求的最大数量。否则 AK_SCHEDULER_DEFERRED_LINED_UP 设备的调度程序将完全像 AK_SCHEDULER_BLOCKING 调度程序一样工作:只有当 Stream Manager 的客户端调用 AK::IAkStdStream::Read()/Write() 时,或者正在运行的自动流的缓冲低于缓冲目标(AkDeviceSettings::fTargetAutoStmBufferLength,请参阅 Audiokinetic Stream Manager 初始化设置 了解有关目标缓冲长度的更多详情)时,它才会决定发布传输请求。

流播放设备将 AkAsyncIOTransferInfo 结构传递给 Read() 和 Write()。此结构将在前述 AkIOTransferInfo 结构基础上增加传输完成时需要的回调函数信息。另外,缓冲区的地址也包含在此结构中,而不是单独传输到 Read() 或 Write()。另提供 pUserData 字段,帮助实现者将元数据附加至待处理的 I/O 传输。AkAsyncIOTransferInfo 结构将一直存活至调用回调函数为止。在调用回调函数后,不得再引用它。

AkIoHeuristics 结构也将被传递给 Read() 或 Write()。如果使用 Read() 或 Write() 调用您自己的 I/O 流技术,该结构包含的信息对于打乱 I/O 请求顺序可能会很有用。

Tip: 默认 Stream Manager 调度程序的实现基于“客户端启发式算法”,而非“磁盘带宽启发式算法”。Stream Manager 不知道文件在磁盘上的布局。如果您自己的流技术允许,它可以使用这一信息来对 I/O 请求重新排序,来尽量减少磁盘寻址。

有时候流播放设备需要刷新数据。当 Stream Manager 的客户端调用 AK::IAkAutoStream::SetPosition() 或更改循环启发式算法时可能发生这种情况。有时候数据甚至需要在相应传输完成前进行刷新。当 AkDeviceSettings::uMaxConcurrentIOAkDeviceSettings::fTargetAutoStmBufferLength 较大时,刷新的可能性就更大。当发生这种情况时,延迟型 I/O 挂钩提供获得通知的途径:Cancel()。当流播放设备需要刷新与 Low-Level I/O 中仍待处理的 I/O 传输相关的数据时,它将在内部把该传输标记为“已取消”,然后调用 AK::StreamMgr::IAkIOHookDeferred::Cancel(),等待调用回调函数。Cancel() 仅用于通知 Low-Level I/O,它可能执行操作,也可能不会。流播放设备知道哪些传输需要取消,所以,如果让其正常完成而不取消,它们一完成就会被刷新。在所有情况下都必须调用回函数据,通知流播放设备可以随意处理 I/O 传输信息和缓冲区。

Caution: 请确保一定不要为给定的传输调用回调函数两次。
Tip:
  • 如果在 Low-Level I/O 中建立了队列,则您可以使用 Cancel() 解散请求队列。如果能够解散队列,那么您就可以直接在 Cancel() 中调用回调函数。
  • 您不应在 Cancel() 中阻塞物理设备控制器。这可能阻塞 Stream Manager 的客户端。
Caution: 对于已被取消的传输,在调用其回调函数时,必须传递 AK_Success。其他任何操作都将被视为 I/O 错误,相关流将被终止。
Caution: AK::StreamMgr::IAkIOHookDeferred::Cancel() 可以从任何线程中调用。因此,如果您实现 AK::StreamMgr::IAkIOHookDeferred::Cancel(),在 Low-Level I/O 中必须非常小心锁定。具体而言,您需要避免从 Cancel() 和从正常 I/O 完成代码路径回调 pCallback 之间发生竞争。 有关更多详情,可从函数说明中找到。
Tip: 请不要仅出于这一原因就实现 AK::StreamMgr::IAkIOHookDeferred::Cancel()。由于锁定问题,有时候试图取消请求会比让它们正常完成可能需要付出更大的代价。

其他考虑因素

块大小(GetBlockSize())

如前所述,Stream Manager 的用户必须考虑 Low-Level I/O 对于允许传输大小的限制。最常见的限制是这些大小必须是某个值的倍数。此基础值是 AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() 为给定文件描述符所返回的值。例如,在 Windows® 中打开的带 FILE_FLAG_NO_BUFFERING 标志的文件,必须使用扇区大小数倍的缓冲区大小进行读取。 AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() 方法返回扇区大小。另一方面,如果 Win32 文件打开后不带该标志,AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() 应返回 1,从而不对 Stream Manager 的客户端加以限制。

Caution: AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() 决不能返回 0。
Tip: 处理底层块大小限制的重任将交给 Stream Manager 的客户端。 块大小值较大时将导致声音引擎浪费更多的流数据。除非平台的 I/O 系统具有特定的对齐限制或者可以极大地改善 I/O 带宽性能,否则您应使用 1 作为底层块大小。

性能分析

AK::StreamMgr::IAkLowLevelIOHook::GetDeviceDesc() 在 Wwise 中用于执行性能分析。Low-Level I/O 的默认实现提供的信息就是在 Wwise 中进行性能分析时看到的实际内容。

AK::StreamMgr::IAkLowLevelIOHook::GetDeviceData() 与此相似,但是会在每个性能分析帧调用它。 其返回值显示在 Streaming Device 选项卡的 Custom Parameter 列。

默认实现示例纵览

Wwise SDK 附带有 Low-Level I/O 的默认实现。它们位于 samples/SoundEngine/ 目录中。

类概述

下图是 Low-Level I/O 示例及其与 Low-Level I/O API 关系的类图。

LowLevelIO_samples.gif

CAkDefaultIOHookBlocking 实现 File Location Resolver API (AK::StreamMgr::IAkFileLocationResolver) 和阻塞型 I/O 挂钩(AK::StreamMgr::IAkIOHookBlocking)。CAkDefaultIOHookDeferred 作用与它相同,不过实现的是延迟型 I/O 挂钩(AK::StreamMgr::IAkIOHookDeferred)。两个实现都可以在单设备 I/O 系统中单独使用。CAkDefaultIOHookBlocking::Init()CAkDefaultIOHookDeferred::Init() 两者都可以在 Stream Manager 中创建流播放设备,向它传递设备设置,并存储返回的设备 ID。唯一的区别在于,您需要将 AK_SCHEDULER_BLOCKING 调度程序类型传递到 CAkDefaultIOHookBlocking::Init() 才能成功地将其初始化,而需要将 AK_SCHEDULER_DEFERRED_LINED_UP 传递给 CAkDefaultIOHookDeferred::Init()

另外,这两个设备会在 Stream Manager 中将自己注册为唯一的 File Location Resolver 。 但是,这只有在 Stream Manager 中尚未注册 File Location Resolver 的情况下才可能。

下图是表示单设备 I/O 系统的方框图。“Low-Level IO” 即是实现 File Location Resolver API 以及 I/O 挂钩 API 之一的类。可以是以下任意示例类:

  • CAkDefaultIOHookBlocking
  • CAkDefaultIOHookDeferred
  • CAkFilePackageIOHookBlocking
  • CAkFilePackageIOHookDeferred
LowLevelIO_SingleDevice.gif

下面是您可以单独使用 CAkDefaultIOHookBlocking(无错误处理)对 I/O 系统进行初始化的方式。

// 创建 Stream Manager。
AkStreamMgrSettings stmSettings;
AK::StreamMgr::Create( stmSettings );
// 使用阻塞型 Low-Level I/O 握手机制创建流播放设备。
AkDeviceSettings deviceSettings;
CAkDefaultIOHookBlocking lowLevelIO;
// 如果 lowLevelIO 尚未定义,则初始化时会将其注册为 File Location Resolver ,并创建流播放设备。
lowLevelIO.Init( deviceSettings );

如果您想使用 CAkDefaultIOHookDeferred 设备,则可以将 CAkDefaultIOHookBlocking 替换为 CAkDefaultIOHookDeferred,并将 AK_SCHEDULER_BLOCKING 替换为 AK_SCHEDULER_DEFERRED_LINED_UP。

这两个设备中实现的 File Location Resolver 使用 CAkFileLocationBase 的服务,两台设备也将继承这些服务。有关 CAkFileLocationBase 中实现的文件位置策略,更多信息请参阅下面的 基本文件定位

请阅读以下 阻塞型 I/O 挂钩纵览延迟型 I/O 挂钩纵览 两节,了解有关默认阻塞型和延迟型 I/O 挂钩的实现的更多详情。

另外还有一个作为模板的类,可以为实现 AK::StreamMgr::IAkFileLocationResolverAK::StreamMgr::IAkLowLevelIOHook 的类(例如 CAkDefaultIOHookBlockingCAkDefaultIOHookDeferred)增添管理文件包的能力。这个类就是 CAkFilePackageLowLevelIO<>。文件包是使用 AK File Packager 实用程序创建的文件。请参阅下面的 示例 File Package Low-Level I/O 实现纵览 一节了解有关 Low-Level I/O 中文件包处理的更多信息。 CAkFilePackageIOHookBlockingCAkFilePackageIOHookDeferred 两个类分别增加了文件包管理功能中 CAkDefaultIOHookBlockingCAkDefaultIOHookDeferred 的具体定义。

如果您要使用多个设备实现 I/O 系统,就需要向 Stream Manager 注册单独的 File Location Resolver ,其任务是为适当的设备分派文件管理任务。SDK 提供实现此功能的画布:CAkDefaultLowLevelIODispatcher。请参阅 多设备 I/O 系统 了解有关多设备 I/O 系统的更多详情。

基本文件定位

声音引擎使用的文件会通过 ID(用于流播放音频文件和 SoundBank)或字符串(一般保留用于 SoundBank)打开。CAkDefaultIOHook[Blocking|Deferred]CAkFileLocationBase 继承而来,它提供设置全局路径的方法(SetBasePath()AddBasePath()SetBankPath()SetAudioSrcPath())。CAkDefaultIOHook[Blocking|Deferred]::Open() 的两个重载调用 CAkFileLocationBase::GetFullFilePath() 来创建完整的文件名,自带的文件打开函数可以使用该文件名。最前面是基本路径, 如果文件是 SoundBank,其后将添加 SoundBank 路径。如果是流播放音频文件,则添加音频源路径。对于这两种文件,如果其位置取决于当前语言,则会添加语言目录名称。

如果使用的是字符串重载,则会在此路径后附加文件名字符串。

在 ID 重载中,只解析 Audiokinetic 的文件 ID。使用 ID 重载的游戏需要根据它的 ID 映射机制来更改实现。CAkFileLocationBase 的映射方案如下:它根据文件 ID 创建字符串,然后附加取决于文件类型(使用编解码器 ID 指定)的扩展名。这与 SoundBank 的 “Copy Streamed Files” 生成后续步骤设置使用的流播放文件命名规则兼容。 请参阅 Wwise 帮助了解有关 SoundBank 设置的更多信息。

Note: 未选择 SoundBank 设置的 “Use SoundBank names” 选项时,Wwise 以 [ID].bnk 命名格式生成 SoundBank 文件。因此,默认的 Low-Level I/O 中将可以对使用 ID 的显式 SoundBank 加载(通过 AK::SoundEngine::LoadBank() 的 ID 重载)和从 AK::SoundEngine::PrepareEvent() 触发的隐式 SoundBank 加载进行正确的映射。选择了 “Use SoundBank names” 时,Wwise 将使用它们的原始名称生成 SoundBank 文件(bank_name.bnk)。隐式 SoundBank 加载和使用字符串的显式 SoundBank 加载将得到正确映射。但使用 ID 的显式 SoundBank 加载将不起作用,因为默认 Low-Level I/O 将尝试打开名为 [ID].bnk 的文件,而此文件根本不存在。

请参阅 使用 SoundBank 名称 了解从 SDK 角度开展的有关 “Use SoundBank names” 选项的讨论,或者参阅 Wwise 帮助文档来了解通用 SoundBank 设置。

同样,默认 Low-Level I/O 打开名称格式为 [ID].[ext] 的流播放音频文件。[ext] 是取决于音频格式的扩展名。可以告诉 Wwise 在 SoundBank 生成结束时,自动复制 GeneratedSoundBank 路径中文件名格式为 [ID].[ext] 的所有流播放音频文件(请参阅 流播放音频文件 和 Wwise 帮助)。

在获得完整文件路径后,CAkDefaultIOHook[Blocking|Deferred]::Open() 将使用系统 API(封装在特定平台的示例文件 AkFileHelpers.h 实现的助手中)来直接打开文件。

在游戏代码中,您可以使用上述 CAkFileLocationBase 的方法设置基本路径、 SoundBank 路径、音频源路径和特定语言路径。请参阅 默认底层 I/O 实现 的代码示例了解更多信息。

Tip: 声音引擎不知道何时需要从特定语言的目录中加载 SoundBank。 因此它总是调用 AK::IAkStreamMgr::CreateStd(),第一次调用时,它将 AkFileSystemFlags 的 bIsLanguageSpecific 标志设置为 True,如果第一次调用失败,则将该标志设置为 false。Low-Level I/O 的默认实现示例这样摸索尝试从当前特定语言目录打开文件,由于调用 fopen() 将失败,因此将影响效率,应避免发生这种情况。

应该总是重新实现 Low-Level I/O 来满足需您的求。如果您知道特定语言的 SoundBank 的名称,或者已定义专门名称来标识它们,则应在加载过程的早期从正确的文件夹中加载它们。

延迟打开的默认实现

请参阅 延迟打开 了解有关 io_bSyncOpen 标志的讨论。

所有默认 I/O 挂钩(阻塞型和延迟型)在初始化后都有一个标志 in_bAsyncOpen(请参阅 CAkDefaultIOHook[Blocking|Deferred]::Init())。如果为 true,则如果可能,文件将被异步打开。这意味着,如果 in_bAsyncOpen 为 true, AK::StreamMgr::IAkFileLocationResolver::Open() 的 io_bSyncOpen 为 false,则文件打开操作将延迟。在这种情况下,将只设置 AkFileDesc 结构的 deviceID 字段,并且方法会立即返回结果。

阻塞型 I/O 挂钩纵览

在所有平台上,CAkDefaultIOHookBlocking::Read()CAkDefaultIOHookBlocking::Write() 的实现都使用 AkIOTransferInfo 中提供的信息,对自带的文件读/写函数执行阻塞调用。 如果操作成功,函数返回 AK_Success。

延迟型 I/O 挂钩纵览

延迟型 I/O 挂钩的实现比阻塞型 I/O 挂钩稍复杂。一般而言,平台上的异步文件读取 API 需要将特定平台的结构传递给 fread()(在 Windows 中为 OVERLAPPED),并在 I/O 操作的整个持续时间内保留它,直至回调函数被调用来通知您 I/O 已经完成。

此实现在所有平台上都有相似之处。CAkDefaultIOHookDeferred 在其内存池中分配了一系列特定于平台的结构。当调用 CAkDefaultIOHookDeferred::Read() 时,它将查找第一个空闲的结构,将它标记为“已占用”,使用 AkAsyncIOTransferInfo 中提供的信息进行填充,然后将其传递给 fread()。它还传递一个本地静态回调函数,其签名与平台异步 fread() 函数兼容。此函数中会确定操作是否成功,释放特定于平台的 I/O 结构,并回调流播放设备。

从阵列中获取和释放特定于平台的 I/O 结构必须逐个进行,因为需要避免 Read()/Write() 与系统回调之间出现竞争情况。

有些平台会提供这样的服务,可以取消已经发送到内核的 I/O 请求。如果是这样,就会在 CAkDefaultIOHookDeferred::Cancel() 中调用它。在 Windows 下, CancelIO() 会取消给定文件句柄的所有请求。因此在调用此函数前,必须先查看参数 io_bCancelAllTransfersForThisFile 来确保流播放设备的确想取消给定文件的所有请求。如果不是这样,则 Cancel() 不会执行任何操作:只需等待请求完成。流播放设备知道需要放弃哪些请求。

Caution: 请勿取消流播放设备已经显式取消的请求。如果您取消,最终可能会输入无效或受损数据,这可能导致声音引擎崩溃。
Caution: CAkDefaultIOHookDeferred 使用自带的异步文件读取函数,仅提供用作实现您自己的延迟型 I/O 挂钩的画布。我们不建议您按原样使用它,因其性能不会比使用自带阻塞调用更高。事实上,性能甚至可能更低。延迟型 I/O 挂钩 一节中的注释中对此进行了说明。延迟型底层 API 的目的是让您使用自己的流技术更加高效地处理 I/O 请求。 使用延迟型 I/O 挂钩而非阻塞型 I/O 挂钩 可帮助您决定哪种挂钩更适合您。

多设备 I/O 系统

下图表示多设备 I/O 系统。

LowLevelIO_MultiDevices.gif

使用多个流播放设备时,您需要做的是将与设备 Low-Level I/O 挂钩不同的 File Location Resolver 实例化并加以注册。 此对象的目的是将文件分派到适当的设备, 确定由哪个设备处理哪些文件的策略由您定义, 可以使用 CAkDefaultLowLevelIODispatcher 作为画布。默认实现使用比较强硬的方法:要求每个设备都尝试打开文件,直至其中一个设备成功。因此这些设备还必须实现 AK::StreamMgr::IAkFileLocationResolver 接口。 可以是 SDK 中提供的示例:

  • CAkDefaultIOHookBlocking
  • CAkDefaultIOHookDeferred
  • CAkFilePackageIOHookBlocking
  • CAkFilePackageIOHookDeferred

下面是对多设备系统中的延迟型设备和文件包阻塞设备进行实例化的方式(虽然此示例没有实践意义)。

// 创建 Stream Manager。
AkStreamMgrSettings stmSettings;
AK::StreamMgr::Create( stmSettings );
// 创建并注册 File Location Resolver 。
CAkDefaultLowLevelIODispatcher lowLevelIODispatcher;
AK::StreamMgr::SetFileLocationResolver( &lowLevelIODispatcher );
// 创建延迟型设备。
CAkDefaultIOHookDeferred hookIODeferred;
AkDeviceSettings deviceSettings1;
deviceSettings1.uSchedulerTypeFlag = AK_SCHEDULER_DEFERRED_LINED_UP;
hookIODeferred.Init( deviceSettings1 );
// 将它添加到全局 File Location Resolver 。
lowLevelIODispatcher.AddDevice( hookIODeferred );
// 创建具有文件包管理功能的阻塞设备。
CAkFilePackageIOHookBlocking hookIOBlockingFP;
AkDeviceSettings deviceSettings2;
deviceSettings2.uSchedulerTypeFlag = AK_SCHEDULER_BLOCKING;
hookIOBlockingFP.Init( deviceSettings2 );
// 将它添加到全局 File Location Resolver 。
lowLevelIODispatcher.AddDevice( hookIOBlockingFP );
Tip: 仅当存在多个物理设备时才应使用多个设备。

示例 File Package Low-Level I/O 实现纵览

一般说明

CAkFilePackageLowLevelIO<> 类是默认 Low-Level I/O 挂钩的上层,作为其延伸,可以加载示例 File Packager 生成的文件(请参阅 File Packager 实用程序 )。 它使用更先进的策略将 ID 解析为文件描述符。文件包包括大量拼接文件(流播放音频文件和 SoundBank 文件),这些文件的文件头中包含与其相关的信息。

请参阅 文件包底层 I/O 实现 了解代码示例。您可以与对应的应用程序 File Packager 一起,“按原样”使用文件包 Low-Level I/O(CAkFilePackageLowLevelIO[Blocking|Deferred]),或者仅将它们视为实现高级文件位置解析方法的概念验证。

File Package Low-Level I/O 提供方法 CAkFilePackageLowLevelIO::LoadFilePackage(),其参数是使用 File Packager 示例生成的文件包名称。 它使用默认实现的服务打开,然后解析文件头并创建查询表。您可以加载任意数量的文件包。LoadFilePackage() 返回 ID,您可以结合 UnloadFilePackage() 使用此 ID 来卸载它。

CAkFilePackage 类包含已加载的文件包,以及用于处理文件查询表的所有数据结构和代码,文件查询表在 CAkFilePackageLUT 类中定义。 CAkFilePackageLowLevelIO<> 类覆盖了默认 I/O 挂钩的一些方法,以调用 CAkFilePackageLUT 的查询服务。未找到文件描述符或者请求的文件描述不属于文件包时,则调用默认实现。

文件位置

文件包中的文件查询原理是:从平台文件打开函数一次性获取文件句柄,然后在每次调用 AK::StreamMgr::IAkFileLocationResolver::Open() 时,只需返回使用此文件句柄的文件描述符,但应使用文件包内原始文件的相对偏置(使用文件描述符 AkFileDesc 的 uSector 字段)。 这也有助于加强对磁盘文件分布的控制。

File Packager 实用程序将仔细构建文件头,以便 Low-Level I/O 仅需将少量指针转型即可获取其中的查询表,即一个指针用于流播放音频文件,一个用于 SoundBank 文件。查询表是以下结构组成的数组:

struct AkFileEntry
{
AkFileID fileID; // 文件标识符。
AkUInt32 uBlockSize; // 一个块的大小,要求对齐(单位:字节)。
AkInt64 iFileSize; // 文件大小,单位:字节。
AkUInt32 uStartBlock;// 启动块,以 uBlockSize 表示。
AkUInt32 uLanguageID;// 语言 ID。如果不是专门针对特定语言,则为 AK_INVALID_LANGUAGE_ID。
};

表格键值是文件的 fileID。然而,不同语言的相应文件具有相同的 fileID,但 uLanguageID 不同。File Packager 总是先根据 fileID 对文件条目排序,然后再根据 uLanguageID 排序。在 CAkFilePackageLowLevelIO::Open() 中,ID 将传递给 CAkFilePackageLUT::LookupFile()(在 Open() 的字符串版本中,字符串首先使用声音引擎 API 的服务 AK::SoundEngine::GetIDFromString() 进行散列)。 CAkFilePackageLUT::LookupFile() 根据 uCodecID 标志选择要搜索的相应表格,并根据 fileID 和 uLanguageID 键执行二进制搜索。如果找到匹配项,将向 CAkFilePackageLowLevelIO::Open() 返回文件条目地址,以收集用于填充文件描述符(AkFileDesc)的必要信息。

Tip: 将搜索每个文件包,直至找到匹配项为止。如果您的一个文件包仅包含 SoundBank,另一个文件包仅包含流播放文件,则可以更改实现,使用 AkFileSystemFlags 仅在正确的文件包中一次性查询文件。

文件描述符的句柄 hFile 也是文件包的句柄。文件大小 iFileSize 直接存储在文件条目中,起始块 uSector 也是。

Note: 如前文所述,对于句柄 hFile 所示文件开头的相对偏置,Stream Manager 不使用字节,而使用块(“扇区”)。块大小代表文件位置的粒度。File Packager 的当前版本对所有文件使用相同的块大小,块大小在生成时指定(使用 -blocksize switch,请参阅 Wwise 帮助文档了解有关 File Packager 命令行参数的更多详情)。将执行补零操作,以便拼接的文件总是从块边界开始。

文件包 Low-Level I/O 使用文件描述符的 uCustomParamSize 字段来存储块大小。这样做有 2 个目的:

  • 易于访问其块大小;
  • 区别文件包中的文件描述符(uCustomParamSize == 0 意味着在文件包中未找到文件)。 例如,当文件属于文件包时,文件句柄(包中所有文件之间共享)在 CAkFilePackageLowLevelIO::Close() 中将不会关闭。

管理语言

自 Wwise 版本 2011.2 以来,当前语言均在默认 Stream Manager 模块中设置,可以在 AkStreamMgrModule.h 中使用 AK::StreamMgr::SetCurrentLanguage() 定义。传递语言的名称时,应 不带结尾的斜杠或反斜杠

CAkFileLocationBase 中继承的默认 Low-Level I/O 实现从 Stream Manager 获取语言名称,并将其附加到基本路径之后。因此语言名称应为该语言本地化素材的存储目录名称。

File Packager 实用程序生成的文件包可以包含同一素材的若干语言版本。 它们的文件头中包含语言名称的字符串映射。文件包 Low-Level I/O 监听 Stream Manager 中的语言变化,并使用当前语言名称查询素材包中的正确本地化版本。