Wwise 声音引擎使用 Stream Manager 来加载 SoundBank 和读取流音频文件。如果您还没有 I/O 管理器,还欢应您将它用于所有游戏 I/O。如果您不将它直接当作客户端使用,仅通过实施 Low-Level I/O 挂钩将 Wwise I/O 集成到游戏中,则您可以跳过本章。
Stream Manager 的主要接口通过用来创建流播放对象的 AK::IAkStreamMgr 进行定义。
在使用 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
|
除文件标识符外,流创建方法还接受指向包含文件定位标记的 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 预定要销毁的流对象向 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: 声音引擎的 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: 在销毁流前(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: 流停止时请勿调用 GetBuffer()。在尝试从流中获取缓冲区之前,应总是调用 Start()。 |
通过调用 AK::IAkAutoStream::GetBuffer() 可以访问数据。如果已从 Low-Level I/O 中读取数据,此方法将返回 AK_DataReady、包含该数据的缓冲区的地址及其大小。当不再需要缓冲区时,可以通过调用 AK::IAkAutoStream::ReleaseBuffer() 来释放它。于是用户所见的流位置将增加释放缓冲区的大小增量。用户可以通过调用 AK::IAkAutoStream::SetPosition() 来强制使用另外的位置。AKIAkAutoStream::GetBuffer() 的下一次调用将从该位置开始。
|
Caution: 更改流位置可能导致一些数据被删除。自动流最适于顺序存取。其中有启发式算法来指定循环,帮助 Stream Manager 高效地管理流内存。有关更多信息,请参阅 常见缺陷和其他注意事项 一节。 |
|
Caution: 如果 Low-Level I/O 报告错误,流将进入错误模式。自动流无法从其错误状态中恢复,AKIAkAutoStream::GetBuffer() 将总是返回 AK_Fail。 |
自动流中的数据可以采用两种不同的方式进行存取:
AK::IAkAutoStream::GetBuffer() 有 4 种可能的返回代码:
如果用户收到填有数据的缓冲区,则此方法将返回 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: 如果可能,请一次只获取一个缓冲区。这可以为 Stream Manager 提供更多的空间来执行 I/O。一次使用多个缓冲区的策略,一般留给需要访问环形/双缓冲区的硬件或其他接口。如果您打算对流一次使用多个缓冲区,应该传递正确的启发式算法( AkAutoStmHeuristics::uMinNumBuffers )来进行通知。 |
|
Caution: 使用自动流时的一个常见错误是忘记调用 ReleaseBuffer()。 |
成功的 GetBuffer() 调用所得的缓冲区大小由 Stream Manager 决定。但它必须是块大小的倍数,除非达到了底层文件的末尾。您还可以使用缓冲限制(请参见 AkAutoStmBufSettings)来强制规定 GetBuffer() 返回的大小,将在创建时传递。
|
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: 缓冲区限制是可选的,一般不应使用,这样 Stream Manager 能够最高效地管理流内存。有些平台上,声音引擎使用解码硬件时将用到缓冲区限制。这些解码器一次通常需要访问 2 个缓冲区,有特定的字节对齐方式(块大小)和最小缓冲区大小。 |
Default Streaming Manager Information |
必须提供启发式算来帮助 Stream Manager 执行调度和内存分配。自动流启发式算法是在创建时根据每个流专门指定的,对于确保最佳表现非常重要。它们可随时通过 AK::IAkAutoStream::GetHeuristics() 和 AK::IAkAutoStream::SetHeuristics() 来查询和更改。
在选择缓冲区大小前,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() 来决定何时应该释放缓冲区。AKIAkAutoStream::SetPosition() 实际上指示的是对于 GetBuffer() 的下一次调用时客户端所期望的位置。 AK::IAkAutoStream::GetPosition() 返回客户端当前占据的第一个缓冲区的位置。如果客户端并未持有缓冲区,它将返回下一次调用 GetBuffer() 时获取的缓冲区位置。 |
AK::IAkAutoStream::GetBuffer() 的 AK_NoMoreData 返回代码是根据流位置来确定的。AKIAkAutoStream::GetPosition() 返回的out_bEndOfStream 可选标志也将绑定到流位置。当且仅当以下条件下,它才会返回 True
自动流 API 的设计不会使用户阻塞 Low-Level I/O 的待定事务,除非阻止 AK::IAkAutoStream::GetBuffer() 调用。甚至 AK::IAkAutoStream::Destroy() 也不会阻塞。通常而言,如果流在销毁时仍在与 Low-Level I/O 交流,它将继续留存于内部,直至 Low-Level I/O 的当前传输结束。
要覆盖整个 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; }