版本

menu_open

高级 Stream Manager API 说明

简介

Wwise 声音引擎使用 Stream Manager 来加载 SoundBank 和读取流音频文件。如果您还没有 I/O 管理器,还欢应您将它用于所有游戏 I/O。如果您不将它直接当作客户端使用,仅通过实施 Low-Level I/O 挂钩将 Wwise I/O 集成到游戏中,则您可以跳过本章。

Stream Manager 主要接口

Stream Manager 的主要接口通过用来创建流播放对象的 AK::IAkStreamMgr 进行定义。

Stream Manager 实例化

在使用 Stream Manager 之前,您需要将其实例化。

Default Streaming Manager Information

Stream Manager 实例化与特定实现相关。Audiokinetic 的 Stream Manager 的默认实现函数在 AkStreamMgrModule.h: AK::StreamMgr::Create() 中定义。您需要将特定的实现设置传递给它(请参阅 Audiokinetic Stream Manager 初始化设置 了解初始化设置的说明)。

高级流播放设备

Default Streaming Manager Information

在创建和使用流对象之前,您需要创建高级流播放设备,此概念仅针对 Audiokinetic 的 Stream Manager 默认实现。高级流播放设备即为 I/O 调度器,它在自己的线程中运行,将与其相关的流对象集中起来,并向 Low-Level I/O 发送数据传输请求。高级设备通常被简称为设备。游戏使用特定的标志和设置创建若干个设备,最好是在初始化时创建。有关初始化设置的更多详情,请参阅 Audiokinetic Stream Manager 初始化设置 一节。AKStreamMgr::CreateDevice() 返回设备 ID,出于文件定位和设备分配目的, Low-Level I/O 应保存此设备 ID 。(请参阅 文件位置解析 一节了解更多信息。)此设备 ID 必须传递给 AK::StreamMgr::DestroyDevice() 才能正确终止设备。跟所有针对特定实现的函数一样,AKStreamMgr::CreateDevice()AK::StreamMgr::DestroyDevice() 在文件头 AkStreamMgrModule.h 中定义。

创建流

Stream Manager 的主要 API 在很大程度上说是流创建方法的集合。根据文件标识符和设置,它们创建一个流对象并向其返回一个接口,所有流操作都将通过此接口执行。 两个流创建方法各有两个重载,根据其使用的文件标识机制而有所不同。第一个重载使用字符串标识文件,第二个重载使用整型 ID。

Default Streaming Manager Information

当您使用 AK::IAkStreamMgr::CreateStd()AK::IAkStreamMgr::CreateAuto() 的字符串文件标识重载版本时,Stream Manager 会将字符串转发给 AK::StreamMgr::IAkFileLocationResolver::Open() 的字符串重载版本。同样,调用 ID 文件标识符重载版本会导致调用 AK::StreamMgr::IAkFileLocationResolver::Open() 的 ID 重载版本。更多信息请参阅 文件位置解析 一节。

Note.gif
Note: Low-Level I/O 接口的 Open() 方法使用文件标识符来创建文件描述符。

除文件标识符外,流创建方法还接受指向包含文件定位标记的 AkFileSystemFlags 结构的指针,

Default Streaming Manager Information

并原封不动传递给 Low-Level I/O。请参阅 SoundBank流播放音频文件基本文件定位 各节了解有关 AkFileSystemFlags 以及声音引擎如何使用它们的更多详情。

流创建方法中还有另一个参数:in_bSyncOpen,当用户(例如声音引擎)需要文件句柄封装在流对象中并在此调用期间打开时,它们将返回 True。如果返回 False,意味着它们允许 Stream Manager 延迟打开文件,文件即有可能被延迟打开。如果延迟打开但打开失败,则 AK::IAkStdStream::Read()AK::IAkAutoStream::GetBuffer() 的下一次调用将返回 AK_Fail,

Note.gif
Note: 这不是致命错误。对于已经流播放的音频文件,声音引擎返回 False。如果 GetBuffer() 返回 False,则会将视此为 I/O 错误并正常地销毁流。
Default Streaming Manager Information

参数 in_bSyncOpen 直接传递给 Low-Level I/O。请参阅 延迟打开 了解有关此标志在 Low-Level I/O 中产生结果的详情。

AK::IAkStreamMgr::CreateStd() 创建标准流,而 AK::IAkStreamMgr::CreateAuto() 创建自动流。下面是这两种流的描述。

销毁流

两种流对象均定义有 Destroy() 方法,供您调用来释放流所占用的资源。释放后您将不可再使用此对象。

Default Streaming Manager Information

在默认的 Stream Manager 中,AKIAkStdStream::Destroy() 和 AK::IAkAutoStream::Destroy() 仅用于在流对象上设置标志,实际的销毁操作由 I/O 线程随后执行。I/O 线程被唤醒时,将首先检查所有打开的流,查找下一个要将 I/O 请求发布给 Low-Level I/O 的流,并清理所有废弃流。此时才会释放内存,并调用 AK::StreamMgr::IAkLowLevelIOHook::Close()(其中释放文件句柄)。

预定要销毁的流对象向 I/O 线程发出信号,当 Low-Level I/O 中再也没有等待其执行的 I/O 传输时,就会将它们释放。

如果正在进行流性能分析,I/O 线程将等待监控线程允许,然后再处理流对象。监控线程每 200 毫秒执行一次性能分析过程。

标准流

调用 AK::IAkStreamMgr::CreateStd() 将创建标准流对象,通过返回的 AK::IAkStdStream 接口可以控制该对象。

定义

标准流使用基本的读/写机制来控制 I/O 操作。当调用 AK::IAkStdStream::Read()AK::IAkStdStream::Write() 时,I/O 请求将排队进入调度程序,并且其状态将被流设置为 AK_StmStatusPending. 用户传递请求的传输大小和缓冲区的地址后,其状态将被流设置为 AK_StmStatusCompleted 或 AK_StmStatusError。下一操作将在现有位置将加上实际传输大小所得的新位置处执行。要强制使用另一位置,必须在初始化新传输前使用 AK::IAkStdStream::SetPosition() 方法。

I/O 操作一次只能执行一个。后续操作需要通过调用 AK::IAkStdStream::Read()AK::IAkStdStream::Write() 来显式地初始化。使用完流后,必须调用 AK::IAkStdStream::Destroy(),接口将变为无效。

Default Streaming Manager Information

读\写调用最终在 Low-Level I/O 中执行 I/O 传输。如果客户端请求的大小大于流播放设备的粒度(AkDeviceSettingsuGranularity),则传输将被拆分。流将保持在 AK_StmStatusPending 或 AK_StmStatusIdle 状态,直到整个传输结束或发生错误,或满足文件结束条件。

此接口还提供其他方法,包括设置查询、访问当前状态或位置,以及访问供先前操作使用的缓冲区。

标准流数据访问机制

标准流操作可以通过两种不同的方式启动:

创建设置

在创建标准流时,您需要指定打开模式(AkOpenMode)。打开模式指定了流是否可用于读取、写入或者两者均可。显然,对于只为读取而打开的流调用 AK::IAkStdStream::Write() 将失败。

您还可以使用 AK::IAkStdStream::SetStreamName() 为流指定名称。字符串复制至流对象中,并可以通过 AK::IAkStdStream::GetInfo() 查询。

Default Streaming Manager Information

流名称就是 Wwise Advanced Profiler 的 Streaming 选项卡中所显示的名称。

启发式算法

标准流的启发式算法根据每个操作而异。

AK::IAkStdStream::Read()AK::IAkStdStream::Write() 需要知道操作的优先级和截止时间(单位:毫秒)。一般而言,Stream Manager 会先处理具有较短截止时间的操作。当应用程序需要的 I/O 带宽超过存储设备能够提供的范围时,将先处理高优先级流。截止时间为零意味着现在就需要数据,因此 I/O 已经迟了。在这种情况下,将先处理它,然后再处理低优先级的其它流。

Note.gif

Note: 声音引擎的 Bank Manager 使用标准流。它在自己的缓冲区中读取和解析 SoundBank 数据。声音引擎 API 中提供一个方法来指定加载 SoundBank 时的平均吞吐量和优先级:AKSoundEngine::SetBankLoadIOSettings()。(请参阅 Wwise 声音引擎中的 SoundBank 一节了解更多信息。)Bank Manager 调用 AK::IAkStdStream::Read(),在调用结束时解析数据,然后进行读取。该方法将使用作为参数的优先级,并根据用户指定的吞吐量来计算操作的截止时间:

fDeadline = uBufferSize / fUserSpecifiedThroughput;

标准流操作的截止时间和自动流的平均吞吐量相当于 Stream Manager 的 I/O 调度程序。通过游戏中的音频流和其他流的这一功能,声音引擎用户即可减轻 I/O 的库加载负担。

常见缺点和其他注意事项

块大小

“块大小”是在 Stream Manager 级别对读取和寻址粒度以及缓冲区对齐的底层限制。这些流接口中的 AK::IAkStdStream::GetBlockSize() 方法可以查询 Low-Level I/O,向其传递与流相关的文件描述符,并返回给调用方。请参阅 Low-Level I/O 一节了解有关文件描述符和 AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() 方法的更多详情。有些存储设备对数据传输大小有限制。例如,在 Win32 平台上,具有 FILE_FLAG_UNBUFFERED 标志的已打开文件所允许的传输大小,是物理设备扇区大小的数倍。如果读取或写入操作请求的传输大小不是块大小的倍数,则操作将失败。查询读取大小是否是 AK::IAkStdStream::GetBlockSize() 返回值的倍数应由高级接口用户负责。AK::IAkStdStream::SetPosition() 也受相同的限制。但它会自动锁定到块下限,并返回实际的文件位置偏差。

一般而言,游戏是 Stream Manager 的用户。因为游戏也会实现底层 I/O 子模块,所以已经清楚允许使用的传输大小。声音引擎不清楚存储设备限制,因此它总是将标准流读取大小锁定到块大小边界上。

当流状态为待定时调用的方法

当流处于 AK_StmStatusPending 状态时,如果调用 AK::IAkStdStream::Read()AK::IAkStdStream::Write(),操作将失败。AKIAkStdStream::GetPosition()AK::IAkStdStream::SetPosition() 将产生不确定的结果,因为这些结果可能发生在 I/O 传输结束之前,也可能在结束之后。但它们不会阻塞 I/O。

当传输处于待定状态时,可以使用 AK::IAkStdStream::Cancel(),但我们不建议使用它,因为性能将受到影响。此方法向调用方确认没有 I/O 处于待定状态。如果在向 Low-Level I/O 发送请求前执行调用,任务将从队列中移除。但如果请求已发送给 Low-Level I/O,调用方在 I/O 结束前将一直被阻塞。

Tip.gif
Tip: 在销毁流前(AKIAkStdStream::Destroy()),用户无需显式调用 AK::IAkStdStream::Cancel()。然而,为了获得最佳性能,只有当流未处于 AK_StmStatusPending 状态时用户才应调用 Destroy()。

自动流

调用 AK::IAkStreamMgr::CreateAuto() 将创建一个自动流对象,通过返回的 AK::IAkAutoStream 接口可控制该对象。

定义

自动流仅用于输入。它们之所以被称为自动流是因为 I/O 请求将“在后台”发送给 Low-Level I/O,用户无需进行任何显式的函数调用。流内存归 Stream Manager 所有,并通过查询流内存地址来访问流数据。当流内存的区域分配给用户时,该区域将保持锁定状态,直至被显式释放。同时,内部调度程序在未锁定的内存中执行数据传输。在创建时,流处于闲置状态。从用户调用 AK::IAkAutoStream::Start() 时开始将执行 I/O 请求的自动调度。通过调用 AK::IAkAutoStream::Stop() 可以停止或暂停流,而再次调用 AK::IAkAutoStream::Start() 将使它继续工作。

Caution.gif
Caution: 流停止时请勿调用 GetBuffer()。在尝试从流中获取缓冲区之前,应总是调用 Start()。

通过调用 AK::IAkAutoStream::GetBuffer() 可以访问数据。如果已从 Low-Level I/O 中读取数据,此方法将返回 AK_DataReady、包含该数据的缓冲区的地址及其大小。当不再需要缓冲区时,可以通过调用 AK::IAkAutoStream::ReleaseBuffer() 来释放它。于是用户所见的流位置将增加释放缓冲区的大小增量。用户可以通过调用 AK::IAkAutoStream::SetPosition() 来强制使用另外的位置。AKIAkAutoStream::GetBuffer() 的下一次调用将从该位置开始。

Caution.gif
Caution: 更改流位置可能导致一些数据被删除。自动流最适于顺序存取。其中有启发式算法来指定循环,帮助 Stream Manager 高效地管理流内存。有关更多信息,请参阅 常见缺陷和其他注意事项 一节。
Caution.gif
Caution: 如果 Low-Level I/O 报告错误,流将进入错误模式。自动流无法从其错误状态中恢复,AKIAkAutoStream::GetBuffer() 将总是返回 AK_Fail。

自动流数据存取机制

自动流中的数据可以采用两种不同的方式进行存取:

AK::IAkAutoStream::GetBuffer() 有 4 种可能的返回代码:

  • AK_DataReady
  • AK_NoMoreData
  • AK_NoDataReady
  • AK_Fail

如果用户收到填有数据的缓冲区,则此方法将返回 AK_DataReady 或 AK_NoMoreData。如果流缓冲区为空,AKIAkAutoStream::GetBuffer() 将返回 AK_NoDataReady,大小为零,缓冲区地址为空。如果 Low-Level I/O 报告错误或者使用无效的参数调用方法,则将返回 AK_Fail。

如果收到的是文件的最后一个缓冲区,则返回 AK_NoMoreData 而非 AK_DataReady。Stream Manager 通过比较当前位置(用户所见位置)和 Low-Level I/O 所提供的文件描述符结构中的文件大小成员,来判断它是否是最后一个缓冲区。此后再调用 AK::IAkAutoStream::GetBuffer() 将返回 AK_NoMoreData,大小为零。

每次调用 AK::IAkAutoStream::GetBuffer() 都会提供一个新缓冲区。缓冲区必须通过调用 AK::IAkAutoStream::ReleaseBuffer() 来进行显式释放。释放的顺序就是使用 GetBuffer() 接收它们的顺序。释放后,缓冲区的地址将变为无效。当客户端不再持有缓冲区时,ReleaseBuffer() 返回 AK_Fail,但这不是致命错误。

Tip.gif

Tip: 如果可能,请一次只获取一个缓冲区。这可以为 Stream Manager 提供更多的空间来执行 I/O。一次使用多个缓冲区的策略,一般留给需要访问环形/双缓冲区的硬件或其他接口。如果您打算对流一次使用多个缓冲区,应该传递正确的启发式算法( AkAutoStmHeuristics::uMinNumBuffers )来进行通知。

Caution.gif
Caution: 使用自动流时的一个常见错误是忘记调用 ReleaseBuffer()。

成功的 GetBuffer() 调用所得的缓冲区大小由 Stream Manager 决定。但它必须是块大小的倍数,除非达到了底层文件的末尾。您还可以使用缓冲限制(请参见 AkAutoStmBufSettings)来强制规定 GetBuffer() 返回的大小,将在创建时传递。

Caution.gif
Caution: 注意,强制规定缓冲区大小可能造成性能欠佳,因为可能会浪费流内存或带宽。 另外也可能不支持用户指定的限制。IAkStreamMgrCreateAuto() 将返回 AK_Fail。
Default Streaming Manager Information

此大小通常为高层设备创建设置中指定的粒度,但流文件结束时例外。

阻塞

在 in_bWait 标志设置为 True 的情况下,调用 AK::IAkAutoStream::GetBuffer() 将导致用户被阻塞,直至数据准备就绪。因此该方法无法返回 AK_NoDataReady。如果最后一个缓冲区也已经被获取和释放,此方法将立即返回 AK_NoMoreData,大小为零。

轮询

调用 AK::IAkAutoStream::GetBuffer() 时将 in_bWait 标志设为 False 后,用户可以通过评估返回的代码来尝试获取数据。如果返回的代码是 AK_NoDataReady,用户应执行其他任务并稍后再试。

创建设置

自动流实例化的设置比标准流要多,其中包括缓冲区大小限制和启发式算法。这些设置会帮助 Stream Manager 分配最佳的内存量,并对数据传输请求进行优先级排序。

缓冲区限制

有些设置是用户指定的内存相关限制。缓冲区大小可以直接指定。如果您想让 Stream Manager 在限制范围内选择缓冲区大小,可以指定最小的缓冲区或块大小,也可以两者都指定。缓冲区大小将是块大小的倍数。

Tip.gif
Tip: 缓冲区限制是可选的,一般不应使用,这样 Stream Manager 能够最高效地管理流内存。有些平台上,声音引擎使用解码硬件时将用到缓冲区限制。这些解码器一次通常需要访问 2 个缓冲区,有特定的字节对齐方式(块大小)和最小缓冲区大小。

Default Streaming Manager Information

当前的实现不允许自定义自动流缓冲区的大小超出设备粒度。(请参阅 Audiokinetic Stream Manager 初始化设置 了解有关粒度的更多详情。)

启发式算法

必须提供启发式算来帮助 Stream Manager 执行调度和内存分配。自动流启发式算法是在创建时根据每个流专门指定的,对于确保最佳表现非常重要。它们可随时通过 AK::IAkAutoStream::GetHeuristics()AK::IAkAutoStream::SetHeuristics() 来查询和更改。

  • 吞吐量:平均吞吐量即决定了 I/O 调度程序的截止时间,因为调度程序知道它有多少数据可供特定流使用。这相当于标准流的各操作专用截止时间启发式。
  • 优先级:通常在应用程序所需的 I/O 带宽超出存储设备可提供范围的情况下适用。在这种情况下,Stream Manager 将试图满足优先级更高的流请求。这相当于标准流的各操作专用优先级启发式。
  • 循环:您可以在循环流中指定循环位置。这可以帮助 Stream Manager 管理它的内存。在释放循环后,比较好的做法是重新将启发式算法设置为“非循环”(设置 uLoopEnd 为 0)。注意,它只是一个启发式算法:除非客户端调用 SetPosition(),否则不应更改流位置。然而,对于循环启发式算法,意外地更改位置通常会导致数据被刷新。
  • uMinClientBuffers:它指示客户端计划同时持有多少个缓冲区。通常同时应至持有一个缓冲区,但有时可能需要同时持有 2 个缓冲区。在这种情况下,请将 uMinClientBuffers 设置为 2。Stream Manager 应尝试使用至少 (uMinClientBuffers + 1) 个缓冲区来对流进行缓冲。注意,为 uMinClientBuffers 指定 0(默认值)相当于 1。

常见缺陷和其他注意事项

块大小

在选择缓冲区大小前,Stream Manager 总是先向 Low-Level I/O 查询给定文件的块大小,以确保缓冲区大小是块大小的倍数。因此,自动流的用户不必关心底层块大小,除非它们强制流进入到另一个位置。在这种情况下,它们需要考虑从 AK::IAkAutoStream::GetBlockSize() 获得的块大小或者 AK::IAkAutoStream::SetPosition() 所返回的实际绝对偏置。

流位置

流位置是从用户的角度来评估的,可以通过 Get/SetPosition() 方法进行查询和设置。在计算位置时不会考虑从 Low-Level I/O 传输到 Stream Manager 缓冲区的数据。用户释放缓冲区时将对其进行更新。如果调用 AK::IAkAutoStream::SetPosition() 时已有数据从 Low-Level I/O 传出,它们一般会被刷新。

Note.gif

Note: 用户应尽可能避免调用 AK::IAkAutoStream::SetPosition()。如果必须使用,则应尽早调用。例如,在声音引擎中,当循环声源从 Stream Manager 获取缓冲区时,它会查看其中是否包含循环结尾。如果包含,则立即调用 AK::IAkAutoStream::SetPosition()(即早在 ReleaseBuffer() 之前调用),从而最大限度地降低传输无用数据的风险。另外,指定循环启发式算法可以帮助内部调度程序对 Low-Level I/O 请求做出更好的决策。

Tip.gif

Tip: 在锁定或未锁定缓冲区的情况下均可调用 AK::IAkAutoStream::SetPosition(),但建议尽早更改位置,以便最大限度地降低徒劳无功地传输数据的风险。正确地停止流还有助于将带宽浪费最小化。

AK::IAkAutoStream::SetPosition() 不会释放被客户端占据的缓冲区。客户端通过显式调用 ReleaseBuffer() 来决定何时应该释放缓冲区。AKIAkAutoStream::SetPosition() 实际上指示的是对于 GetBuffer() 的下一次调用时客户端所期望的位置。

AK::IAkAutoStream::GetPosition() 返回客户端当前占据的第一个缓冲区的位置。如果客户端并未持有缓冲区,它将返回下一次调用 GetBuffer() 时获取的缓冲区位置。

AK::IAkAutoStream::GetBuffer() 的 AK_NoMoreData 返回代码是根据流位置来确定的。AKIAkAutoStream::GetPosition() 返回的out_bEndOfStream 可选标志也将绑定到流位置。当且仅当以下条件下,它才会返回 True

  • uLoopEnd heuristics 设置为 0(无循环),
  • 不再有数据从 Low-Level I/O 传输,
  • 用户已经释放其所有缓冲区。

阻塞待定底层操作的方法

自动流 API 的设计不会使用户阻塞 Low-Level I/O 的待定事务,除非阻止 AK::IAkAutoStream::GetBuffer() 调用。甚至 AK::IAkAutoStream::Destroy() 也不会阻塞。通常而言,如果流在销毁时仍在与 Low-Level I/O 交流,它将继续留存于内部,直至 Low-Level I/O 的当前传输结束。

覆盖 Stream Manager

要覆盖整个 Stream Manager,游戏必须实现 IAkStreamMgr.h 中定义的所有接口。此实现应遵守本节中所述的规则。

您可以将静态库 AkStreamMgr.lib 替换成游戏自己的库。创建函数以及针对实现的其他设置和定义(例如 Low-Level I/O API)位于 AkStreamMgrModule.h 中,因此不是 Stream Manager 的组成部分。

链接声音引擎

声音引擎通过调用其内联的静态 AK::IAkStreamMgr::Get() 方法来访问 Stream Manager,该方法将返回指向 AK::IAkStreamMgr 接口(它是 AK::IAkStreamMgr 的保护成员)的指针。自定义实现只能声明一个变量(AKIAkStreamMgr::m_pStreamMgr),链接将自动执行。如果 Stream Manager 和声音引擎没有在同一个可执行文件或 DLL 中链接在一起,则 AK::IAkStreamMgr::m_pStreamMgr 必须拥有 __dllexport 属性。可以使用 AKSTREAMMGR_API 宏,并在自定义 Stream Manager 库的编译程序设置中定义 AKSTREAMMGR_EXPORTS。

性能分析

还需要实现性能分析接口,以允许与声音引擎的非 AK_OPTIMIZED 版本进行链接。此接口一目了然。实现它后即可在 Wwise 中执行性能分析。如果不需要性能分析,AKIAkStreamMgr::GetStreamMgrProfile() 应返回 NULL,即可使用空代码实现接口。在这种情况下,Wwise 中将不显示任何性能分析信息。

代码示例

以下代码用于说明一些注意事项,使用代码比文字更能解释清楚。当您想要更改或不沿用 Stream Manager 时,将尤其有用。此代码有意写得非常复杂,它指出了您应该特别注意的一些问题。

// 注意:为了简明起见,我们假设测试文件小于 4Gb(其位置基于 32 位)。

// 标准流的用法和注意事项。
// 如果测试成功(即如果 Stream Manager 表现正常),则此函数返回 True。
bool BasicStdStreamTest(
    const AkOSChar * in_pszFilename
    )
{
    // 基本的标准流测试。
    bool bSuccess = true;
    AkUInt64 iCurPosition;
    const AkInt64 POSITION_BEGIN = 0;

    // 为读取操作创建流对象。
    AK::IAkStdStream * pStream;
    AKRESULT eResult = AK::IAkStreamMgr::Get()->CreateStd(
        in_pszFilename,     // 应用流标识符(文件名)。
        NULL,               // 无文件系统标志:我们提供完整路径,无需任何文件位置解析逻辑。
        AK_OpenModeRead,    // 创建设置:打开模式。
        pStream,            // 返回的流接口。
        true );             // 需要同步打开文件。
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Failed creating stream.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 提供内存。
    const int BUFFER_SIZE = 8192;
    unsigned char pBuffer[BUFFER_SIZE];

    // 在尝试读取之前,不知道 Low-Level I/O 模块的用户
    // 应确保它们的读取大小符合块大小限制。此测试假定
    // BUFFER_SIZE 是块大小的倍数。如果不是,将不会继续。
    if ( BUFFER_SIZE % pStream->GetBlockSize() != 0 )
    {
        printf( "Low-Level storage device requirements are not compatible with this test.\n");
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    //
    // 读取缓冲区中的 BUFFER_SIZE 字节。阻塞读取操作。
    //
    AkUInt32 uSizeTransferred;
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 测试:如果未到达文件末尾,则读取大小应等于请求大小。验证。
    bool bReachedEOF;
    pStream->GetPosition( &bReachedEOF );
    if ( !bReachedEOF &&
         uSizeTransferred != BUFFER_SIZE )
    {
        // 这不可能发生。
        printf( "Inconsistent transfer size.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 注:当前流位置应为 BUFFER_SIZE。为了让此测试正确
    // 比较数据,输入文件应足够大。如果已到达文件末尾,会立即离开。
    if ( bReachedEOF )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    //
    // 读取缓冲区中的 BUFFER_SIZE 字节。非阻塞读取。
    //
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 非阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查代码。
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 轮询,直至数据准备就绪。
    AkStmStatus eStatus = pStream->GetStatus();
    while ( eStatus != AK_StmStatusCompleted && eStatus != AK_StmStatusError )
    {
        // 执行其他操作……
        AKPLATFORM::AkSleep(0);
    }

    // 检查状态。
    if ( pStream->GetStatus() != AK_StmStatusCompleted )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 再次检查确保未到达 EOF,否则数据比较将失败。
    pStream->GetPosition( &bReachedEOF );
    if ( bReachedEOF )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }


    //
    // 设置位置为 ABS_POSITION(从文件开头起的绝对值),并读取缓冲区中的 BUFFER_SIZE 字节。
    //
    AkInt64 ABS_POSITION = 12000;
    AkInt64 iRealOffset;
    eResult = pStream->SetPosition(
        ABS_POSITION,
        AK_MoveBegin,
        &iRealOffset );
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "SetPosition failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 注意:使用返回的实际偏置。如果 Low-Level I/O 指定大于 1 的块大小,
    // Stream Manager 将使寻址锁定到下限边界(例如,如果寻址是 9001,而块大小
    // 是 2000,则位置应设为 8000,iRealOffset 同样如此)。
    printf( "Set position to %u, low-level block size is %u, actual position set to %u\n",
        ABS_POSITION,
        pStream->GetBlockSize(),
        iRealOffset );

    // 读取。
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }
    // 再次检查确保未到达 EOF,否则数据比较将失败。
    pStream->GetPosition( &bReachedEOF );
    if ( bReachedEOF )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 保持当前绝对位置。
    iCurPosition = pStream->GetPosition( NULL );

    //
    // 向后设置位置,相当于当前位置设置 -REL_POSITION    //
    AkInt64 REL_POSITION = -8000;
    eResult = pStream->SetPosition(
        REL_POSITION,
        AK_MoveCurrent,
        &iRealOffset );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "SetPosition failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 注:如果块大小大于 1,则实际绝对移动偏置可以大于 REL_POSITION
    // (iRealOffset <= REL_POSITION).

    // 注:新位置相对于文件开头应大致相隔 ABS_POSITION+BUFFER_SIZE+REL_POSITION
    // 字节。
    // iCurPosition 包含“实际”ABS_POSITION,iRealOffset 现在包含“实际”REL_POSITION,
    // 即实际移动偏置。因此,GetPosition() 总是返回绝对位置,
    // 应等于 iCurPosition+iRealOffset。
    if ( pStream->GetPosition( NULL ) != iCurPosition+iRealOffset )
    {
        printf( "Wrong stream position." );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    iCurPosition = pStream->GetPosition( NULL );
    printf( "Reading from offset %u.\n", iCurPosition );

    // 从该位置开始读取。
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }


    //
    // 取消操作
    //
    // 读取。
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 取消。
    pStream->Cancel();

    // 在此,Stream Manager 有时间将请求发送到底层并完成
    // (状态将为 AK_StmStatusCompleted),或者它将从尚未执行的操作队列中移除
    //(状态将为 AK_StmStatusCancelled)。
    // 在任一情况下,都要确认流没有尚未执行的操作。
    if ( pStream->GetStatus() != AK_StmStatusCompleted &&
         pStream->GetStatus() != AK_StmStatusCancelled )
    {
        printf( "Inconsistent status after operation cancellation.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }


    //
    // 写入。
    //
    // 读取。
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 重新写入另一文件(阻塞)。
    // 创建流。
    AK::IAkStdStream * pWrStream;
    eResult = AK::IAkStreamMgr::Get()->CreateStd(
        "out.dat",
        NULL,                   // 我们可以使用此变量来告诉 Low-Level I/O,需要在另一
                                // 存储设备中创建此文件(支持写入的另一个存储设备)。
        AK_OpenModeReadWrite,
        pWrStream,
        true );                 // 需要同步打开文件。
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Failed creating stream for writing.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }
    eResult = pWrStream->Write(
        pBuffer,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success ||
         uSizeTransferred != BUFFER_SIZE )
    {
        printf( "Write failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }
    // 将位置设置为开头,然后重新从另一个缓冲区中读取文件以对数据进行比较。
    eResult = pWrStream->SetPosition(
        POSITION_BEGIN,
        AK_MoveBegin,
        NULL );
    if ( eResult != AK_Success )
    {
        printf( "SetPosition failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    unsigned char pBufferCheck[BUFFER_SIZE];
    eResult = pWrStream->Read(
        pBufferCheck,
        BUFFER_SIZE,
        true,                   // 阻塞
        AK_DEFAULT_PRIORITY,    // 默认优先级。
        0,                      // 截止时间为 0:现在请求数据(当然会延迟)。
        uSizeTransferred );
    // 检查返回代码。
    if ( eResult != AK_Success ||
         uSizeTransferred != BUFFER_SIZE )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // 比较数据。
    if ( memcmp( pBuffer, pBufferCheck, uSizeTransferred ) != 0 )
    {
        printf( "Data read and written do not match.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

stdstmbasictest_cleanup:

    // 关闭流。
    if ( pStream )
        pStream->Destroy();
    if ( pWrStream )
        pWrStream->Destroy();

    return bSuccess;
}


// 自动流使用方法和注意事项。
// 如果测试成功(如果 Stream Manager 表现正常),则此函数返回 True。
bool BasicAutoStreamTest(
    const AkOSChar * in_pszFilename
    )
{
    // 创建自动流。
    bool bSuccess = true;
    AK::IAkAutoStream * pStream;

    // 设置启发式算法。
    AkAutoStmHeuristics heuristics;
    heuristics.fThroughput  = 1048576;  // 1 Mb/s
    // 注:由于它是 Stream Manager 中创建的唯一流,因此吞吐量将没有影响。
    // 调度程序将总是为 I/O 选择此流。
    heuristics.uLoopStart   = 0;
    heuristics.uLoopEnd     = 0;    // 不循环。
    // 注:循环启发式算法非常重要,即使整个应用程序中只有一条流也是如此,
    // 因为它可能改变管理流内存的方式。
    heuristics.priority     = AK_DEFAULT_PRIORITY;
    // 注:优先级也与此无关。然而,您必须指定一个介于 AK_MIN_PRIORITY
    // 和 AK_MAX_PRIORITY 之间(包括两者)的值,否则 Stream Manager 会将其创建设置视为无效。

    // 创建流。
    AKRESULT eResult = AK::IAkStreamMgr::Get()->CreateAuto(
        in_pszFilename, // 文件名。
        NULL,           // 无文件系统标志:我们提供完整路径,不需要文件位置解析逻辑。
        heuristics,     // 我们的启发式算法。
        NULL,           // 缓冲限制:无。如果用户不需要,则不应指定用户限制。
        pStream,        // 返回的流接口。
        true );         // 需要同步打开文件。
    // 检查返回代码。
    if ( eResult != AK_Success )
    {
        printf( "Failed creating stream.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // 启动流。I/O 调度程序开始考虑此流。
    pStream->Start();

    // 获取流大小(通过 GetInfo)。
    AkStreamInfo streamInfo;
    pStream->GetInfo( streamInfo );

    // 获取第一个缓冲区。存取将阻塞。
    AkUInt32 uBufferSize;
    void * pBuffer;
    eResult = pStream->GetBuffer(
        pBuffer,        // 用于获取数据空间的地址。
        uBufferSize,    // 用于获取数据空间的大小。
        true            // 阻塞,直至数据准备就绪。
        );
    // 检查返回代码。
    if ( eResult != AK_DataReady &&
         eResult != AK_NoMoreData )
    {
        printf( "GetBuffer failed.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // 假设我们解析了缓冲区中包含的数据,这提供了文件的相关信息。
    // 比如说文件应在位置 10000 和 150000(字节)之间循环。
    AkUInt32 uLoopStart = 10000;
    AkUInt32 uLoopEnd = 150000;

    // 注:为了让此测试正确比较数据,输入文件应大于 uLoopEnd。
    if ( (AkUInt32)streamInfo.uSize < uLoopEnd )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // 为了使 Stream Manager 发挥最佳性能,应相应地设置启发式算法。
    pStream->GetHeuristics( heuristics );   // 注意:在当前情况下,没必要首先获取启发式算法。
    heuristics.uLoopStart = uLoopStart;
    heuristics.uLoopEnd = uLoopEnd;
    pStream->SetHeuristics( heuristics );

    // 注意:当前流位置是客户端见到的位置。因此它为 0,这是因为
    // 尚未释放缓冲区。
    if ( pStream->GetPosition( NULL ) != 0 )
    {
        printf( "Invalid stream position.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // 释放缓冲区。
    eResult = pStream->ReleaseBuffer();
    if ( eResult != AK_Success )
    {
        printf( "ReleaseBuffer failed.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // 注:既然我们已经释放缓冲区,当前流位置应为 uBufferSize。
    if ( pStream->GetPosition( NULL ) != uBufferSize )
    {
        printf( "Invalid stream position.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    //
    // 在以下序列中,Stream Manager 用户将获取并释放缓冲区,直至过了“循环结束”位置。
    // 查询位置发生在 GetBuffer() 之后,因此它们将参考缓冲区的开头。
    // 用户通过累加流位置和所持缓冲区的大小来“预料”何时通过
    // 循环点。。在此时,位置重新设置到循环的开头,
    // 更改启发式算法,并读取文件到结束(直至 GetBuffer() 返回 AK_NoMoreData)。
    // 在释放缓冲区后更容易获取位置,但建议您
    // 尽早更改位置。Stream Manager 拥有循环启发式算法,但它们的使用
    // 与特定实现紧密相关。
    //
    bool bEOF = false;
    bool bDoLoop = true;
    while ( !bEOF )
    {
        // 轮询读取,直至经过位置 uLoopEnd。
        eResult = pStream->GetBuffer(
            pBuffer,
            uBufferSize,
            false );
        // 检查返回代码。
        if ( eResult == AK_Fail )
        {
            assert( !"I/O error" );
            return false;
        }
        else if ( eResult == AK_NoDataReady )
        {
            printf( "Starving...\n" );
        }
        else
        {
            // 文件结束条件。
            bEOF = ( eResult == AK_NoMoreData ) && !bDoLoop;

            // 处理循环。在第一次调用 SetPosition 后,将 bDoLoop 设为 false
            // 以便我们停止循环。
            if ( bDoLoop )
            {
                // 获取当前位置以查看是否已经穿过边界。
                // 前面说过,因为 Stream Manager 用户拥有缓冲区,所以 GetPosition() 返回
                // 缓冲区开头的位置。将缓冲区大小
                // 加到该值上,以获得缓冲区的结束位置。
                // 注意:在不调用 GetPosition() 的情况下跟踪位置
                // 会更加高效。
                AkUInt32 uCurPosition = pStream->GetPosition( NULL );
                if ( ( uCurPosition + uBufferSize ) > uLoopEnd )
                {
                    // 已经过循环结尾。
                    // 将位置设置为循环开头。
                    AkInt64 iLoopStart;
                    iLoopStart.HighPart = 0;
                    iLoopStart.LowPart = uLoopStart;
                    AkInt64 iRealOffset;
                    eResult = pStream->SetPosition(
                        iLoopStart,
                        AK_MoveBegin,
                        &iRealOffset );
                    if ( eResult != AK_Success )
                    {
                        printf( "SetPosition failed.\n" );
                        bSuccess = false;
                        goto autostmbasictest_cleanup;
                    }

                    // 注:由于我们尚未释放缓冲区,因此 GetPosition() 仍应返回缓冲区的
                    // 开始位置。
                    // 请验证这一点。
                    if ( pStream->GetPosition( NULL ) != uCurPosition )
                    {
                        printf( "Invalid position returned.\n" );
                        bSuccess = false;
                        goto autostmbasictest_cleanup;
                    }

                    // 注意:uLoopStart 位置不会直接与该流的底层 IO
                    // 块大小对齐。Stream Manager 的用户负责处理此限制。
                    // 在此,如果 iRealOffset 不为 0,则它将小于被请求的位置。
                    // Stream Manager 用户可将校正值存储在本地变量中。

                    // 既然已经设置位置,缓冲区的下一次存取将在循环的开头
                    // (减去校正值)。
                    // 为了发挥最佳性能,可立即更改循环启发式值,因为
                    // 将不再出现循环。
                    heuristics.uLoopStart = 0;
                    heuristics.uLoopEnd = 0;    // 0:不再循环。
                    pStream->SetHeuristics( heuristics );

                     // 停止循环。
                    bDoLoop = false;
                }
            }

            eResult = pStream->ReleaseBuffer();
            if ( eResult != AK_Success )
            {
                printf( "ReleaseBuffer failed.\n" );
                bSuccess = false;
                goto autostmbasictest_cleanup;
            }
        }

        // 模拟用户吞吐量。等待时间等于缓冲区大小除以吞吐量。
        //AKPLATFORM::AkSleep( DWORD( 1000*uBufferSize / heuristics.fThroughput ) );
        AKPLATFORM::AkSleep( 0 );
    }

autostmbasictest_cleanup:

    if ( pStream )
        pStream->Destroy();

    return bSuccess;
}

此页面对您是否有帮助?

需要技术支持?

仍有疑问?或者问题?需要更多信息?欢迎联系我们,我们可以提供帮助!

查看我们的“技术支持”页面

介绍一下自己的项目。我们会竭力为您提供帮助。

来注册自己的项目,我们帮您快速入门,不带任何附加条件!

开始 Wwise 之旅