Accessing Music on Android and iOS.
In OMG Dancer! (Play | iOS) We wanted the players to be able to access their own music library to play the game with. Whilst this might be a simple feat if you are focusing on a single platform, but here at Super Cookie Games we like to go cross platform from launch. So this means that we need to learn how to access the music library on iOS and on Android.
The requirement was straight forward, we need to get access to the songs by genre id available on the device and then create a map of song id to genre name, then we want an array of all the songs on device.
First lets tackle Android. To begin with you will need a ContentResolver this is accessible from you Activity via a call to the method
This is actually in ContextWrapper which the Activity inherits from.
Once you have that you can simply query for the information, or content, you actually want. Like so.
Cursor genCur = contentResolver.query(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Audio.Genres.NAME, MediaStore.Audio.Genres._ID}, null, null, null); if (genCur != null && genCur.moveToFirst()) { do { int nameIndex = genCur.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME); int idIndex = genCur.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID); String genreName = genCur.getString(nameIndex); long genreId = genCur.getLong(idIndex); Cursor tempcursor = contentResolver.query(MediaStore.Audio.Genres.Members.getContentUri("external", genreId), new String[]{MediaStore.Audio.Media._ID}, null, null, null); if (tempcursor != null && tempcursor.moveToFirst()) { do { songIdToGenre.put(tempcursor.getLong(tempcursor.getColumnIndex(MediaStore.Audio.Media._ID)), genreName); } while (tempcursor.moveToNext()); tempcursor.close(); } } while (genCur.moveToNext()); genCur.close(); }
In the above code example you can see we are querying for the Genres of music on the device. The result is a cursor that we can iterate over to ge and we use that data to populate a map.
Next we can query for all the songs. It might be worth pointing out it is possible a song might not have a genre!
Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Cursor cur = contentResolver.query(uri, null, MediaStore.Audio.Media.IS_MUSIC + " = 1", null, null); if (cur == null || !cur.moveToFirst()) { return; } int artistColumn = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST); int titleColumn = cur.getColumnIndex(MediaStore.Audio.Media.TITLE); int albumColumn = cur.getColumnIndex(MediaStore.Audio.Media.ALBUM); int durationColumn = cur.getColumnIndex(MediaStore.Audio.Media.DURATION); int idColumn = cur.getColumnIndex(MediaStore.Audio.Media._ID); do { long songId = cur.getLong(idColumn); items.add(new Item( cur.getLong(idColumn), cur.getString(artistColumn), cur.getString(titleColumn), cur.getString(albumColumn), songIdToGenre.get(songId), cur.getLong(durationColumn))); } while (cur.moveToNext()); cur.close();
A little less complex than the genre request, this snippet is fairly straight forward. We query, get a cursor, extract the song information and then add a data object that contains all the information.
When it comes to playing the song, we just request the path of the file using the song id and pass that to our player.
String[] projection = {MediaStore.Audio.Media.DATA}; CursorLoader loader = new CursorLoader(context, getURI(), projection, null, null, null); Cursor cursor = loader.loadInBackground(); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); path = cursor.getString(column_index); cursor.close(); Uri getURI() { return ContentUris.withAppendedId(android.provider.MediaStore.Audio .Media.EXTERNAL_CONTENT_URI, songId); }
the path variable ends up being the internal path we need to play. Like so:
Gdx.audio.newMusic(Gdx.files.internal(path));
One thing to note is that I had to strip off “/storage/emulated/0” from the start of the path.
Awesome so that is Android, but what about iOS?
Well, we want to access the iPod API to get a list of all the songs the device knows of. This is pretty simple compared with Android, using RoboVM, we just make the following requests.
MPMediaQuery mpMediaQuery = MPMediaQuery.createSongsQuery(); for (final MPMediaItem song : mpMediaQuery.getItems()) { if (!song.isCloudItem()) { musicTracks.add( new Item( song.getPersistentID(), song.getArtist(), song.getTitle(), song.getAlbumTitle(), song.getGenre(), song.getAssetURL(), (long) song.getPlaybackDuration() * 1000)); } }
The only caveat here is that an iOS device will report cloud based songs as well, in this instance we filter them out.
Now, with Android playing the music track was straight forward, we got a path to it and played it! With iOS it isn’t as straight forward. You get an asset URL which is great if you plan on playing the music through the native player as you can just throw it at it! However we need to access the music so we can play it ourselves. Get ready for some funky coding gymnastics.
private void export(final Item item) { AVURLAsset avurlAsset = AVURLAsset.create(item.path, null); final AVAssetExportSession exporter = AVAssetExportSession.create(avurlAsset, AVAssetExportPreset.Passthrough); String extension = "m4a"; if (item.path.getPath().contains(".m4a")) exporter.setOutputFileType("com.apple.m4a-audio"); if (item.path.getPath().contains(".mp3")) { exporter.setOutputFileType("com.apple.quicktime-movie"); extension = "mov"; } final String exportURL = getApplicationDocumentsDirectory() + "/omgmusic_" + sessionId + "_" + item.getId() + "." + extension; final File exportFile = new File(exportURL); exportFile.deleteOnExit(); exporter.setOutputURL(new NSURL(exportFile)); exporter.exportAsynchronously(new Runnable() { @Override public void run() { item.filePath = exportFile.getName(); if (item.filePath.endsWith(".mov")) { File mp3File = new File(getApplicationDocumentsDirectory() + "/omgmusic_" + sessionId + "_" + item.getId() + ".mp3"); mp3File.deleteOnExit(); exportFile.renameTo(mp3File); item.filePath = mp3File.getName(); } } }); }
In the above code snippet we are essentially copying the music track to our games document directory. In the process we are examining the file to see what type it is so we can manipulate the output file if needed. Once this is done you have the music file in the local space where you are free to do what you want to do with it! Just remember to delete it once you are done, otherwise your game/app will end up eating a lot of space!