Wwise SDK 2021.1.9
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.
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
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::IAkIOHookDeferredBatch. Whenever a streaming device needs to perform an I/O transfer, it calls the low-level I/O hook's
AK::StreamMgr::IAkIOHookDeferredBatch::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. 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.
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.
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).
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::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::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() 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::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).
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
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
Caution: Deferring file opens has a cost. File open data (flags and filename) have to be stored until
The Stream Manager's
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.
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).
Following an explicit or implicit request to load a bank from the main API (
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.
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 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.
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.
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::IAkIOHookDeferredBatch). These two interfaces inherit from
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::IAkIOHookDeferredBatch. An instance of the appropriate low-level I/O hook must be passed to
Caution: Passing an instance of
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.
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
The blocking interface is simpler than its deferred counterpart. It defines two methods,
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
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.
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.
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
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::IAkIOHookDeferredBatch::BatchCancel(). BatchRead() and BatchWrite() should return immediately, and notify the streaming device when one or more transfers are complete through the provided 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).
For each transfer request sent to
AK::StreamMgr::IAkIOHookDeferredBatch::BatchWrite(), you must fill in a list of results indicating if each dispatch succeeded, with AK_Success, or failed, with AK_Fail. You must also return AK_Success if all of the requests were successfully dispatched, or AK_Fail if any of the dispatches failed. The Low-Level I/O system will investigate the list of dispatch results to identify which transfer failed only if AK_Fail is returned.
Similarly, the callback function must be provided a list of results of each transfer, and each value must be AK_Success if the corresponding transfer successfully completed, or AK_Fail otherwise.
If any transfer is marked as AK_Fail when dispatching or completing transfers, the corresponding stream will be destroyed, and an "I/O error" notification will appear in the transfer log.
Caution: If you mark a transfer with a dispatch result of AK_Fail from
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
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 array of BatchIoTransferItem's to each of
AK::StreamMgr::IAkIOHookDeferredBatch'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::IAkIOHookDeferredBatch::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.|
|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.|
Tip: Do not feel compelled to implement
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.|
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.
Default implementations of the Low-Level I/O are provided with the Wwise SDK. They are located in the samples/SoundEngine/ directory.
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.
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::IAkIOHookDeferredBatch) or an adapter-interface to the non-batched deferred I/O hook (
AK::StreamMgr::IAkIOHookDeferred). Either implementation 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:
Here is how you would initialize the I/O system with CAkDefaultIOHookBlocking used alone (without error handling).
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.
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.
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.
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.
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.
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.|
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:
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).
|Tip: You should only use multiple devices with multiple physical devices.|
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.
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:
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.
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.