版本
menu_open
Wwise SDK 2018.1.11
高级 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 初始化设置 一节。AK::StreamMgr::CreateDevice() 返回设备 ID,出于文件定位和设备分配目的, Low-Level I/O 应保存此设备 ID 。(请参阅 文件位置解析 一节了解更多信息。)此设备 ID 必须传递给 AK::StreamMgr::DestroyDevice() 才能正确终止设备。跟所有针对特定实现的函数一样,AK::StreamMgr::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: 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: 这不是致命错误。对于已经流播放的音频文件,声音引擎返回 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 中,AK::IAkStdStream::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 传输。如果客户端请求的大小大于流播放设备的粒度(AkDeviceSettings::uGranularity),则传输将被拆分。流将保持在 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: 声音引擎的 Bank Manager 使用标准流。它在自己的缓冲区中读取和解析 SoundBank 数据。声音引擎 API 中提供一个方法来指定加载 SoundBank 时的平均吞吐量和优先级:AK::SoundEngine::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(),操作将失败。AK::IAkStdStream::GetPosition()AK::IAkStdStream::SetPosition() 将产生不确定的结果,因为这些结果可能发生在 I/O 传输结束之前,也可能在结束之后。但它们不会阻塞 I/O。

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

Tip: 在销毁流前(AK::IAkStdStream::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: 流停止时请勿调用 GetBuffer()。在尝试从流中获取缓冲区之前,应总是调用 Start()。

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

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

自动流数据存取机制

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

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

  • AK_DataReady
  • AK_NoMoreData
  • AK_NoDataReady
  • AK_Fail

如果用户收到填有数据的缓冲区,则此方法将返回 AK_DataReady 或 AK_NoMoreData。如果流缓冲区为空,AK::IAkAutoStream::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: 如果可能,请一次只获取一个缓冲区。这可以为 Stream Manager 提供更多的空间来执行 I/O。一次使用多个缓冲区的策略,一般留给需要访问环形/双缓冲区的硬件或其他接口。如果您打算对流一次使用多个缓冲区,应该传递正确的启发式算法( AkAutoStmHeuristics::uMinNumBuffers )来进行通知。

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

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

Caution: 注意,强制规定缓冲区大小可能造成性能欠佳,因为可能会浪费流内存或带宽。 另外也可能不支持用户指定的限制。IAkStreamMgr::CreateAuto() 将返回 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: 缓冲区限制是可选的,一般不应使用,这样 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: 用户应尽可能避免调用 AK::IAkAutoStream::SetPosition()。如果必须使用,则应尽早调用。例如,在声音引擎中,当循环声源从 Stream Manager 获取缓冲区时,它会查看其中是否包含循环结尾。如果包含,则立即调用 AK::IAkAutoStream::SetPosition()(即早在 ReleaseBuffer() 之前调用),从而最大限度地降低传输无用数据的风险。另外,指定循环启发式算法可以帮助内部调度程序对 Low-Level I/O 请求做出更好的决策。

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

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

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

AK::IAkAutoStream::GetBuffer() 的 AK_NoMoreData 返回代码是根据流位置来确定的。AK::IAkAutoStream::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 的保护成员)的指针。自定义实现只能声明一个变量(AK::IAkStreamMgr::m_pStreamMgr),链接将自动执行。如果 Stream Manager 和声音引擎没有在同一个可执行文件或 DLL 中链接在一起,则 AK::IAkStreamMgr::m_pStreamMgr 必须拥有 __dllexport 属性。可以使用 AKSTREAMMGR_API 宏,并在自定义 Stream Manager 库的编译程序设置中定义 AKSTREAMMGR_EXPORTS。

性能分析

还需要实现性能分析接口,以允许与声音引擎的非 AK_OPTIMIZED 版本进行链接。此接口一目了然。实现它后即可在 Wwise 中执行性能分析。如果不需要性能分析,AK::IAkStreamMgr::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;
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 )
{
// 执行其他操作……
}
// 检查状态。
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,
&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,
&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 &&
{
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;
"out.dat",
NULL, // 我们可以使用此变量来告诉 Low-Level I/O,需要在另一
// 存储设备中创建此文件(支持写入的另一个存储设备)。
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,
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; // 不循环。
// 注:循环启发式算法非常重要,即使整个应用程序中只有一条流也是如此,
// 因为它可能改变管理流内存的方式。
// 注:优先级也与此无关。然而,您必须指定一个介于 AK_MIN_PRIORITY
// 和 AK_MAX_PRIORITY 之间(包括两者)的值,否则 Stream Manager 会将其创建设置视为无效。
// 创建流。
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,
&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 ) );
}
autostmbasictest_cleanup:
if ( pStream )
pStream->Destroy();
return bSuccess;
}

此页面对您是否有帮助?

需要技术支持?

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

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

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

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

开始 Wwise 之旅