Table of Contents

Wwise SDK 2019.1.6
Low-Level I/O
Default Streaming Manager Information

The Low-Level I/O is a submodule of the default implementation of the high-level Stream Manager's API, which serves to provide an interface for I/O transfers that you will find much simpler to implement than the high-level Stream Manager API. It is therefore only relevant in the context of the default implementation of the Stream Manager.

Introduction

The Low-Level I/O system is specific to Audiokinetic's implementation of the Stream Manager. Its interfaces are defined in <Wwise Installation>/SDK/include/AK/SoundEngine/AkStreamMgrModule.h.

The Low-Level I/O system has two purposes:

  • Resolving file location.
  • Abstracting actual I/O transfers.

One and only one object that implements AK::StreamMgr::IAkFileLocationResolver, referred to as the File Location Resolver, must be registered to the Stream Manager (using AK::StreamMgr::SetFileLocationResolver()). The Stream Manager calls AK::StreamMgr::IAkFileLocationResolver::Open() whenever it creates a standard or automatic stream. This method must return a valid file descriptor (AkFileDesc) which contains, along with file size and other information, the ID of the high-level streaming device that must be used to stream this file. Streaming devices are created and registered in the Stream Manager using AK::StreamMgr::CreateDevice().

For each streaming device, a low-level I/O hook must be provided. The I/O hook implements one of these two interfaces: AK::StreamMgr::IAkIOHookBlocking or AK::StreamMgr::IAkIOHookDeferred. Whenever a streaming device needs to perform an I/O transfer, it uses the low-level I/O hook' Read() or Write() method. No direct call to a platform's I/O API is ever issued from the High-Level Stream Manager. Together, the File Location Resolver and I/O hooks constitute the Low-Level I/O system. Everything that is specific to storage device constraints, files, and platform I/O is handled by the Low-Level I/O system.

The following figure illustrates the Low-Level I/O system interfaces and how they are viewed by the default Stream Manager.

LowLevelIO.gif

Game titles need to implement Low-Level I/O interfaces to resolve file location and perform actual I/O transfers. The easiest and most efficient way to integrate the Wwise sound engine's I/O management in your game is to use the default implementation of the Stream Manager and implement a Low-Level I/O system. From there, you can perform native file reads, or you can route the I/O requests to your own I/O management technology.

Wwise's SDK includes default implementations for the Low-Level I/O. It can be used as is, or as a starting point to implement your own. Refer to the Sample Default Implementation Walkthrough section for a detailed overview of the Low-Level I/O samples.

File Location Resolving

File Location Resolver

A File Location Resolver needs to be registered to the Stream Manager using AK::StreamMgr::SetFileLocationResolver(). Whenever the Stream Manager creates a stream object, it calls this instance's Open() method. AK::StreamMgr::IAkFileLocationResolver::Open() must fill the fields of a file descriptor structure (AkFileDesc). The same number of files exist simultaneously in the Low-Level I/O as stream objects in the Stream Manager.

The game must create at least one streaming device in the Stream Manager using AK::StreamMgr::CreateDevice(). It may however create as many devices as required. Each streaming device runs in its own thread, and sends I/O requests to its own I/O hook. Typically, you should create one streaming device per physical device. In the AkFileDesc structure, there is a field called deviceID. The File Location Resolver needs to set it to the deviceID of one of the streaming devices that were created. This is how file handling is dispatched to the appropriate device. Additionally, the file's size and offset (uSector) must be returned, and a system file handle should be created (although this is not necessary).

File Description

Clients of the Stream Manager identify files either using strings (const AkOSChar *), IDs (AkFileID, integer), or file descriptors. This is why the stream creation methods of the Stream Manager (AK::IAkStreamMgr::CreateStd(), AK::IAkStreamMgr::CreateAuto()) have two overloads. The File Location Resolver has the task of mapping file names and file IDs to file descriptors, hence the two overloads for AK::StreamMgr::IAkFileLocationResolver::Open(). File descriptors are used to identify data sources for all low-level transfers and information queries.

AK::StreamMgr::IAkFileLocationResolver::Open() must fill the file descriptor. This file descriptor is passed back in every call to methods of the low-level I/O hooks. Three members of the structure are used by the High-Level Stream Manager:

  • deviceID: Needs to be a valid device ID acquired from a call to AK::CreateDevice(). It is used by the Stream Manager to associate the file to the proper high-level device. Once the association is made, I/O transfers are executed through the I/O hook that was passed when creating the device.
  • uSector: The offset of the beginning of the described file. This is relative to the beginning of the file represented by the AkFileHandle AkFileDesc::hFile. It is expressed in terms of blocks (sectors), which size corresponds to the value returned by AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() for this file descriptor. When the streaming device calls a method for I/O transfer, it sends the offset (in bytes) from the beginning of the file to its low-level I/O hook as part of the AkTransferInfo structure. The offset is computed this way: Current_Position + ( AkFileDesc::uSector * Block_Size). Note that the block size is also queried through this hook, once for each file descriptor.
  • iFileSize: Used by the Stream Manager to detect the end of file. It can then report it back to users (through AK::IAkStdStream::GetPosition(), AK::IAkAutoStream::GetPosition()) and stop automatic streams' I/O transfers.

The remaining members are owned exclusively by the Low-Level I/O system. For example, AkFileHandle, which is typedefined to HANDLE in Win32, can be used to hold an actual valid file handle that is passed to Win32's ReadFile(). However, it can also be used as an ID, or a pointer. High-level devices never modify or read file descriptor fields. Thus, the Low-Level I/O is free to close and reopen a file handle, for example, if files have been laid out redundantly on the game disk.

Tip: The address of the file descriptor structure in_fileDesc that is passed to AK::StreamMgr::IAkFileLocationResolver::Open() remains the same throughout the lifetime of its associated stream object.

Deferred Opening

AK::StreamMgr::IAkFileLocationResolver::Open() accepts another argument, io_bSyncOpen. When io_bSyncOpen is true, it means that the file must be opened immediately and its descriptor must contain valid information. However, when io_bSyncOpen is false, the File Location Resolver may decide to wait until later before opening the file. Why wait? Because AK::IAkStreamMgr::CreateStd() or AK::IAkStreamMgr::CreateAuto() calls AK::StreamMgr::IAkFileLocationResolver::Open(), in the client's thread. If file opening takes significant time, it may be a good idea to defer opening to the I/O thread. For example, the sound engine creates streams in the audio thread, which blocks on Open(). If it takes a lot of time, you may hear audio stuttering (voice starvation).

When io_bSyncOpen is false, you may or may not open the file immediately. If you do, you need to fill the AkFileDesc structure and set the flag to true to indicate that you opened it. If you don't, you don't need to do anything, except to set the deviceID field of the AkFileDesc. This is very important: you need to tell the Stream Manager which device's thread will take care of this file. Once the file has been dispatched to a device, it cannot be changed. Further modifications to deviceID are ignored.

Caution: You may not defer opening of a file that was requested to be opened immediately. In other words, you may not reset io_bSyncOpen.

If you defer opening, the streaming device specified by AkFileDesc::deviceID will take ownership of this stream object. Before calling Read() on its I/O hook, from its own thread, it will call AK::StreamMgr::IAkFileLocationResolver::Open() again, but this time, with io_bSyncOpen set to true.

Caution: Deferring file opens has a cost. File open data (flags and filename) have to be stored until AK::StreamMgr::IAkFileLocationResolver::Open() is called again. Avoid deferring file opens when it is not necessary.

File System Flags

The Stream Manager's AK::IAkStreamMgr::CreateStd() and AK::IAkStreamMgr::CreateAuto() methods accept a pointer to an AkFileSystemFlags structure, which is passed to AK::StreamMgr::IAkFileLocationResolver::Open(). This structure is a way to pass information directly from the users to the Low-Level I/O. This information is used to complete the file location logic, for example. Generic users of the Stream Manager can pass NULL, but the sound engine always passes a structure filled with relevant information, so that the File Location Resolver knows that a request comes from the sound engine.

The file system flag structure contains the following fields:

  • uCompanyID: the sound engine always sets this field to AKCOMPANYID_AUDIOKINETIC, so the implementer of the Low-Level I/O knows that this file must be read by the sound engine. The sound engine passes AKCOMPANYID_AUDIOKINETIC_EXTERNAL in the case of streamed external sources.
  • uCodecID: this field can be used to distinguish between file types. It is needed for file types used by the sound engine. The codec IDs used by the sound engine are defined in AkTypes.h. The host program may also define its own IDs with the same values, as long as it does not set the companyID to AKCOMPANYID_AUDIOKINETIC or AKCOMPANYID_AUDIOKINETIC_EXTERNAL.
  • bIsLanguageSpecific: this field indicates whether the file being looked up is specific to the current language. Typically, a file that has language-specific content exists in different locations. The Low-Level I/O needs to resolve the path according to the language currently selected. See Language-Specific ("Voice" and "Mixed") Soundbanks for more details.
  • Custom parameter and size: Used for game-specific extensions of the file location scheme. For example, extensions can be copied in the file descriptor. The sound engine always passes 0.

Resolution of the Sound Engine Files

The sound engine reads SoundBank files and streamed audio files. This subsection explains how the Low-Level I/O can resolve their identifiers to actual files.

Different strategies exist to map the ID received in AK::StreamMgr::IAkFileLocationResolver::Open() to a valid file descriptor in the Low-Level I/O:

  • implement and use a map of IDs to file names
  • create file name strings out of IDs
  • get the system handle of a big file that concatenates all streamed audio files (for e.g. a file package generated with the File Packager application - refer to the Wwise Help for more details), and implement and use a map of IDs to file descriptor structures that define their size and offset in this big file.

The SDK samples of the Low-Level I/O resolve file location using two different strategies. One of them (CAkFileLocationBase) concatenates paths, set globally, to create a full path string that can be used with platform fopen() methods. The other one (CAkFilePackageLowLevelIO) manages a file package that was created with the File Packager utility. A file package is simply a big file in which many files are concatenated, with a header that indicates the relative offset of each original file.

Implementations of File Location Resolvers are provided as SDK samples. They use a different strategy to manage file location. These strategies are described in the sample code walkthroughs, at the end of this section.

Refer to Basic File Location for a description of the strategy used by the default implementations of the Low-Level I/O.

Refer to File Location for a description of the strategy used by implementations of the Low-Level I/O that use File Packages (CAkFilePackageLowLevelIO).

SoundBanks

Following an explicit or implicit request to load a bank from the main API (AK::SoundEngine::LoadBank() and AK::SoundEngine::PrepareEvent()), the Bank Manager of the sound engine may call either the ANSI string or the ID overload of AK::IAkStreamMgr::CreateStd(). For a detailed explanation on the conditions that determine which overload is chosen, refer to Loading Banks From File System.

In both cases, AKCOMPANYID_AUDIOKINETIC is used as the company ID in the file system flags, and AKCODECID_BANK is used as the codec ID.

The LoadBank() methods of the sound engine API do not expose a flag to specify whether or not the bank is language-specific. It is up to your implementation of the Low-Level I/O to resolve this. Since the sound engine is not aware of the SoundBank's language specificity, it calls the Stream Manager with the bIsLanguageSpecific flag set as True. If the Stream Manager (Low-Level I/O) fails to open it, the sound engine tries again, this time with the bIsLanguageSpecific flag set as False. Refer to Language-Specific ("Voice" and "Mixed") Soundbanks for more information on working with language-specific banks in the Wwise SDK.

Tip: If you want to avoid having the Bank Manager call the Stream Manager twice for unlocalized banks, you may ignore the bIsLanguageSpecific flag and open the soundbank directly, at the correct location. Only you know what and where the soundbanks are. Also, the cookie that you pass to asynchronous versions of AK::SoundEngine::LoadBank() is passed to the AK::StreamMgr::IAkFileLocationResolver::Open() as the value of in_pFlags->pCustomData. You may use it to help you determine if the bank should be opened from the language specific directory.

Streamed Audio Files

Streamed file references are stored in banks as integer IDs. The real file paths of the converted audio files that are meant to be streamed can be found in the SoundBanksInfo.xml file, generated along with the banks (refer to SoundBanksInfo.xml for details).

Tip: Selecting "Copy streamed files" in Wwise's SoundBank settings to automatically copy files in the Generated SoundBanks folder of the specific platform(s), renamed with the scheme [ID].[ext]. See Streamed Audio Files for more information. The default file location implementation (CAkFileLocationBase) is meant to be used with the "Copy streamed files" option.

When the sound engine wants to play a streamed audio file, it calls the ID overload of AK::IAkStreamMgr::CreateAuto(). This propagates down to the ID overload of AK::StreamMgr::IAkFileLocationResolver::Open(). Along with the ID, it passes an AkFileSystemFlags structure with the following information:

  • uCompanyID is AKCOMPANYID_AUDIOKINETIC
  • uCodecID is one of the audio formats defined in AkTypes.h (AKCODECID_XXX).
  • bIsLanguageSpecific flag that is true when the file needs to be searched for in a location that depends on the current game language, false otherwise.

I/O Transfer Interface

Once file location has been resolved, the Stream Manager hands the file descriptor to the appropriate streaming device, which interacts with the Low-Level I/O system through its own I/O hook. The first thing it does is to query the low-level block size constraint, by calling AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize(). Then, every input data transfer is executed through the hook's Read() method, and output through the hook's Write() method. When the stream is destroyed, AK::StreamMgr::IAkLowLevelIOHook::Close() is called.

Each of these methods are passed the same file descriptor that was filled by the File Location Resolver.

High-Level Devices Specificity

The current implementation of the Stream Manager defines two types of streaming devices, and consequently, two types of I/O hooks. One of them uses synchronous handshaking with the Low-Level I/O (AK::StreamMgr::IAkIOHookBlocking) whereas the other uses asynchronous handshaking (AK::StreamMgr::IAkIOHookDeferred). These two interfaces inherit from AK::StreamMgr::IAkLowLevelIOHook.

AK::StreamMgr::CreateDevice() is passed a structure called AkDeviceSettings which contains the device-wide initialization settings. One of them, AkDeviceSettings::uSchedulerTypeFlags, specifies the type of high-level I/O scheduler. Set it to AK_SCHEDULER_BLOCKING if you want to create a blocking device meant to work with AK::StreamMgr::IAkIOHookBlocking, or AK_SCHEDULER_DEFERRED_LINED_UP if you want to create a deferred device meant to work with AK::StreamMgr::IAkIOHookDeferred. An instance of the appropriate low-level I/O hook must be passed to AK::StreamMgr::CreateDevice().

Caution: Passing an instance of AK::StreamMgr::IAkIOHookDeferred to an AK_SCHEDULER_BLOCKING device, or vice-versa, is illegal, but will fail silently at creation time and crash at run-time.

AK::StreamMgr::CreateDevice() returns a device ID that is set in the file descriptor structure by the File Location Resolver.

The following two sections describe the blocking and deferred I/O hooks, respectively.

Blocking I/O Hook

When AK_SCHEDULER_BLOCKING flag is specified, the Stream Manager creates a streaming device which interacts with the Low-Level I/O system through the AK::StreamMgr::IAkIOHookBlocking interface.

The blocking interface is simpler than its deferred counterpart. It defines two methods, AK::StreamMgr::IAkIOHookBlocking::Read() and AK::StreamMgr::IAkIOHookBlocking::Write(), which should return only when I/O transfer is complete.

Caution: Do not confuse synchronous I/O transfers at the Low-Level I/O level with synchronous stream access at the Stream Manager level (or even at the sound engine level, in AK::SoundEngine::LoadBank()). They are completely unrelated. Recall that streaming devices run in their own thread. This thread decouples low-level I/O transfers with high-level IAkStdStream and IAkAutoStream access. Therefore, the blocking low-level I/O hook handles non-blocking stream access of the Stream Manager as well as its deferred counterpart, and vice-versa.

The AK_SCHEDULER_BLOCKING device selects the high-level stream object that should be serviced in priority, according to an algorithm that is written inside the device's implementation. Then it calls Read() or Write() with the file descriptor associated with this stream object. It also passes a structure AkIOTransferInfo which specifies the position in file where to start the transfer and the requested transfer size, and a pointer to the address of the buffer to write data to, or to read data from. Additionally, it passes heuristics for this transfer. Transfer heuristics are defined by the structure AkIoHeuristics, and contain the deadline in milliseconds, and a priority value between AK_MIN_PRIORITY and AK_MAX_PRIORITY. The streaming device's scheduler is made so that the transfer request with smallest deadline and highest priority is sent to the Low-Level I/O.

Note:

Inside Read() or Write(), the data transfer must be executed completely. If the transfer was completed successfully, the function should return AK_Success. Otherwise it should return AK_Fail.

Deferred I/O Hook

When AK_SCHEDULER_DEFERRED_LINED_UP flag is specified, the Stream Manager creates a streaming device which interacts with the Low-Level I/O system through the AK::StreamMgr::IAkIOHookDeferred interface.

The deferred interface is more complex than its blocking counterpart. It is meant to be used with Low-Level I/O implementations that handle multiple transfer requests at the same time. It defines 3 methods, AK::StreamMgr::IAkIOHookDeferred::Read(), AK::StreamMgr::IAkIOHookDeferred::Write() and AK::StreamMgr::IAkIOHookDeferred::Cancel(). Read() and Write() should return immediately, and notify the streaming device that the transfer is complete through a callback function. You specify the maximum number of concurrent I/O transfers that the streaming device may send to the Low-Level I/O in its initialization settings (AkDeviceSettings::uMaxConcurrentIO).

If you pass AK_Fail (or anything other than AK_Success) to the callback function, or return AK_Fail from AK::StreamMgr::IAkIOHookDeferred::Read()/Write(), the stream will be destroyed an an "I/O error" notification will appear in the transfer log.

Caution: If you return AK_Fail from AK::StreamMgr::IAkIOHookDeferred::Read() (or Write()), the streaming device will not be waiting for the callback function, and you should therefore not call it.

Tip: AK_SCHEDULER_DEFERRED_LINED_UP devices are useful when your implementation achieves better performance when dealing with many requests at the same time. If it is not the case, then you should use an AK_SCHEDULER_BLOCKING device. For example, there is no gain using Win32 OVERLAPPED ReadFile() instead of blocking ReadFile. Recall that ReadFile() is already decoupled from the Stream Manager interface by the I/O thread.

On the other hand, deferred devices have some drawbacks. They use more memory, and I/O transfers are more likely to get flushed or cancelled, because they are issued earlier than with the blocking device. For example, the scheduler may post several transfers for a stream at once, and if the sound engine calls SetPosition(), all unresolved transfers will be flushed upon completion.

Note: AkDeviceSettings::uMaxConcurrentIO represents the maximum number of transfer requests that the device may post to the Low-Level I/O. Otherwise the AK_SCHEDULER_DEFERRED_LINED_UP device's scheduler works exactly like the AK_SCHEDULER_BLOCKING scheduler: it decides to post transfer requests only when clients of the Stream Manager call AK::IAkStdStream::Read()/Write(), or when a running automatic stream's buffering is below the buffering target (AkDeviceSettings::fTargetAutoStmBufferLength, refer to Audiokinetic Stream Manager Initialization Settings for more details on the target buffering length).

The streaming device passes an AkAsyncIOTransferInfo structure to Read() and Write(). This structure extends the AkIOTransferInfo structure mentioned earlier with information about the callback function that needs to be called when transfers are completed. Also, the address of the buffer is contained in this structure, instead of being passed to Read() or Write() separately. A pUserData field is also provided to help implementers attach meta data to pending I/O transfers. The AkAsyncIOTransferInfo structure lives until the callback is called. You must not reference it after you called the callback.

An AkIoHeuristics structure is also passed to Read() or Write(). The information it contains may be useful if you route Read() or Write() calls to your own I/O streaming technology, in order to shuffle I/O requests.

Tip: The implementation of the default Stream Manager's scheduler is based on "client heuristics", not on "disk bandwidth heuristics". The Stream Manager is not aware of the layout of files on disk. If your own streaming technology permits it, it can use this knowledge to re-order I/O requests to minimize seeking on disk.

Streaming devices sometimes need to flush data. This may occur when clients of the Stream Manager call AK::IAkAutoStream::SetPosition(), or change the looping heuristics. Sometimes, data may even need to be flushed before the corresponding transfer is complete. This is more likely to occur when AkDeviceSettings::uMaxConcurrentIO and AkDeviceSettings::fTargetAutoStmBufferLength are large. The deferred I/O hook API provides an entry point to get notified when this occurs: Cancel(). When the streaming device needs to flush data that is associated with an I/O transfer that is still pending in the Low-Level I/O, it internally tags this transfer as "cancelled", calls AK::StreamMgr::IAkIOHookDeferred::Cancel(), and waits for the callback to be called. Cancel() is only used to notify the Low-Level I/O, and it may or may not do anything. The streaming device knows what transfers need to be cancelled, so if you let them complete normally instead of cancelling them, they will be flushed upon completion. In all cases, the callback function must be called in order to notify the streaming device that it can freely dispose of the I/O transfer information and buffer.

Caution: Ensure that you never call the callback twice for a given transfer.
Tip:
  • If you implement a queue in the Low-Level I/O, you may use Cancel() to dequeue the request. If you are able to dequeue it, then you may call the callback function directly from within Cancel().
  • You should not block on an physical device controller inside Cancel(). This could block clients of the Stream Manager.
Caution: When calling the callback function of a cancelled transfer, you must pass AK_Success. Anything else will be considered as an I/O error and the associated stream(s) will be terminated.
Caution: AK::StreamMgr::IAkIOHookDeferred::Cancel() may be called from any thread. Consequently, you must be extremely cautious with locking in the Low-Level I/O if you implement AK::StreamMgr::IAkIOHookDeferred::Cancel(). In particular, you need to avoid race conditions between calling back pCallback from Cancel() and from the normal I/O completion code path. More details can be found in the function's description.
Tip: Do not implement AK::StreamMgr::IAkIOHookDeferred::Cancel() just for the sake of it. Because of locking issues, it can sometimes be more costly to try to cancel requests than to let them complete normally.

Other Considerations

Block Size (GetBlockSize())

As mentioned before, users of the Stream Manager must take low-level I/O constraints on allowed transfer sizes into account. The most common constraint is that these sizes be a multiple of some value. This value is returned by AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() for a given file descriptor. For example, a file opened in Windows® with the FILE_FLAG_NO_BUFFERING flag must be read with sizes that are a multiple of the sector size. The AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() method returns the sector size. If, on the other hand, a Win32 file is not opened with that flag, AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() should return 1, so as not to constrain clients of the Stream Manager.

Caution: AK::StreamMgr::IAkLowLevelIOHook::GetBlockSize() must never return 0.
Tip: The burden of dealing with the low-level block size constraint is passed to the client of the Stream Manager. Larger block size values result in more wasted streaming data by the sound engine. You should use a low-level block size of 1 unless the platform's I/O system has specific alignment constraints or unless it helps you improve I/O bandwidth performance significantly.

Profiling

AK::StreamMgr::IAkLowLevelIOHook::GetDeviceDesc() is used for profiling in Wwise. The information provided by the default implementation of the Low-Level I/O is what is actually seen in Wwise when profiling.

AK::StreamMgr::IAkLowLevelIOHook::GetDeviceData() is similar, but it is called at every profiling frame. The value it returns appears in the Custom Parameter column of the Streaming Device tab.

Sample Default Implementation Walkthrough

Default implementations of the Low-Level I/O are provided with the Wwise SDK. They are located in the samples/SoundEngine/ directory.

Classes Overview

The figure below is a class diagram that represents the Low-Level I/O samples and their relation with the Low-Level I/O API.

LowLevelIO_samples.gif

CAkDefaultIOHookBlocking implements the File Location Resolver API (AK::StreamMgr::IAkFileLocationResolver) and the blocking I/O hook (AK::StreamMgr::IAkIOHookBlocking). CAkDefaultIOHookDeferred is equivalent, but implements the deferred I/O hook instead (AK::StreamMgr::IAkIOHookDeferred). Both implementations can be used alone, in a single-device I/O system. CAkDefaultIOHookBlocking::Init() and CAkDefaultIOHookDeferred::Init() both create a streaming device in the Stream Manager, passing it the device settings, and store the returned device ID. The only difference is that you need to pass the AK_SCHEDULER_BLOCKING scheduler type to CAkDefaultIOHookBlocking::Init() in order to successfully initialize it, whereas you need to pass AK_SCHEDULER_DEFERRED_LINED_UP to CAkDefaultIOHookDeferred::Init().

Also, both these devices register themselves to the Stream Manager as the one and only File Location Resolver. But, this is only if there is not already a File Location Resolver registered to the Stream Manager.

The following figure is a block diagram that represents a single-device I/O system. "Low-Level IO" is any class that implements the File Location Resolver API as well as one of the I/O hook APIs. It can be any of these sample classes:

  • CAkDefaultIOHookBlocking
  • CAkDefaultIOHookDeferred
  • CAkFilePackageIOHookBlocking
  • CAkFilePackageIOHookDeferred
LowLevelIO_SingleDevice.gif

Here is how you would initialize the I/O system with CAkDefaultIOHookBlocking used alone (without error handling).

// Create the Stream Manager.
AkStreamMgrSettings stmSettings;
AK::StreamMgr::Create( stmSettings );
// Create a streaming device with blocking low-level I/O handshaking.
AkDeviceSettings deviceSettings;
CAkDefaultIOHookBlocking lowLevelIO;
// Init registers lowLevelIO as the File Location Resolver if it was not already defined, and creates a streaming device.
lowLevelIO.Init( deviceSettings );

If you want to use the CAkDefaultIOHookDeferred device instead, replace CAkDefaultIOHookBlocking by CAkDefaultIOHookDeferred and AK_SCHEDULER_BLOCKING by AK_SCHEDULER_DEFERRED_LINED_UP.

The File Location Resolver implementation inside both these devices uses the services of CAkFileLocationBase, from which they also both inherit. For more details on the file location strategy implemented in CAkFileLocationBase, refer to Basic File Location below.

Read sections Blocking I/O Hook Walkthrough and Deferred I/O Hook Walkthrough below for more details on the implementation of the default blocking and deferred I/O hooks.

There is another class, written as a template, which adds the ability to manage file packages to classes that implement both AK::StreamMgr::IAkFileLocationResolver and AK::StreamMgr::IAkLowLevelIOHook (like CAkDefaultIOHookBlocking and CAkDefaultIOHookDeferred). It is CAkFilePackageLowLevelIO<>. File packages are files that are created using the AK File Packager utility. Refer to section Sample File Package Low-Level I/O Implementation Walkthrough below for more information about file package handling in the Low-Level I/O. Classes CAkFilePackageIOHookBlocking and CAkFilePackageIOHookDeferred are concrete definitions of CAkDefaultIOHookBlocking and CAkDefaultIOHookDeferred, respectively enhanced with file package management.

If you want to implement an I/O system with more than one device, you need to register a separate File Location Resolver to the Stream Manager, which task is to dispatch management of files to appropriate devices. The SDK provides a canvas to implement this functionality: CAkDefaultLowLevelIODispatcher. Refer to Multi-Device I/O System for more details on multi-device I/O systems.

Basic File Location

Files used by the sound engine are opened either with IDs (used for both streamed audio files and SoundBanks) or with strings (generally reserved for SoundBanks). CAkDefaultIOHook[Blocking|Deferred] inherit from CAkFileLocationBase, which exposes methods to set global paths (SetBasePath(), AddBasePath(), SetBankPath(), SetAudioSrcPath()). Both overloads of CAkDefaultIOHook[Blocking|Deferred]::Open() call CAkFileLocationBase::GetFullFilePath(), to create a full file name that can be used with native file open functions. The base path is prepended first. Then, if the file is a SoundBank, the SoundBank path is added. If it is a streamed audio file, the audio source path is added. In both cases, if it is a file with a location dependent on the current language, the language directory name is added.

If the string overload is used, the file name string is appended to this path.

In the ID overload, only Audiokinetic’s file IDs are resolved. A game that uses the ID overload needs to change the implementation according to its ID mapping scheme. The mapping scheme of CAkFileLocationBase is the following: it creates a string based on the file ID, and appends an extension that depends on the file type (specified with the Codec ID). This is compatible with the streamed files naming convention used by the "Copy Streamed Files" post-generation step soundbank setting. Refer to the Wwise Help for more information about the soundbank settings.

Note: When the option "Use SoundBank names" of the SoundBank settings is not selected, Wwise generates bank files with names' in the format [ID].bnk. Therefore, explicit bank loads by ID (through the ID overload of AK::SoundEngine::LoadBank()) and implicit bank loads triggered from AK::SoundEngine::PrepareEvent() will be mapped properly in the Default Low-Level I/O. When "Use SoundBank names" is selected, Wwise generates bank files with their original names (bank_name.bnk). Implicit bank loading, and explicit bank loading by string will be mapped properly. However, explicit bank loading by ID will not work, because the Default Low-Level I/O will try to open a file named [ID].bnk, which does not exist.

Refer to Using SoundBank Names for a discussion on the "Use SoundBank names" option from an SDK point of view, or the Wwise Help for SoundBank settings in general.

Likewise, the Default Low-Level I/O opens streamed audio files with names in the format [ID].[ext]. The [ext] is an extension that depends on the audio format. It is possible to tell Wwise to automatically copy all streamed audio files in the Generated SoundBank path, with the [ID].[ext] file name format, at the end of the SoundBank generation (refer to Streamed Audio Files and the Wwise Help).

After the full file path is obtained, CAkDefaultIOHook[Blocking|Deferred]::Open() opens the file directly using the system API (wrapped in helpers implemented in the platform-specific sample file AkFileHelpers.h).

From the game code you can set the base, SoundBank, audio source, and language-specific paths by using the methods of CAkFileLocationBase mentioned above. Refer to the sample code of the Default Low-Level I/O Implementation for more information.

Tip: The sound engine does not know when banks need to be loaded from a language-specific directory. Therefore, it always calls AK::IAkStreamMgr::CreateStd() with the bIsLanguageSpecific flag of the AkFileSystemFlags structure set to true first, then to false if the first call failed. The sample default implementation of the Low-Level I/O blindly tries to open the file from the current language-specific directory, which is of course inefficient because of the failed calls made to fopen(), which should be avoided.

You should always reimplement the Low-Level I/O to fit your needs. If you know the names of the language-specific SoundBanks, or you defined a nomenclature to identify them, load them from the correct folders early in the process.

Default Implementation of Deferred Opening

Refer to Deferred Opening for a discussion on the io_bSyncOpen flag.

All default I/O hooks (blocking and deferred) are initialized with a flag, in_bAsyncOpen (see CAkDefaultIOHook[Blocking|Deferred]::Init()). If it is true, files are opened asynchronously when possible. This means that if in_bAsyncOpen is true AND the io_bSyncOpen argument of AK::StreamMgr::IAkFileLocationResolver::Open() is false, file opening is deferred. In this case, only the deviceID field of the AkFileDesc structure is set, and the method returns immediately.

Blocking I/O Hook Walkthrough

On all platforms, the implementation of CAkDefaultIOHookBlocking::Read() and CAkDefaultIOHookBlocking::Write() perform a blocking call to the native file read/write function, using the information provided in AkIOTransferInfo. The function returns AK_Success if the operation was successful.

Deferred I/O Hook Walkthrough

Implementation of the deferred I/O hook is slightly more complicated than the blocking I/O hook. Generally, asynchronous file read APIs on platforms require that you pass a platform-specific structure to fread() (OVERLAPPED on Windows), and keep it for the whole duration of the I/O operation, until a callback function gets called to notify you that I/O is complete.

The implementation is somewhat similar on all platforms. CAkDefaultIOHookDeferred allocates an array of these platform-specific structures in its own memory pool. When CAkDefaultIOHookDeferred::Read() is called, it finds the first structure that is free, marks it as "used", fills it with the information provided in AkAsyncIOTransferInfo, and passes it to fread(). It also passes a local static callback function whose signature is compatible with the platform's asynchronous fread() function. In this function, it determines if the operation was successful, releases the platform-specific I/O structure, and calls back the streaming device.

Obtaining and releasing the platform-specific I/O structure from the array must be atomic, as we need to avoid race conditions between Read()/Write() and the system's callback.

Some platforms expose a service to cancel I/O requests that were already sent to the kernel. When this is the case, it is called from within CAkDefaultIOHookDeferred::Cancel(). On Windows, CancelIO() cancels all requests for a given file handle. Therefore, we must ensure that the streaming device wants to cancel all requests for a given file before calling this function, by looking at the argument io_bCancelAllTransfersForThisFile. If it doesn't, then Cancel() does nothing: we just wait until requests are complete. The streaming device knows which ones it needs to discard.

Caution: Do not cancel requests that were not cancelled explicitly by the streaming device. If you do, you may end up feeding it with invalid or corrupted data, which may crash the sound engine.
Caution: CAkDefaultIOHookDeferred uses native asynchronous file read functions, and is really only provided as a canvas to implement your own deferred I/O hook. We do not recommend that you use it as is. You will not achieve better performance than with native blocking calls. In fact, performance may even degrade. This is explained in a note in section Deferred I/O Hook. The purpose of the deferred low-level API is to allow you to handle I/O requests more efficiently with your own streaming technology. Using a Deferred I/O Hook instead of a Blocking I/O Hook may help you decide what kind of hook is more appropriate for you.

Multi-Device I/O System

The following figure represents a multi-device I/O system.

LowLevelIO_MultiDevices.gif

What you need to do to work with multiple streaming devices is to instantiate and register a File Location Resolver that is distinct from the device's low-level I/O hooks. The purpose of this object is to dispatch files to the appropriate device. The strategy you employ to decide which device handles which files is yours to define. You may use CAkDefaultLowLevelIODispatcher as a canvas. The default implementation uses brute force: each device is asked to open the file until one of them succeeds. Thus these devices must also implement the AK::StreamMgr::IAkFileLocationResolver interface. It can be any of the samples provided in the SDK:

  • CAkDefaultIOHookBlocking
  • CAkDefaultIOHookDeferred
  • CAkFilePackageIOHookBlocking
  • CAkFilePackageIOHookDeferred

Here's how you would instantiate both a deferred device and a file package blocking device in a multi-device system (although this example is not useful in practice).

// Create Stream Manager.
AkStreamMgrSettings stmSettings;
AK::StreamMgr::Create( stmSettings );
// Create and register the File Location Resolver.
CAkDefaultLowLevelIODispatcher lowLevelIODispatcher;
AK::StreamMgr::SetFileLocationResolver( &lowLevelIODispatcher );
// Create deferred device.
CAkDefaultIOHookDeferred hookIODeferred;
AkDeviceSettings deviceSettings1;
deviceSettings1.uSchedulerTypeFlag = AK_SCHEDULER_DEFERRED_LINED_UP;
hookIODeferred.Init( deviceSettings1 );
// Add it to the global File Location Resolver.
lowLevelIODispatcher.AddDevice( hookIODeferred );
// Create a blocking device with file package management.
CAkFilePackageIOHookBlocking hookIOBlockingFP;
AkDeviceSettings deviceSettings2;
deviceSettings2.uSchedulerTypeFlag = AK_SCHEDULER_BLOCKING;
hookIOBlockingFP.Init( deviceSettings2 );
// Add it to the global File Location Resolver.
lowLevelIODispatcher.AddDevice( hookIOBlockingFP );
Tip: You should only use multiple devices with multiple physical devices.

Sample File Package Low-Level I/O Implementation Walkthrough

General Description

The CAkFilePackageLowLevelIO<> class is a layer above the Default low-level I/O hooks. It extends the latter by being able to load a file that was generated by the sample File Packager (see File Packager Utility). It uses a more advanced strategy to resolve the IDs into file descriptors. File packages are composed of many concatenated files (streamed audio files and bank files), and their header contains information about these files.

Refer to the File Package Low-Level I/O Implementation for the sample code. You can use the File Package Low-Level I/O "as-is" (CAkFilePackageLowLevelIO[Blocking|Deferred]), along with its counterpart, the File Packager utility. Or, simply consider them as a proof of concept for the implementation of an advanced file location resolving method.

The File Package Low-Level I/O exposes the method CAkFilePackageLowLevelIO::LoadFilePackage(), whose argument is the file name of a package that was generated with the sample File Packager. It opens it using the services of the default implementation, then parses the header and builds the look-up tables. You may load as many file packages as you want. LoadFilePackage() returns an ID that you can use with UnloadFilePackage() to unload it.

The class CAkFilePackage represents a loaded file package, and all data structures and code to handle file look-up is defined in class CAkFilePackageLUT. The CAkFilePackageLowLevelIO<> class overrides some of the methods of the default I/O hooks, to invoke the look-up services of CAkFilePackageLUT. When a file descriptor is not found, or when the request does not concern a file descriptor that belongs to the file package, then the default implementation is called.

File Location

The idea behind file look-up inside packages is the following: you get a file handle from the platform's file open function only once, and then at each call to AK::StreamMgr::IAkFileLocationResolver::Open(), you simply return a file descriptor that uses this file handle, but with an offset that corresponds to the offset of the original file inside the file package (using the uSector field of the file descriptor AkFileDesc). This also has the benefit of allowing more control over the placement of files on disk.

The File Packager utility carefully prepares its header so that the Low-Level I/O only has to cast some pointers to obtain the look-up tables it contains, that is, one for the streamed audio files, and one for the bank files. Look-up tables are arrays of the following structure:

struct AkFileEntry
{
AkFileID fileID; // File identifier.
AkUInt32 uBlockSize; // Size of one block, required alignment (in bytes).
AkInt64 iFileSize; // File size in bytes.
AkUInt32 uStartBlock;// Start block, expressed in terms of uBlockSize.
AkUInt32 uLanguageID;// Language ID. AK_INVALID_LANGUAGE_ID if not language-specific.
};

The table key is the file's fileID. However, corresponding files of different languages have the same fileID, but a different uLanguageID. The File Packager always sorts the file entries by the fileID first, then by the uLanguageID. In CAkFilePackageLowLevelIO::Open(), the ID is passed to CAkFilePackageLUT::LookupFile() (in the string version of Open(), the string is hashed first, using the service of the sound engine API, AK::SoundEngine::GetIDFromString()). CAkFilePackageLUT::LookupFile() selects the appropriate table to search, based on the flags' uCodecID, and performs a binary search by the fileID and uLanguageID keys. If it finds a match, the file entry's address is returned to CAkFilePackageLowLevelIO::Open(), which gathers the necessary information to fill the file descriptor (AkFileDesc).

Tip: Each file package is searched until a match is found. If you use a file package with soundbanks exclusively, and another with streamed files exclusively, then you may modify the implementation to use the AkFileSystemFlags to only look up files in the proper file package, once.

The handle of the file descriptor, hFile, is that of the file package. The file size, iFileSize, was stored directly in the file entry, and so was the starting block, uSector.

Note: Recall that the Stream Manager does not expect a byte offset from the beginning of the file represented by the handle hFile, but rather an offset in terms of blocks ("sectors"). The block size represents the granularity of file positions. The current version of the File Packager uses the same block size for all files, which is specified at the time of generation (using -blocksize switch - refer to Wwise Help for more details on the File Packager's command line arguments). It performs zero-padding so that concatenated files always start on a block boundary.

The File Package low-level I/O uses the uCustomParamSize field of the file descriptor to store the block size. This has 2 purposes:

  • easy access to its block size;
  • distinguish file descriptors that belong to the file package (uCustomParamSize == 0 means that the file was not found in the package). For example, the file handle (which is shared between all files of a package) is not closed in CAkFilePackageLowLevelIO::Close() when the file is part of a package.

Managing Languages

Since Wwise version 2011.2, the current language is set on the default Stream Manager module, using AK::StreamMgr::SetCurrentLanguage(), defined in AkStreamMgrModule.h. Pass the name of the language, without a trailing slash or backslash.

The default low-level I/O implementations inheriting from CAkFileLocationBase get the language name from the Stream Manager, and append it to the base path. The language name should therefore correspond to the name of the directory where are stored localized assets for this particular language.

File packages generated by the File Packager utility may contain one or many versions of the same asset in different languages. Their header contains a string map of language names. The File Package Low-Level I/O listens to language changes on the Stream Manager, and uses the current language name to look-up the correct localized version of packaged localized assets.