Loading wav, MP3 and MIDI from INSIDE a .jar file

Hi!

I’ve been extremely successful in making a 2D RPG using some of Bucky’s Java Game Development tutorials from TheNewBoston, and generally just pushing my own envelope. Now I’ve run into a problem I CANNOT solve. I’ve tried for days now, and when you see what the problem is, you will understand my frustration on the matter.

I’ve tried to implement a simple MIDIPlayer (class-code supplied at the bottom). The thing works great, until I export to .jar, then no sound plays, but the game keeps chugging away. So, I read up on it, and apparently there’s some problem with soundbanks, so I installed Java’s deluxe soundbank, to no avail.

NOW, finally I’ve made it work…but ONLY if the MIDI-files are in the same folder as the .jar file, or a folder in that directory. So I read up on THAT, and it seems you’re not supposed to use the standard filepath-method when loading streams (mainly audio) from a .jar file. ONLY if you’re referencing an actual file in the folder of the .jar, as per example:
getSequence(“SomeMIDIfile.mid”);

or in a dedicated folder next to it, as per example:
getSequence(“music/SomeMIDIfile.mid”);

In fact, you’re supposed to reference it using the URL class, and possibly converting that to a URI, and then use the path you can get from there. So I tried that in MANY different forms, and the weirdest thing happens!

The first couple of tries, I got a nullpointer exception, because I was trying to find the correct path-syntax, and when I finally got it, it gave me an error, stating: "Java cannot find the file at the specified location: " and then proceeds to write the correct path, just to mock me :slight_smile:

SO, I’m not getting a nullpointer, because it does find the file…but it doesn’t find the file…
The file has been imported into my project on the path: src/snm/music/midis/SomeMIDI.mid
These are the methods I’ve tried to use to get a usable path (tried ALL of them with every single combination of directories in the path above), and they all end up giving me that terrible error-message when I hit the right “spot”.

getSequence(getClass().getResource("/snm/music/midis/SomeMIDIfile.mid").getPath());
getSequence(getClass().getClassLoader().getResource("/snm/music/midis/SomeMIDIfile.mid").getPath());

URL url = new URL("/snm/music/midis/SomeMIDIfile.mid");
getSequence(url.getPath());

As I said, I’ve also tried to convert the URL to a URI to circumvent the %20’s which appear in place of spaces when getting a file-path. I’ve ALSO tried to rewrite the MIDIPlayer so getSequence doesn’t work with File(), but with FileInputStream() instead, still to no avail. It is the same thing with wav-files. Haven’t tried MP3s, but logically it should be the same.

I’m at wit’s end…and I have exhausted Google. They called me and told me to stop searching for JAVA +MIDI +ARGH over and over again…something about their revenues.

PLEASE, can anyone cast some light on this subject?


package snm.music.midiplayer;

import java.io.File;
import java.io.IOException;
import javax.sound.midi.*;

public class MidiPlayer implements MetaEventListener {

    // Midi meta event
    public static final int END_OF_TRACK_MESSAGE = 47;

    private Sequencer sequencer;
    private boolean loop;
    private boolean paused;
	
	// load 2 pieces of music
    // This commented out one doesn't give a nullpointer exception, because the path IS right,
    // but it doesn't play anything...The two underneath it, play the MIDI happily when
    // executing the .jar file, but only if the .mid is in the same folder.
    
    //private Sequence worldMusic = getSequence(getClass().getResourceAsStream("/snm/music/midiplayer/MainTheme.mid"));
	private Sequence worldMusic = getSequence("MainTheme.mid");
	private Sequence fightMusic = getSequence("Fighting.mid");

    /**
        Creates a new MidiPlayer object.
    */
    public MidiPlayer() {
        try {
            sequencer = MidiSystem.getSequencer();
            sequencer.open();
            sequencer.addMetaEventListener(this);
        }
        catch ( MidiUnavailableException ex) {
            sequencer = null;
        }
    }


    /**
        Loads a sequence from the file system. Returns null if
        an error occurs.
    */
    public Sequence getSequence(String filename) {
        try {
            return MidiSystem.getSequence(new File(filename));
        }
        catch (InvalidMidiDataException ex) {
            ex.printStackTrace();
            return null;
        }
        catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }


    public void playFight(){
    	play(fightMusic, true);
    }
    
    public void playWorldmap(){
    	play(worldMusic, true);
    }

    /**
        Plays a sequence, optionally looping. This method returns
        immediately. The sequence is not played if it is invalid.
    */
    public void play(Sequence sequence, boolean loop) {
        if (sequencer != null && sequence != null) {
            try {
                sequencer.setSequence(sequence);
                sequencer.start();
                this.loop = loop;
            }
            catch (InvalidMidiDataException ex) {
                ex.printStackTrace();
            }
        }
    }


    /**
        This method is called by the sound system when a meta
        event occurs. In this case, when the end-of-track meta
        event is received, the sequence is restarted if
        looping is on.
    */
    public void meta(MetaMessage event) {
        if (event.getType() == END_OF_TRACK_MESSAGE) {
            if (sequencer != null && sequencer.isOpen() && loop) {
                sequencer.start();
            }
        }
    }


    /**
        Stops the sequencer and resets its position to 0.
    */
    public void stop() {
         if (sequencer != null && sequencer.isOpen()) {
             sequencer.stop();
             sequencer.setMicrosecondPosition(0);
         }
    }


    /**
        Closes the sequencer.
    */
    public void close() {
         if (sequencer != null && sequencer.isOpen()) {
             sequencer.close();
         }
    }


    /**
        Gets the sequencer.
    */
    public Sequencer getSequencer() {
        return sequencer;
    }


    /**
        Sets the paused state. Music may not immediately pause.
    */
    public void setPaused(boolean paused) {
        if (this.paused != paused && sequencer != null) {
            this.paused = paused;
            if (paused) {
                sequencer.stop();
            }
            else {
                sequencer.start();
            }
        }
    }


    /**
        Returns the paused state.
    */
    public boolean isPaused() {
        return paused;
    }
}

Are your .mid files making it into your jar? If so, getClass().getResource("/snm/music/midis/SomeMIDIfile.mid") should do what you want. Don’t bother constructing URL by hand, since it’s not aware of the scheme you need to access it, whereas getResource() is.

If you see it in the jar and you know you have the right resource path, I think your best bet is to step through it in a debugger. Could be the library you’re using is doing something stupid through no fault of your own.

Thanks for your reply! I checked, and the .mid’s are in the right place in the .jar, and tried almost exactly what you wrote.

Thing is, the MidiSystem.getSequence() takes a String, so I have to add .getPath() to get the path out of the URL returned by getClass().getResource(). Maybe that’s what’s destroying the “real” path? I checked what getClass().getResource() returns.

This is it:
file:/D:/Egne%20Projekter/SwordsAndMagic%20v9%20test4.jar!/snm/music/midis/SomeMIDI.mid

Looks right…but it’s still not working. I’d think it was because of a missing soundbank in the .jar, but as I mentioned, when I reference a MIDI outside the .jar, it plays perfectly.

java.io.File’s don’t work for resources in jars. By using new File(…) you’re always looking for a physical file on disk, not a resource in the jar. Change your method to this:


    public Sequence getSequence(String resource) {
        try {
            return MidiSystem.getSequence(getClass().getResource(resource));
        }
        catch (InvalidMidiDataException ex) {
            ex.printStackTrace();
            return null;
        }
        catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

Then passing in the path to your resource in the jar should work fine.

[quote=“Ultroman”]Thing is, the MidiSystem.getSequence() takes a String
[/quote]
Are we talking about the same MidiSystem class? I quote the java api:

I couldn’t find any method getSequence(String s). Anyway, I’ve recently had to deal with a similar problem when creating a .jar. Here’s how I handled it:

  1. Suppose you have the .java files in a package named org.myApp.main and the files (image & audio) in org.myApp.main.resources (notice it’s the same prefix for both, “org.myApp.main”).

  2. Since you can use an InputStream with getSequence(InputStream stream), I’d try this:


// No '/' before "resources/mySound.midi"
Secuence mySound = getSequence(getClass().getResourceAsStream("resources/mySound.midi");

Good luck!

Are we talking about the same MidiSystem class? I quote the java api:

[/quote]
Hi. Thanks for your reply! Sorry, that was a typo, or a brainfart or whatever. I wanted to say, that the implemented MidiPlayer-class wants a String as a location-reference. The MidiSystem.getResource used in it, wants a File. My problem, I think, is referencing to the file-path correctly, for MidiSystem to understand it. Am I right when I say that one cannot instantiate a File() using a normal String-reference when trying to access something in a .jar?

The use of new File() in ‘my’ MidiPlayer class should probably be exchanged for an InputStream. I tried that, but to no avail. I will try again, though. It was pretty late the last time ^^

I will keep you posted…

Just to be sure I’m not reading an empty file, I’d remove the comment and change a little your getSequence(String filename) method:


    public Sequence getSequence(String filename) {
    
    //I added this!
    File midiFile = new File(filename);
    System.out.println(midiFile.lenght());

        try {
            return MidiSystem.getSequence(midiFile);
        }
        catch (InvalidMidiDataException ex) {
            ex.printStackTrace();
            return null;
        }
        catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

There’s the problem. Your resource is in a jar, but you’re treating the string as a file unconditionally. You need to use a URL that you get back from getResource(), not File.

Thanks again!

Well, now I started my hunt for glory, so I got rid of the getSequence, and started doing my referencing in the constructor, just to make sure nothing was getting in the way.

I tried this, and now it happily plays the MIDI when I run it in Eclipse, but gives a nullpointer when I try opening a built .jar with it
(well, the game crashes with black screen).

In MidiPlayer.java I load up:
fightMusic = MidiSystem.getSequence(getClass().getResourceAsStream("…/midis/Fighting.mid"));

Just to be absolutely clear on the circumstances:
I have a main package called ‘snm’. Inside that, is ‘music’, and inside that, are two packages: ‘midis’ and ‘midiPlayer’.

So it looks like this:
snm.music.midis <=== holds the midi-files
snm.music.midiPlayer <==== holds the MidiPlayer.java
snm.game <===== holds the main game.java with the gameloop in it

Use a forward slash at the beginning of the String to specify the root of the jar, so as many others have repeatedly posted:


MidiSystem.getSequence(getClass().getResource("/snm/music/midis/Fighting.mid");

THANK YOU! This was exactly what I needed. I wonder why nothing like this came up during my searches. I am sort of a noob at Java, but I feel like I’m getting a good grip on things. None of my teachers could tell me this, and I’m in 3rd semester of computer science! That’s so embarrassing.

Thank you SO much, H3rnst and ra4king. This solved everything! It runs in both the IDE and from the .jar now.
I just read your great replies in the List-thread. Very interesting thoughts!

@sproingie:
I really wanted to go around that whole File-thing, and just put the stream right into that baby, and now it does. You we’re all right :smiley:

I’m glad you could fix it! ;D

Since I’m a newbie as well, I’ll ask just out of curiosity:

If he’s calling this method from inside the snm.music.midiPlayer package. Shouldn’t the following code have to work anyway?


MidiSystem.getSequence(getClass().getResource("midis/Fighting.mid");

Well, noob to noob, I think you’d need:


MidiSystem.getSequence(getClass().getResource("../midis/Fighting.mid");

In order to tell the thing to go back from midiplayer-package to music, and then proceed into midis

Without a forward slash at the beginning of the path, it becomes relative to the Class returned by getClass().

You should not use getClass() for resource loading as this will break when the class is sub classed. Use MidiPlayer.class.getResource() to have a fixed starting point.
Also make sure that the resources are in the same JAR as the class you use as a base reference otherwise you could get issues depending on how the class loaders are setup.

It will only “break” for relative paths and subclasses in a different package, in that it will be relative to the subclass in the new subclass, but you could also call that proper behavior. You should always be using absolute resource paths anyway unless you specifically want some resource co-located with the class.

Separate jars are not a problem – you only get separate classloaders if you’re writing web apps or OSGi or if you grabbed the boot classloader for some crazy reason (like calling getClassLoader on any java.lang.* class)