[Solved] How can I play a .MP3 file from file outside of the content folder?

I have a game which downloads .MP3 songs from my server. It saves them locally and I need to play those songs using a Song.

I have the song downloaded and call Load<Microsoft.Xna.Framework.Media.Song>(songFileName), but I get an exception “Could not load asset as a non-content file!”

I can’t embed this in my project since it is downloaded from a server - is this possible in MonoGame?

You cannot use the ContentManager to load a song you have downloaded and saved to a local directory. You would use Song.FromUri() to load the song.

Thanks so much for the help, but I can’t get it to load - either I get a file not found exception if I do a UriKind.Relative, or that absolute paths aren’t supported. System.IO.File.Exists works fie though - any thoughts?

What platform are you developing for?

if your developing for android there is a bug.

here is more information about it and a workaround.

Actually I did stumble across this last night, and I think my problem may be that I’m saving the file to a location using an absolute path, but I need to convert that path to a relative path to pass it to the FromUri. I plan on investigating this tonight, but if anyone has a code snippet on how to make a path relative to the executing Android app, I won’t complain :smile:

the above code is to a relative path. Just replace content with the directory you saved your mp3 to.your save location.

Song.FromUri("mp3location", new Uri("mp3location", UriKind.Relative)

just replace mp3location with your filepath

Here’s the code I tried:

// Song is downloaded and saved to the songFile location:
string songFile = System.Environment.GetFolderPath (System.Environment.SpecialFolder.Personal) + "/file.mp3";

// and later after the song is downloaded, we check for it:
if(System.IO.File.Exists(songFile))
{
	var uri = new Uri( songFile, UriKind.Relative);
	var song = Song.FromUri(songFile, uri);
	MediaPlayer.Play(song);
}

You can see that the file must exist for the code to execute, but calling Play on the song will throw an exception as follows:

  --- End of managed exception stack trace ---
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Caused by: java.lang.reflect.InvocationTargetException
	at java.lang.reflect.Method.invoke(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:372)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
	... 1 more
Caused by: java.io.FileNotFoundException: /data/data/FlatRedBallAndroidTemplate.FlatRedBallAndroidTemplate/files/file.mp3
	at android.content.res.AssetManager.openAssetFd(Native Method)
	at android.content.res.AssetManager.openFd(AssetManager.java:329)
	at mono.java.lang.RunnableImplementor.n_run(Native Method)
	at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:29)
	at android.os.Handler.handleCallback(Handler.java:739)
	at android.os.Handler.dispatchMessage(Handler.java:95)
	at android.os.Looper.loop(Looper.java:135)
	at android.app.ActivityThread.main(ActivityThread.java:5221)
	... 4 more

So you can see that simply using the file and making a URI out of it does not work, despite the file being present. I’m guessing the string being passed in to the URI needs to be made relative, or perhaps I’m saving the file in the wrong place. As shown by the exception, the file location is:

/data/data/FlatRedBallAndroidTemplate.FlatRedBallAndroidTemplate/files/file.mp3

Any thoughts on how this should work?

There are some things we need to fix, especially on Android. In the current code there is an Android-specific AssetUri property on the Song class. If this is not set, it will default to looking in the AssetManager for the song file at the given path. The AssetManager is where content deployed with the app is found. The location you saved your MP3 file is not accessible from the AssetManager.

What you will need to do on Android is to set the AssetUri property on the Song instance to an Android.Net.Uri instance that describes where the song is located. I don’t have an example handy right now, but this may work for you.

This isn’t a good solution though, and I agree that loading songs is not ideal at the moment. I will add this to my list of things to fix.

@KonajuGames Thanks again. I just barely discovered the problem by navigating the source as well and that’s also what I found. I don’t know if you’re suggesting that I set it in source, but the property is get-only and the field is private, so I can’t set it from outside of MonoGame.

For anyone else who encounters this, I found I can set the assetUri through reflection. It’s definitely hackish but I’m relying on the NuGet packages. Here it is:

var uri = new Uri( songFile, UriKind.Relative);
var song = Song.FromUri(songFile, uri);
var songType = song.GetType();
					
var fields = songType.GetField("assetUri",
	System.Reflection.BindingFlags.NonPublic | 
	System.Reflection.BindingFlags.Instance);

Android.Net.Uri androidUri = Android.Net.Uri.Parse(songFile);
fields.SetValue(song, androidUri);
MediaPlayer.Play(song);