Wwise SDK 2023.1.9
|
The Stream Manager is used by the Wwise sound engine to load soundbanks and read streamed audio files. You are also more than welcome to use it for all your game I/O if you don't already have an I/O manager. You may skip this entire chapter if you don't use it directly as a client, and only integrate Wwise I/O into your game by implementing the low-level I/O hooks.
The Stream Manager's main interface is defined by AK::IAkStreamMgr, which is basically a factory for streaming objects.
Before using the Stream Manager, you need to instantiate it.
Default Streaming Manager Information Stream Manager instantiation is implementation specific. The factory function of Audiokinetic's default implementation of the Stream Manager is thus defined in AkStreamMgrModule.h: AK::StreamMgr::Create(). You need to pass it implementation-specific settings (refer to Audiokinetic Stream Manager Initialization Settings for a description of the initialization settings). |
Default Streaming Manager Information Before you can create and use stream objects, you need to create a high-level streaming device. This concept is specific to Audiokinetic's default implementation of the Stream Manager. A high-level streaming device is essentially an I/O scheduler implementation, running in its own thread, which centralizes the stream object associated with it and posts data transfer requests to the Low-Level I/O. High-level devices are generally referred to simply as devices. One or more devices are created by the game, preferably at initialization time, with specific flags and settings. For more details about the initialization settings, refer to section Audiokinetic Stream Manager Initialization Settings. AK::StreamMgr::CreateDevice() returns a device ID, which should be kept by the Low-Level I/O for file location and device assignation purposes. (Refer to the File Location Resolving section for more information. ) This device ID must be passed to AK::StreamMgr::DestroyDevice() for proper termination. As for all implementation-specific functions, AK::StreamMgr::CreateDevice() and AK::StreamMgr::DestroyDevice() are defined in the header AkStreamMgrModule.h. |
Much of the Stream Manager's main API is a set of stream factory methods. Given a file identifier and settings, they create a stream object and return an interface to that stream. All stream operations are performed through this interface. In this API, it is possible to identify a stream file either by file name (string) or by file ID (integer). This choice is wrapped into the structure AkFileOpenData. Only one method should be used to designate a file. If both are used, the string method will be preferred.
Note: Open() methods of the Low-Level I/O interface create file descriptors out of file identifiers. |
Along with the file identifier, stream creation methods accept a pointer to an AkFileSystemFlags structure that contains flags for file location.
Default Streaming Manager Information This pointer is passed as is to the Low-Level I/O. Refer to sections SoundBanks, Streamed Audio Files and Basic File Location for more details on the AkFileSystemFlags and how the sound engine uses them. |
There is an additional argument in the stream creation methods: in_bSyncOpen. Users (like the sound engine) pass true when they require that the file handle, wrapped by the stream object, be opened during this call. If they pass false, it means that they allow the Stream Manager to defer opening of the file. The latter may or may not defer it. If it does defer it, and opening fails, then the next call to AK::IAkStdStream::Read() or AK::IAkAutoStream::GetBuffer() will return AK_Fail.
Note: This should not be considered as a fatal error. The sound engine passes false for streamed audio files. If GetBuffer() returns false, it considers it as an I/O error, and destroys the stream gracefully. |
Default Streaming Manager Information Argument in_bSyncOpen is passed directly to the Low-Level I/O. Refer to Deferred Opening for details on the consequences of this flag in the Low-Level I/O. |
AK::IAkStreamMgr::CreateStd() creates a standard stream, and AK::IAkStreamMgr::CreateAuto() creates an automatic stream. These two types of streams are described in the following sections.
Both types of stream objects define a Destroy() method. You need to call this method to free the resources used by the stream. After doing so, you must not use the object again.
Default Streaming Manager Information Stream objects that are scheduled for destruction signal the I/O thread so that they get freed as soon as no more I/O transfers they own are pending in the Low-Level I/O. If stream profiling is occurring, the I/O thread waits for the monitoring thread to give its approval before disposing of the stream object. The monitoring thread performs a profiling pass every 200 ms. |
Calling AK::IAkStreamMgr::CreateStd() creates a standard stream object that is controllable via the returned AK::IAkStdStream interface.
A standard stream uses a basic Read/Write scheme to control I/O operations. When AK::IAkStdStream::Read() or AK::IAkStdStream::Write() is called, an I/O request is queued to the scheduler, and the stream sets its status to AK_StmStatusPending. The user passes the transfer size requested and the address of the buffer. Upon completion, the stream sets its status to either AK_StmStatusCompleted or AK_StmStatusError. Its position is incremented by the actual size transferred, so the next operation will occur at the new position. To force another position, the user must use the AK::IAkStdStream::SetPosition() method before initiating a new transfer.
Only one I/O operation can occur at a time. Subsequent operations need to be explicitly initiated by calling AK::IAkStdStream::Read() or AK::IAkStdStream::Write(). When the user has finished using a stream, AK::IAkStdStream::Destroy() must be called. Then the interface becomes invalid.
Default Streaming Manager Information Read or write calls end up in I/O transfers in the Low-Level I/O. If the size requested at the client level is greater than the streaming device's granularity (AkDeviceSettings::uGranularity), the transfers are split up. The stream stays in AK_StmStatusPending or AK_StmStatusIdle state until the whole transfer is complete, or until an error or an end-of-file condition occurs. |
The interface exposes other methods, including setting queries, access to current status or positions, and access to buffers supplied to previous operations.
A standard stream operation can be launched in two different ways:
AK::IAkStdStream::Read()
or AK::IAkStdStream::Write()
is true, the method will immediately block until all I/O is complete.AK::IAkStdStream::Read()
or AK::IAkStdStream::Write()
is false, the methods will return before I/O is complete, and the remainder of the operation will complete asynchronously. The status of the stream can be polled by calling AK::IAkStdStream::GetStatus()
until AK_StmStatusCompleted is returned, or it is possible to do a blocking wait for completion of the asynchronous task by calling AK::IAkStdStream::WaitForPendingOperation()
. For a read opepration, data can then be safely accessed by calling AK::IAkStdStream::GetData()
, or reading the buffer provided during the initial call to AK::IAkStdStream::Read()
.You need to specify the open mode (AkOpenMode) when creating a standard stream. Open mode specifies if the stream can be used for reading, writing, or both. Obviously, calling AK::IAkStdStream::Write() on a stream opened for reading only will fail.
You can also specify a name for the stream by using AK::IAkStdStream::SetStreamName(). The string is copied in the stream object, and can be queried with AK::IAkStdStream::GetInfo().
Default Streaming Manager Information The stream name is what appears in the streaming tab of Wwise's Advanced Profiler. |
Heuristics of standard streams are specified on a per-operation basis.
AK::IAkStdStream::Read() and AK::IAkStdStream::Write() require a priority and a deadline (in milliseconds) for the operation. Typically, the Stream Manager will favor operations that have a short deadline. When the application requires more I/O bandwidth than what the storage device can provide, it favors streams that have high priority. Specifying a deadline of zero means that data is needed now, so I/O is already late. In that case, it will be serviced before any other streams that have a lower priority.
Note: The sound engine's Bank Manager is a user of standard streams. It reads and parses SoundBank data in its own buffer. A method is exposed in the sound engine API to specify an average throughput and priority of bank loading: AK::SoundEngine::SetBankLoadIOSettings(). (Refer to Banks in the Wwise Sound Engine for more information. ) The Bank Manager calls AK::IAkStdStream::Read(), parses data when it completes, and reads. It uses the priority given as an argument of that method. It computes the operation's deadline from the user-specified throughput: fDeadline = uBufferSize / fUserSpecifiedThroughput;
Deadlines of standard stream operations and average throughput of automatic streams are equivalent for the Stream Manager's I/O scheduler. Users of the sound engine can thus tweak the burden of bank loading on I/O with that of audio streams, and other streams in the game. |
The low-level constraint on read and seek granularity and buffer alignment is referred to as "block size" at the Stream Manager level. The AK::IAkStdStream::GetBlockSize()
method on the streams' interface queries the Low-Level I/O, passing it the file descriptor associated with the stream, and returning that value to the caller. Refer to the Low-Level I/O section for more details on file descriptors and the AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize()
method. Some storage devices have restrictions on data transfer sizes. For example, on Win32 platforms, a file opened with the FILE_FLAG_UNBUFFERED flag will permit transfer sizes that are a multiple of the physical device's sector size only. Read or Write operations will fail if they request a transfer size that is not a multiple of the block size. It is the responsibility of the user of the high-level interface to ask for read sizes that are multiples of the value returned by AK::IAkStdStream::GetBlockSize()
. AK::IAkStdStream::SetPosition()
is subject to the same constraint. However, it automatically snaps to the lower block boundary, and returns the actual file position offset.
Typically, a game title is a user of the Stream Manager. Since the game title also implements the Low-Level I/O submodule, it is already aware of the transfer sizes it is allowed to use. The sound engine is not aware of storage device constraints, so it always snaps its standard stream read sizes to a block size boundary.
AK::IAkStdStream::Read() and AK::IAkStdStream::Write() fail if called while a stream is in AK_StmStatusPending state. AK::IAkStdStream::GetPosition() and AK::IAkStdStream::SetPosition() yield undetermined results, since they could occur before or after the I/O transfer is complete. However, they do not block on I/O.
AK::IAkStdStream::Cancel() can be used while a transfer is pending, but we do not recommend using it, as performance will suffer. This method confirms to the caller that no I/O is pending. If the call is executed before a request is sent to the Low-Level I/O, the task is simply removed from the queue. However, if a request has already been sent to the Low-Level I/O, the caller is blocked until the I/O is complete.
Tip: Users do not have to call AK::IAkStdStream::Cancel() explicitly before destroying a stream (AK::IAkStdStream::Destroy()). However, for best performance, users should call Destroy() only when the stream is not in AK_StmStatusPending state. |
Calling AK::IAkStreamMgr::CreateAuto() creates an automatic stream object that is controllable through the returned AK::IAkAutoStream interface.
Automatic streams are streams used for input only. They are called automatic because I/O requests are sent to the Low-Level I/O "under the hood", without any explicit function calls by the user. Streaming memory is owned by the Stream Manager, and streamed data is accessed by asking for a streaming memory address. When a region of the streaming memory is granted to the user, it remains locked until it is explicitly released. Meanwhile, the internal scheduler performs data transfers in memory that is not locked. A stream is idle when it is created. Automatic scheduling of I/O requests begins when the user calls AK::IAkAutoStream::Start(). It can be stopped or paused by calling AK::IAkAutoStream::Stop(), and resumed by calling AK::IAkAutoStream::Start() again.
Caution: Do not call GetBuffer() while the stream is stopped. You should always call Start() before attempting to get buffers from the stream. |
Data is accessed by calling AK::IAkAutoStream::GetBuffer(). If data has already been read from the Low-Level I/O, the method will return AK_DataReady, the address of the buffer containing that data, and its size. When the buffer is not required anymore, it is released by callingAK::IAkAutoStream::ReleaseBuffer(). The stream position as seen by the user is then incremented by the size of the released buffer. The user can force a new position by calling AK::IAkAutoStream::SetPosition(). The next call to AK::IAkAutoStream::GetBuffer() will be consistent with that position.
Caution: Changing the stream position may cause some data to be flushed. Automatic streams are optimized for sequential access. There are heuristics that specify looping, which help the Stream Manager manage its streaming memory efficiently. For more information, refer to the Common Pitfalls and Other Considerations section. |
Caution: If the Low-Level I/O reports an error, the stream puts itself in error mode. Automatic streams cannot recover from their error state, and AK::IAkAutoStream::GetBuffer() will always return AK_Fail. |
The data in automatic streams can be accessed in two different ways:
AK::IAkAutoStream::GetBuffer() has 4 possible return codes:
If the user is granted a buffer filled with data, the method returns either AK_DataReady, or AK_NoMoreData. If the stream buffer is empty, AK::IAkAutoStream::GetBuffer() returns AK_NoDataReady, with a size of zero and a buffer address of null. If the Low-Level I/O reports an error, or the method is called with invalid parameters, it returns AK_Fail.
AK_NoMoreData is returned instead of AK_DataReady when the last buffer of the file is granted. The Stream Manager evaluates that it is the last buffer by comparing the current position (as seen by the user) with the file size provided by the Low-Level I/O as a member of the file descriptor structure. Subsequent calls to AK::IAkAutoStream::GetBuffer() return AK_NoMoreData with a size of zero.
Every call to AK::IAkAutoStream::GetBuffer() provides a new buffer. Buffers must be explicitly released by calling AK::IAkAutoStream::ReleaseBuffer(). They are released in the order they were granted by GetBuffer(). When a buffer is released, its address becomes invalid. ReleaseBuffer() returns AK_Fail when the client holds no buffer. However this should not be considered as a fatal error.
Tip: If possible, get only one buffer at a time. This provides more room to the Stream Manager to perform I/O. Using more than one buffer at a time is typically reserved for hardware or other interfaces that need access to a ring/double buffer. If you plan to use more than one buffer at a time for a given stream, you should inform by passing proper heuristics (AkAutoStmHeuristics::uMinNumBuffers). |
Caution: A common mistake when using automatic streams is to forget to call ReleaseBuffer(). |
The buffer size granted by a successful GetBuffer() call is decided by the Stream Manager. It must however be a multiple of the block size unless the end of the underlying file was reached. You may also force the size returned by GetBuffer() using buffering contraints (see AkAutoStmBufSettings), passed at creation time.
Caution: Note that forcing buffer sizes may result in suboptimal performance, as streaming memory and/or bandwidth may be wasted. Also, user-specified contraints may not supported. IAkStreamMgr::CreateAuto() would return AK_Fail. |
Default Streaming Manager Information This size will usually correspond to the granularity specified in high-level devices creation settings, except if the stream reached its end. |
Calling AK::IAkAutoStream::GetBuffer() with the in_bWait flag set to True blocks the user until data is ready. Therefore, the method cannot return AK_NoDataReady. If the final buffer has already been granted and released, the method returns AK_NoMoreData right away with size zero.
The user can try to get data by evaluating the code returned by AK::IAkAutoStream::GetBuffer(), called with the in_bWait flag set to False. If it is AK_NoDataReady, the user should perform other tasks and try again later.
Automatic streams are instantiated with more settings than standard streams, among them constraints on buffer sizes and heuristics. These settings help the Stream Manager allocate optimal amounts of memory and prioritize data transfer requests.
Some settings are memory-related constraints specified by the user. The buffer size can be specified directly. If you wish to let the Stream Manager choose the buffer size but stay within limits, you can specify a minimal buffer size or block size, or both. The buffer size will be a multiple of that block size.
Tip: Buffer constraints are optional, and should generally not be used in so that the Stream Manager may manage its streaming memory optimally. The sound engine uses buffer constraints when it uses decoding hardware on some platforms. These decoders typically need to access 2 buffers at a time, with a specific byte alignment (block size) and minimum buffer size. |
Default Streaming Manager Information |
Heuristics must be provided to help the Stream Manager perform scheduling and memory allocation. Automatic streams heuristics are specified per-stream, at creation time, and are very important for optimal behavior. They can be queried and changed anytime through AK::IAkAutoStream::GetHeuristics() and AK::IAkAutoStream::SetHeuristics().
The Stream Manager always queries the block size of a given file to the Low-Level I/O before choosing the buffer size, to ensure that the buffer size is a multiple of the block size. Therefore, users of automatic streams do not have to concern themselves with the Low-Level block size, unless they are forcing the stream to another position. In this case, they need to consider the block size obtained from AK::IAkAutoStream::GetBlockSize(), or the real absolute offset returned by AK::IAkAutoStream::SetPosition().
Stream position is evaluated from the user's point of view, and is queried and set by the Get/SetPosition() methods. Data transferred from the Low-Level I/O into the Stream Manager's buffers is not taken into account when computing the position. It is updated when a buffer is released by the user. If some data had already been transferred from the Low-Level I/O when AK::IAkAutoStream::SetPosition() was called, that data is generally flushed.
Note: Users should avoid calling AK::IAkAutoStream::SetPosition() whenever possible. If it must be used, it should be called as early as possible. For example, in the sound engine, when a looping sound source obtains a buffer from the Stream Manager, it checks to see if the loop end is contained in that buffer. If it is, AK::IAkAutoStream::SetPosition() is called right away (that is, long before ReleaseBuffer()), thus minimizing the risk of streaming useless data. Also, specifying looping heuristics can help the internal scheduler make better decisions regarding requests to the Low-Level I/O. |
Tip: AK::IAkAutoStream::SetPosition() can be called with or without having a buffer locked, but it is recommended to change the position as early as possible, to minimize the risk of streaming data for nothing. Stopping the stream appropriately can also help minimize wasted bandwidth. AK::IAkAutoStream::SetPosition() does not release buffers held by the client. Clients decide when buffers should be released by calling ReleaseBuffer() explicitly. AK::IAkAutoStream::SetPosition() actually indicates the position expected by the client for the next call to GetBuffer(). AK::IAkAutoStream::GetPosition() returns the position of the first buffer currently held by the client. If the client holds no buffer, then it returns the position of the buffer that will be granted at the next call to GetBuffer(). |
The AK_NoMoreData return code of AK::IAkAutoStream::GetBuffer() is determined from the stream position. The out_bEndOfStream optional flag returned by AK::IAkAutoStream::GetPosition() is also bound to the stream position. It returns True if and only if
The automatic stream API has been designed so that the user never blocks on any pending transactions with the Low-Level I/O, except for blocking AK::IAkAutoStream::GetBuffer() calls. Even AK::IAkAutoStream::Destroy() should not block. Typically, if a stream is still interacting with the Low-Level I/O when it is destroyed, it will continue to live internally until the current transfer with the Low-Level I/O is complete.
To override the entire Stream Manager, the game title must implement all the interfaces defined in IAkStreamMgr.h. The implementation should follow the rules described elsewhere in this section.
You can replace the static library AkStreamMgr.lib by the game's own. The creation function and other implementation-specific settings and definition, such as the Low-Level I/O API, are located in AkStreamMgrModule.h and are therefore not part of the Stream Manager.
The sound engine accesses the Stream Manager by calling its inline static AK::IAkStreamMgr::Get() method, which returns a pointer to the AK::IAkStreamMgr interface, defined as a protected member of AK::IAkStreamMgr. The custom implementation must declare only one variable (AK::IAkStreamMgr::m_pStreamMgr), and linkage is automatic. If the Stream Manager and the sound engine are not linked together in the same executable or DLL. AK::IAkStreamMgr::m_pStreamMgr must have the __dllexport attribute. The AKSTREAMMGR_API macro may be used, with AKSTREAMMGR_EXPORTS defined in the compiler settings for the custom Stream Manager library.
The profiling interfaces also need to be implemented to enable linking with the non-AK_OPTIMIZED version of the sound engine. The interface is fairly self-explanatory. Implementing it enables profiling in Wwise. If profiling is not required, AK::IAkStreamMgr::GetStreamMgrProfile() should return NULL, and the interfaces can then be implemented with empty code. In this case, no profiling information will be displayed in Wwise.
The code below is intended to show some considerations that are better explained by code than textually. It is particularly useful if you wish to override or change the implementation of the Stream Manager. This code is intentionally heavy, as it points out some issues to which you should pay particular attention.
Questions? Problems? Need more info? Contact us, and we can help!
Visit our Support pageRegister your project and we'll help you get started with no strings attached!
Get started with Wwise