Android: sound and music via SDL_RWops

I was asked a while back to publish some example code from C++ Android development. I’ve decided instead to publish the Android version Black Dog when it’s finished, rather than selling it. There’s better stuff out there available for free, so I might as well open it up.

For the record, I’d originally planned to sell the game on the Android market for a next-to-nothing, but it’s not nearly as impressive a game as it is an act of masochism, so I’d rather post the source to help people out with their own native Android games. Encouraging cross-platform development is in my best interests too, seeing as I use a very marginal system ;)

Before I can release Black Dog though I need to get the music working. This is something that stalled development on Biotop as well: it’s been my nemesis for almost 6 months now! Though admittedly most of that time was spent giving up and working on other projects instead…

SDL_RWops “magic”

I’ve talk about this previously, but not in detail. SDL_RWops is a C structure containing a bunch of function-pointers (seek, read, write and close) to provide an abstraction layer for cross-platform external reading and writing. If you’re used to object-oriented programming you might understand it better as an abstract class, only more 1337.

On Android it will, by default, load resources from the assets folder of application’s APK archive. The Android Java SDK provides simple functions to do this, which can be accessed via the JNI. This is handy as it means everything, code and resources, can be shipped in a single package, and this makes installation easy.

Sound – “Unrecognized file type (not WAVE)”

Thanks to this structure we’re able to easily load sound files, or “chunks” as SDL_mixer calls them, into memory to be played when we see fit. This time around I’m using singleton resource managers to store all the various graphic and sound assets that I may want to render or play.

For the moment sound is working on Android, but bizarrely mixer seems to disagree with the file when loading it from Linux:

"Unrecognized file type (not WAVE)"

The beauty of open-source software is that we can go and have a look what exactly is causing the problem:

[c]RIFFchunk = SDL_ReadLE32(src);
wavelen = SDL_ReadLE32(src);
WAVEmagic = SDL_ReadLE32(src);
if ( (RIFFchunk != RIFF) || (WAVEmagic != WAVE) ){
Mix_SetError(“Unrecognized file type (not WAVE)”);
was_error = 1;
goto done;
}[/c]

What’s that I see? A goto statement? No wonder it’s not working :P So basically some unintelligible binary “magic” at the beginning of the file doesn’t match a constant expression… or, more likely since it’s the same file and the same constant expression on both platforms, SDL_ReadLE32 isn’t performing as it should on Linux.

I guess we need to go deeper…

Music – “java.io.FileNotFoundException”

The main difference between sound and music is that music is generally longer, hence a big file, hence streamed rather than loaded completely. This is a problem if we’re using SDL_RWops to load assets from the APK file. It means streaming data from a ZIP file through the JNI and into SDL so that it can be passed to and played by SDL_mixer. Perhaps unsurprisingly this isn’t all that thread-safe somewhere along the line, so a few seconds of music tend to be followed by a complete crash.

The solution is to extract the file to the device’s SD card and then load it directly from the file-system. In order to do so we need to add a permission to the android manifest file:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Next we load the music from the APK using SDL_RWops and save it to the filesystem using… not SDL_RWops: the trouble with the “magic” shortcut for accessing the assets folder is that it prevents us from accessing the filesystem directly. Needless to say this is a problem for loading the music file back from the SD card too, as Mix_LoadMUS makes use of -you guessed it- SDL_RWops. In other words you can’t load music without SDL_RWops, and streaming via SDL_RWops isn’t thread-safe. Oh dear :(

Music – SDL_RWFromFP

Actually streaming via SDL_RWops is only unsafe if you’re streaming from the APK, so the real problem is that you can’t create RWops structures on Android that don’t point to the APK… or can you?

The trick is to use SDL_RWFromFP to initialise an RWops structure from a FILE* rather than a file name. This allows us to create an RWops that accesses the file-system (the SD card) rather than the APK archive:

[cpp]FILE* file = fopen(“/sdcard/data/music.ogg”, “wb”);

SDL_RWops* sdcard_rw = SDL_RWFromFP(file, SDL_TRUE);[/cpp]

Once we’re able to do this we can juggle around file-pointers and sets of read-write operations (which, to reiterate, is what an RWops is) to first externalise the resource and then stream it from the SD card:

[cpp]int AudioManager::load_music(const char* source_file)
{
// Attempt to open the music file
music_file = SDL_RWFromFile(source_file, “rb”); // read binary
ASSERT(music_file, source_file); // print the name of the file being opened

#ifdef __ANDROID__
// initialise RWops structure from file in SD card
FILE* sdcard = fopen(“/sdcard/data/music.ogg”, “wb”);
ASSERT(sdcard, “Opening file to export music from APK to filesystem”);
SDL_RWops* sdcard_rw = SDL_RWFromFP(sdcard, SDL_TRUE); // autoclose
ASSERT(sdcard_rw, “Creating SDL_RWops structure from file pointer”);

// externalise music data from APK assets folder to the SD card
char buffer[io::MAX_BLOCKS];
int read_amount = SDL_RWread(music_file, buffer, 1, io::MAX_BLOCKS);
while(read_amount > 0)
{
SDL_RWwrite(sdcard_rw, buffer, 1, read_amount);
SDL_RWseek(music_file, SEEK_CUR, read_amount*io::BLOCK_SIZE);
read_amount = SDL_RWread(music_file, buffer, 1, io::MAX_BLOCKS);
}
SDL_RWclose(music_file);
SDL_RWclose(sdcard_rw);

// load the music from the filesystem, not the archive
sdcard = fopen(“/sdcard/data/music.ogg”, “rb”);
ASSERT(sdcard, “Opening file to import music from filesystem”);
music_file = SDL_RWFromFP(sdcard, SDL_TRUE); // autoclose
ASSERT(music_file, “Creating SDL_RWops structure from file pointer”);

#endif //ifdef __ANDROID__

// Attempt to read the file contents as music
ASSERT_MIX(music = Mix_LoadMUS_RW(music_file),
“Extracting music from SDL_RWops structure”);

/// NB – file is NOT closed as music data must be streamed

// Success !
return EXIT_SUCCESS;
}[/cpp]

In case you’re curious about why streaming from a ZIP file is bad news, here’s John from the SDL mailing-list:

Avoid the combination of SDL_Mixer, SDL_RWops, and assets on Android if  possible. First problem: SDL seek is implemented as a close(), followed by a  re-open, then repeatedly reading until it hits the offset. The reason has to do  with the assets stream, it’s a forward-only, read-only decompression stream.  Second problem: SDL_mixer likes to seek() in certain cases as a kind of `probe` to see what the stream does. This wreaks havoc on the assets stream or any other non-trivial stream. Third problem: SDL’s seek will mask bad code by clamping to boundaries. SDL_mixer appears to rely on this clamping, and in some cases will seek to extreme offsets such ((size_t)-1). Fourth problem: SDL_mixer triggers some fun reports from valgrind. Fifth problem: it’s pretty easy to crash SDL on Android by touching any of the streams after the app is restored from background or orientation has changed. If you are targeting platform 10 or above, you can directly access assets via the direct C api.

Unfortunately I don’t have a version 2.3.3 phone, and neither does the majority of Android users…

7 thoughts on “Android: sound and music via SDL_RWops

    1. Wilbefast

       Sorry for the slow reply: an eye infection has been keeping me away from screens these last few days :/ in SDL_android.cpp I’ve got a missing variable mEnv. I noticed you shunted the environnement variable around a little… hmm, I wonder if my version is 100% up to date.

      jni/SDL/src/core/android/SDL_android.cpp: In function ‘void SDL_Android_Init(JNIEnv*, _jclass*)':
      jni/SDL/src/core/android/SDL_android.cpp:98: error: ‘mEnv’ was not declared in this scope
      jni/SDL/src/core/android/SDL_android.cpp: In function ‘void Android_JNI_CloseAudioDevice()':
      jni/SDL/src/core/android/SDL_android.cpp:345: error: ‘isAttached’ was not declared in this scope
      jni/SDL/src/core/android/SDL_android.cpp: In function ‘bool Android_JNI_ExceptionOccurred()':
      jni/SDL/src/core/android/SDL_android.cpp:353: error: ‘mEnv’ was not declared in this scope
      jni/SDL/src/core/android/SDL_android.cpp: In function ‘int Android_JNI_FileOpen(SDL_RWops*)':
      jni/SDL/src/core/android/SDL_android.cpp:406: error: ‘mEnv’ was not declared in this scope
      jni/SDL/src/core/android/SDL_android.cpp:416: error: ‘mEnv’ was not declared in this scope
      jni/SDL/src/core/android/SDL_android.cpp: In function ‘int Android_JNI_FileOpen(SDL_RWops*, const char*, const char*)':
      jni/SDL/src/core/android/SDL_android.cpp:497: error: ‘mEnv’ was not declared in this scope
      jni/SDL/src/core/android/SDL_android.cpp: In function ‘size_t Android_JNI_FileRead(SDL_RWops*, void*, size_t, size_t)':
      jni/SDL/src/core/android/SDL_android.cpp:514: error: ‘mEnv’ was not declared in this scope

      Reply
      1. Gabriel Jacobo

        Yes, I had to remove the JNIEnv static variable because it needs to be retrieved for each thread, so there can not be a global static pointer to it. Probably the patch didn’t apply cleanly if you made modifications to the code.

        Reply
        1. Wilbefast

          Hmm… so far returning to the main branch has caused a few problems. I’ll try again tomorrow: if I’m on the main then the patch should work in theory, but I’m definately not up to date.

          Reply
          1. Wilbefast

            Well done then! Sorry I didn’t managed to confirm this for you myself, I was a bit in the hustle and bustle of leaving. When I get back I’ll see if I can’t get the compilation working with the newer version of SDL (because of dependencies I also need to update image, ttf, sound and mixer I think).

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>