You can change the size with the Font.deriveFont(float size) method. I wanted the display to look like the vacuum fluorescent displays used on high end electronics equipment, so I chose a bright bluish green on black and used two really cool freeware fonts: Delusion and Digital Readout.
Each digit of the playing time is an individual JLabel, arranged in an absolute (non-flexible) orientation with the text centered and spaced apart farther than the widest character, which, aside from using a monospaced font, is the only way to keep things from moving around as the numbers change. The file name, status, and length are also JLabels, and can be set externally by public methods.
There's some internal logic for converting raw file names like "Jane_Child - Hey_Mr_Jones.mid" to strings suitable for display. This is surprisingly complex:
What it's doing here is breaking the string up into an
array of chars, then counting backwards until it finds a '.' character,
signifying the the file extension has been found. This works regardless
of the length of the string or the file extension. But why count
backwards? If it counted forward, any '.' characters in the filename
would cause everything after it to be chopped off. For example, a track
with the filename "Run D.M.C. - It's Like That.mid" would be shortened
to "Run D". Once it reaches the last dot, it breaks the loop,
storing the position of the dot as separator.
The next loop goes through the charcters and converts
them to uppercase, and uses their numeric value to sort out any
non-alphanumeric characters. Numbers have a numeric code between 0 and
9, while letters have a code between 10 and 35. A space is used to
replace any punctuation. Each character is appended to the string
builder until the index of the file extension comes up or it goes over
the 36 charcter limit.
The buttons are the same as you'd find on any CD player: Play/Pause, Stop, Forward and Back. The default way to make a button is to use JButton.setText(String text) but I wanted something cooler than text labels so I made my own icons. You can load them into the JButtons with only a few lines of code:
All of the GUI elements are encapsulated in the Window class. When you're working with a graphical program it's good not to have the graphical objects themselves doing too much work. Instead, all of the work of keeping the playlist organized and determining what tracks should be played fall to a class that I made called Control. All of the action listeners in Window are programmed to call methods in Control after updating whatever graphical components need updating. To keep Window from having too much access to Control, it accesses it through a custom interface, ControlInterface, which has one abstract method for each button in Window.
Control is essentially the central class of this program. It's instantiated by Main, which then exits. Control builds the MIDI Player, the window components and any other objects that are needed in its constructor. It also acts as a WindowListener for Window and as a MetaEventListener for the Sequencer in MIDIPlayer, but we'll get to that later.
The most complicated logic in Control is to determine
which file should be accessed from the playlist. There are actually two
playlists: one is the JList graphical representation of it in Window
and the other is a Playlist object, which I subclassed from ArrayList.
The Playlist object is setup as a list of File
objects, which describe absolute paths to a given file. JList are
modified by adding and removing things from a DefaultListModel,
a type of List.
It's capable of holding File objects, but would display them with
File's toString() method, which prints the entire file path from the
root directory. Instead I setup a separate Playlist object for keeping
the File objects. When stuff is added to the Playlist it's also added
to the JList. Keeping them synchronized across all of the method calls
that may modify them can be challenging. Here's the mothod in Control
that's called when a user clicks on Open File:
In this example, the variable main is the Window, picker is a JFileChooser,
and the method calls are custom methods that do pretty much what their
names say they do. The method addToPlaylist(File
file) is called from the various add/open file methods. It gets
the list model from the JList in main, adds the filename only (not the
entire path) to that, then adds the File object itself to the Playlist.
To keep from adding duplicates, it looks up the incoming File object to
see if it's already in the Playlist, if it's not Playlist.indexOf(File
file) will return -1 which means the file is not a duplicate.
The code for moving along the playlist can also be somewhat complex. It has to determine if we're doing random or looping, look at where the current location is, then decide where to go from there.
Playlist keeps track of the index of the last file that was accessed, which is the number given by Playlist.lastIndex(). There's also an autoNext() method in Control that's called at the end of a track. It does the same thing, but stops at the end of the playlist when random and loop = false, instead of doing nothing. Control implements the MetaEventListener interface, which attaches to a Sequencer and provides meta data. The autoNext() method is called when the MetaMessage equals 47, the end of file message for MIDI files.
The Playlist class is a subclass of ArrayList, it
implements Serializable, which basically means that it can be written
to a disk. The .fox playlists
written by this program are raw Playlist objects written using an ObjectOutputStream.
It saves some effort on processing the File strings into a text file,
and doesn't really take up any extra room, so I figured why not. Here's
the code for saving playlists.
It looks complicated, but basically what it's doing is letting the user select a location and name to save the file, then checking it for the .fox file extension and adding it if it's not there. Buffered input and output streams are used because they don't hang other processes while writing to the disk, which is very important on slower computers, as is closing the streams when you're done with them.
Deleting a file from the playlist can also be hard, because you have to adjust the lastIndex variable in the playlist if the file being deleted has a smaller index than the currently playing file to keep from getting an ArrayIndexOutOfBoundsException, that's what the method Playlist.decLastIndex() does, decreases the last index by 1 every time it's called. You'll also have to determine if the currently playing file is the one being deleted, and whether or not it's the last one in the list. If it's not, you want to go down the list, if it is, you want to go back up, and if it's the only one in the list you just want to clear everything.
Another thing that came up that a lot of people don't consider is keeping the configuration from one run to the next. Things like the value of random and loop, the last directory visited with the file chooser, and the location of the window on the screen are a good thing to preserve for the next time the program runs. When this program exits, it saves a small configuration file which is an instance of Configuration, a class I wrote that encapsulates a few variables. It's created, saved, and read by a ConfigurationManager class, and read into Control when it starts up. Random and Repeat are set by boolean variables in Control, the location on the screen is defined by a Point, and the last directory that was visited is stored as a File. This method is called from the constructor when Control is created:
Saving is the reverse of that. When the program exits cleanly, by either selecting File -> Quit or closing the window, a method is called that saves the value of all of these variables and writes them using an ObjectOutputStream to the program's resources directory
This is, of course the player class that encapsulates the Sequencer and its support components. The cool thing is that Java basically has a lot of functionality built in when it comes to handling MIDI files, so there really isn't a lot of code in this class. In fact, loading a new Sequence into the sequencer is as easy as calling the static method MidiSystem.getSequence(File file). What is there essentially deals with setting up the Sequencer and implementing some extra functionality. Java's Sequencer doesn't have a pause function, so I added pause and resume methods. The pause method calls Sequencer.getMicrosecondPosition(), stops playback, and saves that value in an instance variable, and resume passes that variable to Sequencer.setMicrosecondPosition() and restarts playback.
The time and length figures returned by Sequencer as a long integer representing the microsecond position. I didn't need this kind of resolution, so the MIDIPlayer.getTime() and MIDIPlayer.getLength() methods divide that number by 1,000,000 and convert it to an integer before returning the value.
This method is called by a Timer
in Control that fires every 333 mS, which then updates Display with the
new time values. Using odd numbers for timers where an arbitrary value
is needed is a habit of mine, since in some unlikely circumstances it
breaks up some unforseeable race conditions.
That was a long article wasn't it? Surprisingly this
wasn't that hard of a project. Like many programs, the big parts were
easy, but the tiny details take a while to work out. I spent most of my
time on this project on things the user wouldn't really notice unless
they were missing or broken, a lot more time than I spent on the UI
anyway. I am pleased with it though. I might extend it down the road
with sampled sound playback capabilities. Java has built in support for
handling linear PCM files like Sun Audio, RIFF Wave and AIFF, but lacks
native support for more complex formats like MP3, which means I'll have
to write an MP3 decoder myself from scratch.
That being said, there won't be any future versions of
this program. Unlike some people, I don't believe in bug fixes or
updates. If your program has bugs in it you shouldn't have released it.
So if you download this it's not going to pop up a thing every three
days reminding you that there's an update available. That's my rant for
today, and I think we all know who it's directed at.
If you liked reading about this project, you may want to
go to the download page.