목차

High-Level Stream Manager API Description

Introduction

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.

Stream Manager Main Interface

The Stream Manager's main interface is defined by AK::IAkStreamMgr, which is basically a factory for streaming objects.

Instantiating the Stream Manager

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).

High-Level Streaming Devices

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.

Stream Creation

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.

Both sets of stream creation methods have two overloads, which differ according to the file identification scheme they use. The first overload uses strings to identify files, the second uses integer IDs.

Default Streaming Manager Information

When you use the AK::IAkStreamMgr::CreateStd() or AK::IAkStreamMgr::CreateAuto() string file identifier overload, the Stream Manager forwards this string to the string overload of AK::StreamMgr::IAkFileLocationResolver::Open(). Likewise, calling the ID file identifier overload triggers a call to the ID overload of AK::StreamMgr::IAkFileLocationResolver::Open(). For more information, refer to the section File Location Resolving.

Note.gif
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.gif
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.

Stream Destruction

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

In the default implementation of the stream manager, AK::IAkStdStream::Destroy() and AK::IAkAutoStream::Destroy() are simply used to set a flag on the stream object. Actual destruction is performed later, by the I/O thread. Every time it wakes up, it inspects all the open streams, finds the next stream for which an I/O request will be issued to the Low-Level I/O, and cleans up all dead streams. It is only at this point that memory will be freed, and AK::StreamMgr::IAkLowLevelIOHook::Close() (where the file handle is released) will be called.

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.

Standard Streams

Calling AK::IAkStreamMgr::CreateStd() creates a standard stream object that is controllable via the returned AK::IAkStdStream interface.

Definition

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.

Standard Stream Data Access Schemes

A standard stream operation can be launched in two different ways:

Creation Settings

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

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.gif

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.

Common pitfalls and other considerations

Block Size

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.

Methods Called While Stream Status is Pending

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.gif
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.

Automatic Streams

Calling AK::IAkStreamMgr::CreateAuto() creates an automatic stream object that is controllable through the returned AK::IAkAutoStream interface.

Definition

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.gif
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 calling AK::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.gif
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.gif
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.

Automatic Stream Data Access Schemes

The data in automatic streams can be accessed in two different ways:

AK::IAkAutoStream::GetBuffer() has 4 possible return codes:

  • AK_DataReady
  • AK_NoMoreData
  • AK_NoDataReady
  • AK_Fail

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.gif
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.gif
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.gif
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.

Blocking

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.

Polling

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.

Creation Settings

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.

Buffer Constraints

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.gif
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

The current implementation does not allow custom automatic stream buffer sizes to exceed the device granularity. (Refer to Audiokinetic Stream Manager Initialization Settings for more details on granularity.)

Heuristics

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().

  • Throughput: The average throughput translates as a deadline for the I/O scheduler, since it knows how much data it has already available for a given stream. It is equivalent to the standard stream's per-operation deadline heuristic.
  • Priority: This is generally used when the application requires more I/O bandwidth than what the storage device can provide. In that case, the Stream Manager will try to fulfill requests of streams with higher priority. It is equivalent to the standard stream's per- operation priority heuristic.
  • Looping: You can specify looping positions in a stream that loops. This helps the Stream Manager in managing its memory. If the loop is released, it is a good practice to set that heuristic back to "non-looping" (set uLoopEnd to 0). Note that this is only a heuristic: the stream position shall not be changed unless the client called SetPosition(). However, unexpected position changes in regard to looping heuristics will typically result in data flushing.
  • uMinClientBuffers: This indicates how many buffers the client plans to hold at the same time. Typically, you should hold one buffer at a time. Sometimes it may be required to hold 2 buffers at a time. In this case, set uMinClientBuffers to 2. The Stream Manager should try to buffer your stream with at least (uMinClientBuffers + 1) buffers. Note that specifying 0 for uMinClientBuffers (default value) is equivalent to 1.

Common Pitfalls and Other Considerations

Block Size

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

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.gif
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.gif

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

  • uLoopEnd heuristics is set to 0 (not looping),
  • no more data is being transferred from the Low-Level I/O,
  • the user has released all its buffers.

Methods Blocking on Pending Low-Level Operations

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.

Overriding the Stream Manager

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.

Linkage with the Sound Engine

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.

Profiling

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.

Example code

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.

// Note: For the sake of simplicity, we assume that the test file is smaller than 4Gb (its
position fits on 32 bits).

// Standard stream usage and considerations.
// This function returns true if the test is successful (if the Stream Manager behaves
correctly).
bool BasicStdStreamTest(
    const AkOSChar * in_pszFilename
    )
{
    // Basic standard stream test.
    bool bSuccess = true;
    AkUInt64 iCurPosition;
    const AkInt64 POSITION_BEGIN = 0;

    // Create a stream object for reading.
    AK::IAkStdStream * pStream;
    AKRESULT eResult = AK::IAkStreamMgr::Get()->CreateStd(
        in_pszFilename,     // Application stream identifier (file name).
        NULL,               // No file system flags: we provide the full path and do not need any file location resolving logic.
        AK_OpenModeRead,    // Creation setting: Open Mode.
        pStream,            // Returned stream interface.
        true );             // Require synchronous file open.
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Failed creating stream.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Provide memory.
    const int BUFFER_SIZE = 8192;
    unsigned char pBuffer[BUFFER_SIZE];

    // Before attempting any read, users who are unaware of the implementation of the Low-Level I/O module
    // should ensure their read sizes are consistent with the block size constraint. This test assumes
    // that BUFFER_SIZE is a multiple of the block size. If it is not, we do not continue.
    if ( BUFFER_SIZE % pStream->GetBlockSize() != 0 )
    {
        printf( "Low-Level storage device requirements are not compatible with this test.\n");
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    //
    // Read BUFFER_SIZE bytes in buffer. Blocking read.
    //
    AkUInt32 uSizeTransferred;
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Test: If we did not reach the end of file, size read should equal size requested. Verify.
    bool bReachedEOF;
    pStream->GetPosition( &bReachedEOF );
    if ( !bReachedEOF &&
         uSizeTransferred != BUFFER_SIZE )
    {
        // This cannot happen.
        printf( "Inconsistent transfer size.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Note: the current stream position should be BUFFER_SIZE. In order for this test to properly
    // compare data, the input file should be big enough. If we already reached the end of file, leave now.
    if ( bReachedEOF )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    //
    // Read BUFFER_SIZE bytes in buffer. Non-blocking read.
    //
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Non-blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of
        course).
        uSizeTransferred );
    // Check code.
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Poll until data is ready.
    AkStmStatus eStatus = pStream->GetStatus();
    while ( eStatus != AK_StmStatusCompleted && eStatus != AK_StmStatusError )
    {
        // Do something else...
        AKPLATFORM::AkSleep(0);
    }

    // Check status.
    if ( pStream->GetStatus() != AK_StmStatusCompleted )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Check again that we did not reach EOF, because data comparison would fail.
    pStream->GetPosition( &bReachedEOF );
    if ( bReachedEOF )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }


    //
    // SetPosition to ABS_POSITION (absolute from the beginning of the file), and read BUFFER_SIZE bytes in buffer.
    //
    AkInt64 ABS_POSITION = 12000;
    AkInt64 iRealOffset;
    eResult = pStream->SetPosition(
        ABS_POSITION,
        AK_MoveBegin,
        &iRealOffset );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "SetPosition failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // NOTE: Use the returned Real Offset. If the Low-Level I/O specifies a block size greater than 1,
    // the Stream Manager will snap the seek to the lowest boundary (for example, if seek was 9001 and block
    // size was 2000, the position would be set to 8000, and so would be iRealOffset).
    printf( "Set position to %u, low-level block size is %u, actual position set to %u\n",
        ABS_POSITION,
        pStream->GetBlockSize(),
        iRealOffset );

    // Read.
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }
    // Check again that we did not reached EOF, because data comparison would fail.
    pStream->GetPosition( &bReachedEOF );
    if ( bReachedEOF )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Keep the current absolute position.
    iCurPosition = pStream->GetPosition( NULL );

    //
    // SetPosition backwards, -REL_POSITION relative to current 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;
    }

    // NOTE: If the block size is greater than 1, the real absolute move offset could be greater than REL_POSITION
    // (iRealOffset <= REL_POSITION).

    // NOTE: The new position should be about ABS_POSITION+BUFFER_SIZE+REL_POSITION bytes relative to the beginning
    // of the file.
    // iCurPosition contains the "real" ABS_POSITION, and iRealOffset now contains the "real" REL_POSITION,
    // that is, the real move offset. Therefore, GetPosition(), which always returns the absolute position,
    // should equal 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 );

    // Read from there.
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of
        course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }


    //
    // Cancelling an operation
    //
    // Read.
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of
        course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Cancel.
    pStream->Cancel();

    // Here, either the Stream Manager had time to send and complete the request to low-level
    // (status would be AK_StmStatusCompleted), or it removed it from the queue of pending operations
    // (status would be AK_StmStatusCancelled).
    // In either case, we can be sure that no operation is pending for our stream.
    if ( pStream->GetStatus() != AK_StmStatusCompleted &&
         pStream->GetStatus() != AK_StmStatusCancelled )
    {
        printf( "Inconsistent status after operation cancellation.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }


    //
    // Writing.
    //
    // Read.
    eResult = pStream->Read(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

    // Write back to another file (blocking).
    // Create stream.
    AK::IAkStdStream * pWrStream;
    eResult = AK::IAkStreamMgr::Get()->CreateStd(
        "out.dat",
        NULL,                   // We could use this argument to tell the Low-Level I/O that this file needs to
                                // be created in another storage device (one that supports writing).
        AK_OpenModeReadWrite,
        pWrStream,
        true );                 // Require synchronous file open.
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Failed creating stream for writing.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }
    eResult = pWrStream->Write(
        pBuffer,
        BUFFER_SIZE,
        true,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success ||
         uSizeTransferred != BUFFER_SIZE )
    {
        printf( "Write failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }
    // Set position to beginning, and read back file in another buffer to compare data.
    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,                   // Blocking
        AK_DEFAULT_PRIORITY,    // Default priority.
        0,                      // Deadline is 0: data is requested NOW (will be late, of course).
        uSizeTransferred );
    // Check return code.
    if ( eResult != AK_Success ||
         uSizeTransferred != BUFFER_SIZE )
    {
        printf( "Read failed.\n" );
        bSuccess = false;
        goto stdstmbasictest_cleanup;
    }

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

stdstmbasictest_cleanup:

    // Close streams.
    if ( pStream )
        pStream->Destroy();
    if ( pWrStream )
        pWrStream->Destroy();

    return bSuccess;
}


// Automatic streams usage and considerations.
// This function returns true if the test is successful (if the Stream Manager behaves correctly).
bool BasicAutoStreamTest(
    const AkOSChar * in_pszFilename
    )
{
    // Create an automatic stream.
    bool bSuccess = true;
    AK::IAkAutoStream * pStream;

    // Set heuristics.
    AkAutoStmHeuristics heuristics;
    heuristics.fThroughput  = 1048576;  // 1 Mb/s
    // Note: Since this will be the only stream created in the Stream Manager, throughput has no effect.
    // The scheduler will always choose this stream for I/O.
    heuristics.uLoopStart   = 0;
    heuristics.uLoopEnd     = 0;    // Not looping.
    // Note: Looping heuristics are important even if there is only one stream in the whole application,
    // because it might change the way streaming memory is managed.
    heuristics.priority     = AK_DEFAULT_PRIORITY;
    // Note: Priority is also irrelevant. However, you must specify a value between AK_MIN_PRIORITY
    // and AK_MAX_PRIORITY (inclusive), otherwise the Stream Manager will consider its creation settings to be invalid.

    // Create stream.
    AKRESULT eResult = AK::IAkStreamMgr::Get()->CreateAuto(
        in_pszFilename, // File name.
        NULL,           // No file system flags: we provide the full path and do not need any file location resolving logic.
        heuristics,     // Our heuristics.
        NULL,           // Buffer constraints: none. Users should not specify user constraints if they don't need to.
        pStream,        // Returned stream interface.
        true );         // Require synchronous file open.
    // Check return code.
    if ( eResult != AK_Success )
    {
        printf( "Failed creating stream.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // Start stream. I/O scheduler starts considering this stream.
    pStream->Start();

    // Get stream size (through GetInfo).
    AkStreamInfo streamInfo;
    pStream->GetInfo( streamInfo );

    // Get the first buffer. This access will be blocking.
    AkUInt32 uBufferSize;
    void * pBuffer;
    eResult = pStream->GetBuffer(
        pBuffer,        // Address of granted data space.
        uBufferSize,    // Size of granted data space.
        true            // Block until data is ready.
        );
    // Check return code.
    if ( eResult != AK_DataReady &&
         eResult != AK_NoMoreData )
    {
        printf( "GetBuffer failed.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // Let's suppose we parsed data contained in the buffer, which gave us information about the file.
    // For example, it says that the file should be looping between positions 10000 and 150000 (bytes).
    AkUInt32 uLoopStart = 10000;
    AkUInt32 uLoopEnd = 150000;

    // Note: In order for this test to properly compare data, the input file should be bigger than uLoopEnd.
    if ( (AkUInt32)streamInfo.uSize < uLoopEnd )
    {
        printf( "Input file not big enough. Could not complete test.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    // For optimal Stream Manager behavior, we should set the heuristics accordingly.
    pStream->GetHeuristics( heuristics );   // Note: In the present case, it is not necessary to get the heuristics first.
    heuristics.uLoopStart = uLoopStart;
    heuristics.uLoopEnd = uLoopEnd;
    pStream->SetHeuristics( heuristics );

    // Note: The current stream position is the position seen by the client. It is therefore 0, because
    // we did not release the buffer yet.
    if ( pStream->GetPosition( NULL ) != 0 )
    {
        printf( "Invalid stream position.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

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

    // Note: Now that we have released the buffer, the current stream position should be uBufferSize.
    if ( pStream->GetPosition( NULL ) != uBufferSize )
    {
        printf( "Invalid stream position.\n" );
        bSuccess = false;
        goto autostmbasictest_cleanup;
    }

    //
    // In the following sequence, the stream manager user will get and release buffers until the "loop end" position is crossed.
    // Positions are queried after GetBuffer(), so they refer to the beginning of the buffer.
    // The user "foresees" when the looping point will be crossed by adding the stream position and the size of the owned
    // buffer. At this point, the position is set back to the beginning of the loop,
    // change the heuristics, and read the file until the end (until GetBuffer() returns AK_NoMoreData).
    // It is easier to get the position after having released the buffer, but we recommend you
    // change the position as early as possible. Stream Manager has looping heuristics but their usage
    // is implementation-specific.
    //
    bool bEOF = false;
    bool bDoLoop = true;
    while ( !bEOF )
    {
        // Polling reads until we cross position uLoopEnd.
        eResult = pStream->GetBuffer(
            pBuffer,
            uBufferSize,
            false );
        // Check return code.
        if ( eResult == AK_Fail )
        {
            assert( !"I/O error" );
            return false;
        }
        else if ( eResult == AK_NoDataReady )
        {
            printf( "Starving...\n" );
        }
        else
        {
            // End of file condition.
            bEOF = ( eResult == AK_NoMoreData ) && !bDoLoop;

            // Handle looping. After first call to SetPosition, set bDoLoop to false
            // so that we stop looping.
            if ( bDoLoop )
            {
                // Get the current position to see if we crossed the boundary.
                // Recall that since the stream manager user owns a buffer, GetPosition() returns the
                // position of the beginning of the buffer. Add the buffer size to
                // that value to get the position of the end of the buffer.
                // Note: Keeping track of the position without calling GetPosition()
                // would be more efficient.
                AkUInt32 uCurPosition = pStream->GetPosition( NULL );
                if ( ( uCurPosition + uBufferSize ) > uLoopEnd )
                {
                    // We crossed the loop end.
                    // Set the position to the loop start.
                    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;
                    }

                    // Note: Since we did not release the buffer yet, GetPosition() should still return the
                    // position of the start of the buffer.
                    // Verify this.
                    if ( pStream->GetPosition( NULL ) != uCurPosition )
                    {
                        printf( "Invalid position returned.\n" );
                        bSuccess = false;
                        goto autostmbasictest_cleanup;
                    }

                    // Note: Our uLoopStart position might not fall directly on the Low-LevelIO block size
                    // for that stream. Users of the Stream Manager are responsible for handling this constraint.
                    // Here, if iRealOffset is not 0, it will be smaller than the requested position.
                    // The stream manager user could store that corrected value in a local variable.

                    // Now that position is set, the next buffer access will be at the beginning of the loop
                    // (minus the correction value).
                    // For best performance, the looping heuristic value can be changed right away because
                    // there is no looping anymore.
                    heuristics.uLoopStart = 0;
                    heuristics.uLoopEnd = 0;    // 0: Not looping.
                    pStream->SetHeuristics( heuristics );

                     // Stop looping.
                    bDoLoop = false;
                }
            }

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

        // Simulate user throughput. Wait time is the buffer size divided by the throughput.
        //AKPLATFORM::AkSleep( DWORD( 1000*uBufferSize / heuristics.fThroughput ) );
        AKPLATFORM::AkSleep( 0 );
    }

autostmbasictest_cleanup:

    if ( pStream )
        pStream->Destroy();

    return bSuccess;
}