Firelight Technologies FMOD Studio API
Asynchronous I/O and deferred file reading
Introduction
This tutorial will describe how to defer file reading in FMOD so that you don't have to immediately satisfy FMOD's requests for data.This sort of behavior is highly desirable in game streaming engines that do not have access to the data yet, or for when accessing data out of order or in a non sequential fashion would greatly degrade performance.
FMOD's asynchronous I/O callbacks will allow you to receive an FMOD read request and defer it to a later time when the game is ready. FMOD will use priorities to notify the game engine how urgent the read request is, as sometimes deferring a music stream read for example could result in stuttering audio.
Setup : Override FMOD's file system with callbacks
The idea is that you are wanting to override the file I/O that FMOD normally performs internally. You may have done this before with the System::setFileSystem by overriding the following callbacks:FMOD_FILE_OPENCALLBACK useropen FMOD_FILE_CLOSECALLBACK userclose FMOD_FILE_READCALLBACK userread FMOD_FILE_SEEKCALLBACK userseekThe normal behavior here is that you would need to satisfy FMOD's read and seek requests immediately in a blocking fashion.
In the open callback, you open your internal file handle and return it to FMOD, along with the file size.
You would have to set all callbacks or file system override would not work. Any callback that is null in the above callback list will cause FMOD to use the default internal system and ignore your callbacks. All callbacks must be set.
With async I/O, there are 2 new callbacks which you can use to replace the 'userread' and 'userseek' callbacks:
FMOD_FILE_ASYNCREADCALLBACK userasyncread FMOD_FILE_ASYNCCANCELCALLBACK userasynccancelIf these callbacks are set, the 'userread' and 'userseek' callbacks are made redundant. You can of course keep 'userread' and 'userseek' defined if you want to switch between the 2 systems for some reason, but when 'userasyncread' is defined, the normal read/seek callbacks will never be called.
Defining the basics - opening and closing the file handle.
Before we start, we'll just define the open and close callback. A very simple implementation using stdio is provided below:FMOD_RESULT F_CALLBACK myopen(const char *name, unsigned int *filesize, void **handle, void **userdata) { if (name) { FILE *fp; fp = fopen(name, "rb"); if (!fp) { return FMOD_ERR_FILE_NOTFOUND; } fseek(fp, 0, SEEK_END); *filesize = ftell(fp); fseek(fp, 0, SEEK_SET); *userdata = (void *)0x12345678; *handle = fp; } return FMOD_OK; } FMOD_RESULT F_CALLBACK myclose(void *handle, void *userdata) { if (!handle) { return FMOD_ERR_INVALID_PARAM; } fclose((FILE *)handle); return FMOD_OK; }
Defining 'userasyncread'
The idea for asynchronous reading, is that FMOD will request data (note, possibly from any thread - so be wary of thread safety in your code!), but you don't have to give the data to FMOD immediately. You can return from the callback without giving FMOD any data. This is deferred I/O.For example, here is a definition of an async read callback:
FMOD_RESULT F_CALLBACK myasyncread(FMOD_ASYNCREADINFO *info, void *userdata) { return PutReadRequestOntoQueue(info); }Note that we didnt actually do any read here. You can return immediately and FMOD will internally wait until the read request is satisfied. Note that if FMOD decides to wait from the main thread (which it will do often), then you cannot satisfy the queue from the main thread, you will get a deadlock. Just put the request onto a queue. We'll discuss how to let FMOD know that the data is ready in the next section.
There are a few things to consider here:
Defining 'userasynccancel'
If you have queued up a lot of read requests, and have not satisfied them yet, then it is possible that the user may want to release a sound before the request has been fulfilled (ie Sound::release is called).In that case FMOD will call the async cancel callback to let you cancel any operations you may have pending, that are related to this file.
FMOD_RESULT F_CALLBACK myasynccancel(void *handle, void *userdata) { return SearchQueueForFileHandleAndRemove(info); }
Note that the above callback implementation will search through our internal linked list (in a thread safe fashion), removing any requests from the queue so that they don't get processed after the Sound is released. If it is in the middle of reading, then the callback will wait until the read is finished and then return.
Do not return while a read is happening, or before a read happens, as the memory for the read destination will be freed and the deferred read will read into an invalid pointer.
Filling out the FMOD_ASYNCREADINFO structure when performing a deferred read
The FMOD_ASYNCREADINFO is the structure you will pass to your deferred I/O system, and will be the structure that you read and fill out when fulfilling the requests.The structure exposes the features of the async read system. These are:
typedef struct { void * handle; unsigned int offset; unsigned int sizebytes; int priority; void * buffer; unsigned int bytesread; FMOD_RESULT result; void * userdata; } FMOD_ASYNCREADINFO;
The first 4 members (handle, offset, sizebytes, priority) are read only values, which tell you about the file handle in question, where in the file it wants to read from (so no seek callbacks required!) and how many bytes it wants. The priority value tells you how important the read is as discussed previously.
The next 3 members (buffer, bytesread and result) are values you will fill in, and to let FMOD know that you have read the data.
Read your file data into buffer. sizebytes is how much you should be reading. bytesread is how much you actually read (this could be less than sizebytes).
If you hit the 'end of file' condition and need to return less bytes than were requested - set bytesread to less than sizebytes, and then set the result to FMOD_ERR_FILE_EOF.
Set the result last!
Note! Do not set the result before setting the bytesread value and reading the data into buffer.The initial value for result, is going to be FMOD_ERR_NOTREADY. When you set the value to FMOD_OK (or appropriate error code) then internally FMOD will immediately see this as an indication to continue, so if the bytesread or buffer contents are not ready, you will get corruption, errors or unexpected behaviour.
So to summarize, the last thing you will do before finishing your queue process is to set result. You will not set it before setting bytesread or filling in buffer.
Threading issues & read priorities
As mentioned earlier in this tutorial, FMOD can call the read callback from various different threads, so it is common sense to protect your I/O system from operations happening simultaneously from different threads.A system that would use FMOD's async I/O feature would most likely be running in its own thread. This is so the blocking wait loops in FMOD's loading calls are not forever waiting for data because the user can't provide it to FMOD.
If the system runs in another thread, it can detect the queue insert, and process the data while FMOD is waiting.
It is actually possible to complete the read as if it wasn't deferred, and do a direct file read into the buffer and set sizebytes/result values from the FMOD async read callback. This is a possible way to reduce delays for extremely urgent FMOD reads.
Currently there are 3 different categories of read priority.