Table of Contents
Wwise SDK 2019.2.3
Default Streaming Manager Information
The collection of tips and other I/O oriented considerations described in this chapter are only relevant if you use the default implementation of the high-level Stream Manager's API and hook your I/O code below the Low-Level I/O interface. For the most part, understanding of the default Stream Manager's settings (Audiokinetic Stream Manager Initialization Settings) and of the Low-Level I/O interface (Low-Level I/O) is assumed.
The default device settings (obtained from AK::StreamMgr::GetDefaultDeviceSettings) are appropriate for a DVD device. The default granularity is 16 KB, but larger granularities often result in better DVD throughput for a single stream. 32 KB seems to be a good choice for most DVD drives. Use a larger granularity only if you never stream more than one or two audio files. Otherwise, you might optimize the throughput of a single stream at the expense of others. Also, be aware that a larger granularity requires more stream I/O memory.
If you use the soundengine to load soundbanks from files, it may also be beneficial to modify one of the sound engine initialization settings, AkInitSettings::uBankReadBufferSize. This is the amount of data requested at a time when reading in soundbank data from a file. Larger values will issue fewer reads, or fewer sets of reads, at the cost of more memory. This will have no effect if you are loading soundbanks from memory.
At some point, you will probably wish to optimize the memory usage. You may find useful information about this in Reducing the I/O Pool Memory Usage.
Using a deferred I/O hook instead of a blocking I/O hook is generally less efficient. This is because deferred I/O devices make more false predictions (resulting in cancelled transfers and wasted bandwidth) and require more memory in the Stream Manager pool. Use such a hook only if one of the following situations apply.
If your I/O manager only exposes an asynchronous API and you wish to route the Wwise I/O calls to it, it is much easier for you to implement an adapter layer with a deferred I/O hook. In order to reduce false predictions, typical to the deferred I/O device, set AkDeviceSettings::uMaxConcurrentIO to 1 or 2.
The Wwise Stream Manager is incapable of taking decisions according to where files are physically located on disk. If your I/O manager can shuffle the pending requests order based on its knowledge of file placement, favoring transfers that are close next to one another for example, it may be more beneficial to receive more transfers from the Wwise Stream Manager at once, and to push them into your own scheduler.
Some devices resolve transfer requests periodically, based on mechanisms that are mostly independent from the number of transfers that are issued. This is often the case with DMA controller-based devices. Transfers are completed when DMA completes, but you don't want to wait until it does before preparing another DMA; in this case, use a deferred device with a large AkDeviceSettings::uMaxConcurrentIO. It should be large enough so that it never prevents I/O requests to be sent in to the Low-Level I/O. The amount of bandwidth is therefore solely controlled by the target buffering length.
Generally, you should use a different streaming device for each physical device (DVD, HDD, RAM/VRAM, and so on). The first reason is that each streaming device runs in a separate thread, which is mandatory if you don't want transfer requests to be serialized among your physical devices. It would be inefficient to wait for a DVD read before issuing an HDD read. The second reason is that optimal settings are usually different for each device.
When using multiple devices, you need a dispatcher. Refer to Multi-Device I/O System for more details on implementing a system with multiple devices.
Dispatching requires that you know which files should be opened from which device. The preferred solution is to use file packages. After generating soundbanks, soundbanks and streamed audio files can be packed together in various file packages using the File Packager. Each file package is meant to be loaded in a specific device. At run-time, a simple dispatcher could query each device until one of them accepts to open the file. This is what the default dispatcher does (see AkDefaultLowLevelIODispatcher.h/cpp in the samples of the SDK). With this technique, you may even implement fallback mechanisms. For example, try to load a file from the RAM/VRAM device, and if it does not work, load it from the DVD.
Note that file dispatching/device assignation must be synchronous: you cannot defer it, so ensure that it is fast. File package look up implemented in the samples of the SDK is fast, because it uses a binary search algorithm.
Connect the Wwise tool to your game and use the profiler to troubleshoot and optimize your I/O system and settings. The capture log informs you of source starvations and other I/O errors. The two tabs of the Advanced Profiler view are solely dedicated to I/O: the Streams tab and the Streaming Devices tab.
Also, you may use the custom parameters of AkFileDesc, as well as AK::StreamMgr::IAkLowLevelIOHook::GetDeviceDesc() and AK::StreamMgr::IAkLowLevelIOHook::GetDeviceData() to display some of your low-level I/O system data in the Wwise profiler.
Source (I/O) starvation occurs whenever streaming data was not sent to the Stream Manager within the required deadline. An error notification appears in the capture log of the profiler, along with the name/ID of the starving audio source.
Source starvation may not be caused by bad I/O settings, but for other reasons instead:
1) Interactive music: Streaming objects of the Interactive Music hierarchy are scheduled ahead of time, and require that streaming data be ready on time. This look-ahead time is a property of each music track. If starvation occurs with interactive music, it may be because the look-ahead time you have specified is not large enough.
2) Zero-latency streaming: If you check the "Zero Latency" option in sounds and music tracks properties, the beginning of the streaming file is stored into soundbanks. When you post an event to play them, they start playing immediately, using the data that was stored in the soundbank (prefetched data). But if they run out of prefetched data before the following part of their file has been streamed from disk, source starvation occurs. In such a case, increase the prefetch length.
Sounds of the actor-mixer hierarchy that are not marked as "Zero-Latency" never start playing before their target buffering length is reached. The time it takes to reach the target buffering constitutes the initial latency of the sound. So if starvation occurs with these sounds, it is probably due to improper or unbalanced I/O conditions.
Apart from asking more throughput than your device can provide, the main cause of starvation is to run out of I/O memory. When the I/O memory pool is full, the streaming device stops sending transfer requests to the low-level I/O. Check the I/O memory usage in the Streaming Devices tab of the profiler.
If source starvation is generalized in some situations, and is not specific to some streams, it may be because you are asking too much audio data than what your I/O device can provide. Don't forget that you need to share its bandwidth with other assets of the game. The preferred way of limiting the burden of audio on I/O is to limit the number of streams using the data-driven features of Wwise, like instance limiting and virtual voices.
Starvation may also be caused if your target buffering is too small. In the Streams tab, pay attention to the Buffering Status and the amount of referenced memory (Ref. Memory). If the widget seems to indicate "full" most of the time, but the referenced memory often drops to 0, it probably means that your target buffering is too small: the I/O thread is mostly idle, but buffering is insufficient to make up for the time it takes to read a buffer from disk once the I/O scheduler decided to post an I/O request.
On the other hand, if the Buffering Status widget seems to indicate "not full" for streams that should be in steady state (that is, that have already started playing), it means that the low-level I/O device is too slow to service them. Note that it could also be because there is no more available memory in the I/O pool (check the Streaming Devices tab), or because the scheduler is waiting for the concurrent I/O request count to drop below AkDeviceSettings::uMaxConcurrentIO.
Sometimes, source starvation may occur systematically in the presence of certain streams.
The total throughput required for a streaming device to correctly service all audio streams is balanced between them using heuristics based on their compression format, sample rate, number of channels, and so on. However, some variable bitrate codecs (like XMA and Vorbis) sometimes pull more I/O data that they initially declared, during seeking for example, and sometimes also at loop boundaries. If this occurs with seeking, try using a smaller seek table block size in the conversion settings. Note that seeking also occurs in interactive music and with the "From Elapsed Time" virtual voice behavior.
Otherwise, it can also be caused by a file being far away on disk. Use the platform's tools (or the File Packager utility if you use file packages) to optimize file placement on disk by minimizing seeks at run-time.
You may also compensate for these throughput requirement spikes by increasing the target buffering length (but watch out for increased I/O memory usage).
If you stream small looping sounds, it may be useful to enable caching (AkDeviceSettings::bUseStreamCache). This will help save bandwidth and I/O memory.
Also, avoid wasting bandwidth with DVD/HDD devices. If you use a deferred device, watch for cancelled transfers (in the Streaming Devices tab of the profiler). Reducing the maximum number of concurrent transfers (AkDeviceSettings::uMaxConcurrentIO) should help.
Threads of I/O devices usually use an insignificant amount of CPU, as they spend most of their time waiting for I/O. This CPU usage may be more significant with very fast devices (like RAM devices). In such a case, you can
- Decrease target buffering;
- Decrease thread priority.
If CPU spikes occur only when opening files, it may be because file opening takes a significant amount of time. Some platforms/disk devices are slow to return from fopen(). In such a case, defer file open (refer to Deferred Opening for more details).
Apart from reducing the number of streams playing concurrently, you may reduce the size of I/O memory required by
- Lowering the granularity (beware of negative impacts on device throughput);
- Lowering the target buffering (beware of source starvation);
- Using stream data caching (on the other hand, note that caching is less efficient with smaller pools).
Note that fragmentation never occurs in the I/O memory pool. Also, it is acceptable to run out of memory sporadically in this pool. Often, data that has been already streamed in is sufficient to sustain this spike in memory usage. The worst that can occur is source starvation.
The Stream Manager memory pool is used for small object allocations: device and stream objects, transfer structures, storage for asynchronous file opening, stream memory look-up tables, and so on. The typical size required for this pool is quite small, but ideally, ensure that you never run out of memory in this pool. This would provoke unrecoverable I/O failures. Note that most of it is allocated when devices are created.
The following settings will decrease the memory required from the Stream Manager pool:
- Use a blocking device instead of a deferred device;
- If you use a deferred device, decrease the maximum number of concurrent requests (AkDeviceSettings::uMaxConcurrentIO);
- Disable the stream cache (AkDeviceSettings::bUseStreamCache);
- Execute synchronous file opens (refer to Deferred Opening for more details);
- Limit the number of streams opened at the same time.