Version

menu_open
Wwise SDK 2023.1.9
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()). To open a standard or automatic stream, the Stream Manager will call AK::StreamMgr::IAkFileLocationResolver::GetNextPreferredDevice() to find which streaming device is likely to have the 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. This I/O hook implements AK::StreamMgr::IAkLowLevelIOHook and is responsible for communicating with the platform's I/O API and handles the unique properties and behavior of that physical device. Whenever a streaming device needs to perform an I/O transfer, it calls the low-level I/O hook's AK::StreamMgr::IAkLowLevelIOHook::BatchRead(), or AK::StreamMgr::IAkLowLevelIOHook::BatchWrite() 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.

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

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 resolver's GetNextPreferredDevice() method and it should return the AkDeviceID that identifies the I/O Device to use to open the file. The purpose of this function is to find the file location only from its name, flags, language, etc. without performing a disk operation or other physical check. The real Open call will be forwarded to the specified device to perform the disk operation. Therefore, this function is expected to return quickly. If the Device fails to find or open the file, GetNextPreferredDevice will be called again. If no other Devices are known, or the file is unlikely to be found in other locations, then AK_FileNotFound should be returned to notify the end of the search.

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(). The returned AkDeviceID should be kept by the caller and returned in AK::StreamMgr::GetNextPreferredDevice as described above. 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 strictly necessary).

File Description

Clients of the Stream Manager identify files either using strings (const AkOSChar *), IDs (AkFileID, integer), or file descriptors (AkFileDesc). This is why the stream creation methods of the Stream Manager (AK::IAkStreamMgr::CreateStd(), AK::IAkStreamMgr::CreateAuto()) use a struct call AkFileOpenData that groups both possible file identification methods. The AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() method must create a valid file descriptor, once the file is opened.

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.

The file descriptor structure that is returned by AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() needs to remains the same throughout the lifetime of its associated stream object, until AK::StreamMgr::IAkLowLevelIOHook::Close() is called.

Deferred Opening

All file open operations carried out by AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() can be synchronous or deferred. If the native API of the device supports deferred opening, it is always better to use it for performance reasons. This way the file open and read/write operations might be carried out in parallel, or prioritized differently, on certain systems.

The parameter to Open() is an AkAsyncFileOpenData struct which derives from AkFileOpenData that contains all the information passed from the AK::IAkStreamMgr::CreateStd() or AK::IAkStreamMgr::CreateAuto() functions. It also contains a callback function necessary to notify the completion of the Open operation to the high-level Streaming Manager and the return AkFileDesc structure to fill out.

All calls to AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() are considered asynchronous, from the Stream Manager perspective. Once the open operation is resolved, signal the result of the operation through the notification callback (io_pOpenData->pCallback). It is imperative to notify of the result once it is known, otherwise the stream object waiting for this file will stay alive indefinitely and cause a memory leak. If the result reported by io_pOpenData->pCallback is AK_Success, it is expected that io_pOpenData->pFileDesc is filled properly and usable. Note that it is supported to call io_pOpenData->pCallback right away (synchronously) if the result of the operation is known at the moment of the BatchOpen() call.

Tip: The AkAsyncFileOpenData (io_pOpenData) parameter that is passed to AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() will stay valid until the notification io_pOpenData->pCallback is received.

If the file is opened successfully, set in_eResult to AK_Success and create the file descriptor structure (io_pOpenData->pFileDesc) properly. The pFileDesc member must point to memory that will be valid for the lifetime of the stream object, until Close() is called. Use AK_FileNotFound if the file was not found and set io_pOpenData->pFileDesc to null. Other error codes are possible, such as AK_FilePermissionError, AK_FilePathTooLong or AK_UnknownFileError.

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::GetNextPreferredDevice() and AK::StreamMgr::IAkLowLevelIOHook::BatchOpen(). 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. Note the predefined CodecID AKCODECID_BANK, AKCODECID_BANK_EVENT, AKCODECID_BANK_BUS which, on top of the traditional Codecs, can be useful for file organization.
  • 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 (language folders typically). 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::IAkLowLevelIOHook::BatchOpen() 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 and your file organization scheme allows you to find it regardless of language, 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::IAkLowLevelIOHook::BatchOpen() 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 Loose/Streamed Media" 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::IAkLowLevelIOHook::BatchOpen(). 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. By default, loading soundbanks query the language-specific folder first.

I/O Transfer Interface

Once file location has been resolved through AK::IAkFileLocationResolver::GetNextPreferredDevice(), the Stream Manager then calls AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() on the selected 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 BatchRead() method, and output through the hook's BatchWrite() 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 a single type of I/O hook, which uses asynchronous handshaking with the Low-Level I/O.

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

The following section describes the deferred I/O hook.

Deferred I/O Hook

The Stream Manager creates a streaming device which interacts with the Low-Level I/O system through the AK::StreamMgr::IAkLowLevelIOHook interface.

Low-Level I/O implementations must handle multiple transfer requests at the same time. The interface defines a few important methods, AK::StreamMgr::IAkLowLevelIOHook::BatchOpen(),AK::StreamMgr::IAkLowLevelIOHook::BatchRead(), AK::StreamMgr::IAkLowLevelIOHook::BatchWrite() and AK::StreamMgr::IAkLowLevelIOHook::BatchCancel(). BatchRead() and BatchWrite() should return immediately, and notify the streaming device when one or more transfers are complete through the provided callback function. See details about AK::StreamMgr::IAkLowLevelIOHook::BatchOpen() in the section Deferred Opening.

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

For each transfer request sent to AK::StreamMgr::IAkLowLevelIOHook::BatchRead() or AK::StreamMgr::IAkLowLevelIOHook::BatchWrite(), the provided callback function in AkAsyncIOTransferInfo must be called once, with the result of the transfer. It must be called even in failure cases, otherwise the engine will wait for this transfer forever. The callback can be deferred to later, as it is common for asynchronous I/O subsystems to execute the transfer in the background.

The in_eResult parameter of the callback should be AK_Success if the transfer succeeds or any other error code if it fails. If any transfer is marked as AK_Fail, the corresponding stream will be destroyed, and an "I/O error" notification will appear in the transfer log.

Note: AkDeviceSettings::uMaxConcurrentIO represents the maximum number of transfer requests that the device may post to the Low-Level I/O. The device's scheduler 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 array of BatchIoTransferItem's to each of AK::StreamMgr::IAkLowLevelIOHook's functions. This structure contains information on each transfer, including the AkFileDesc, AkIoHeuristics, and an AkAsyncIOTransferInfo. The AkAsyncIOTransferInfo structure extends the AkIOTransferInfo structure mentioned earlier. AkAsyncIOTransferInfo includes the address of the buffer to read to or write from, and a pUserData field is provided to help implementers attach metadata to the pending transfer. The AkAsyncIOTransferInfo structure lives until the callback is called. You must not reference it after you called the callback.

The information contained in the provided AkIoHeuristics may be useful if you route reads or writes to your own I/O streaming technology, in order to re-order 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: BatchCancel(). When the streaming device needs to flush data that is associated with one or more I/O transfers that are still pending in the Low-Level I/O, it internally tags the transfers as "cancelled", calls AK::StreamMgr::IAkLowLevelIOHook::BatchCancel(), and waits for the callback to be called. BatchCancel() 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 BatchCancel() to dequeue the request. If you are able to dequeue it, then you may call the callback function directly from within BatchCancel().
  • You should not block on an physical device controller inside BatchCancel(). 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::IAkLowLevelIOHook::BatchCancel() 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::IAkLowLevelIOHook::BatchCancel(). In particular, you need to avoid race conditions between calling back pCallback from BatchCancel() and from the normal I/O completion code path. More details can be found in the function's description.
Tip: Do not feel compelled to implement AK::StreamMgr::IAkLowLevelIOHook::BatchCancel() 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.

CAkDefaultIOHookDeferred implements the File Location Resolver API (AK::StreamMgr::IAkFileLocationResolver) and the deferred I/O hook (AK::StreamMgr::IAkLowLevelIOHook). This implementation can be used in a single-device I/O system. CAkDefaultIOHookDeferred::Init() creates a streaming device in the Stream Manager, passing it the device settings, and store the returned device ID.

Also, this device registers itself 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:

  • CAkDefaultIOHookDeferred
  • CAkFilePackageIOHookDeferred

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

// Create the Stream Manager.
AkStreamMgrSettings stmSettings;
AK::StreamMgr::Create( stmSettings );
// Create a streaming device.
AkDeviceSettings deviceSettings;
CAkDefaultIOHookDeferred lowLevelIO;
// Init registers lowLevelIO as the File Location Resolver if it was not already defined, and creates a streaming device.
lowLevelIO.Init( deviceSettings );

The File Location Resolver implementation inside the device uses the services of CAkFileLocationBase, from which it also inherits. For more details on the file location strategy implemented in CAkFileLocationBase, refer to Basic File Location below.

Read section Deferred I/O Hook Walkthrough below for more details on the implementation of the default deferred I/O hook.

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 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. Class CAkFilePackageIOHookDeferred is a concrete definition of CAkDefaultIOHookDeferred, 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). CAkDefaultIOHookDeferred inherit from CAkFileLocationBase, which exposes methods to set global paths (SetBasePath(), AddBasePath(), SetBankPath(), SetAudioSrcPath()). Both overloads of CAkDefaultIOHookDeferred::BatchOpen() 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, CAkDefaultIOHookDeferred::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.

Deferred I/O Hook Walkthrough

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.

Multi-Device I/O System

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

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:

  • CAkDefaultIOHookDeferred
  • CAkFilePackageIOHookDeferred

Here's how you would instantiate both a deferred device and a file package 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 a first device.
CAkDefaultIOHookDeferred hookIODeferred;
AkDeviceSettings deviceSettings1;
hookIODeferred.Init( deviceSettings1 );
// Add it to the global File Location Resolver.
lowLevelIODispatcher.AddDevice( hookIODeferred );
// Create a second device with file package management.
CAkFilePackageIOHookDeferred hookIOFilePackage;
AkDeviceSettings deviceSettings2;
hookIOFilePackage.Init( deviceSettings2 );
// Add it to the global File Location Resolver.
lowLevelIODispatcher.AddDevice( hookIOFilePackage );
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" (CAkFilePackageLowLevelIODeferred), 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::IAkLowLevelIOHook::BatchOpen(), 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.

AKSOUNDENGINE_API void GetDefaultSettings(AkStreamMgrSettings &out_settings)
AkUInt32 AkFileID
Integer-type file identifier.
Definition: AkTypes.h:77
int64_t AkInt64
Signed 64-bit integer.
AKSOUNDENGINE_API IAkStreamMgr * Create(const AkStreamMgrSettings &in_settings)
AKSOUNDENGINE_API void GetDefaultDeviceSettings(AkDeviceSettings &out_settings)
AKSOUNDENGINE_API void SetFileLocationResolver(IAkFileLocationResolver *in_pFileLocationResolver)
uint32_t AkUInt32
Unsigned 32-bit integer.

Was this page helpful?

Need Support?

Questions? Problems? Need more info? Contact us, and we can help!

Visit our Support page

Tell us about your project. We're here to help.

Register your project and we'll help you get started with no strings attached!

Get started with Wwise