Harmonyfx – JavaFx Media Player
Ditulis oleh Muhammad Hakim di/pada 4 Juli 2009 11:12 am
Introduction
Before you start read the rest of the article, watch the demo below
and the longer one (with drag & drop + search demo: 4:19 minutes)
This article has been in the making a while, and has sort of become a labour of love. When I first started the sole purpose of this article was to gain a deeper understanding of some of JavaFX features APIs, (CustomNode, Storage & Resource, RESTful Web Service using HttpRequest & pullparser, Binding with java object, drag and drop, effects, animation, “3D” etc) I needed an arena in which to perform this self study, so what I decided to do was construct a workable/searchable MP3 player. The MP3 player would work of MP3 ID3 tag metadata which would be obtained using myID3 a java ID3 tag library.
In essence this is what this article is all about.
Overview
Essentially HarmonyFx (the codename ;)) looks like this. It is made up of a number of different consituent CustomNode (Custom Control),
How It Is Intended to Work
I have tried to write this application in a logical manner, and I hope it works as most people would expect it to work. So without further ado, let me delve in to how I intended Harmonyfx to work.
When Harmonyfx is run it will examine the Storage (Library) and tray read all files, that previously saved, Read the metadata (ID3) tag, and arrange it in shelf (cover flow), and download the Album art based on the Album name from last.fm web service. User can add songs in the library by drag and drop folder or files into harmonyfx.
If Harmonyfx is shutdowned, the library updated again with last changed library in application.
Library – Resource & Storage
Here this the class for save and load library from storage:
public class Library {
var storage:Storage;
var resource:Resource;
postinit{
var storeExist = checkStore();
if (storeExist){
// load songs
PlaylistView.filelist = loadSongs();
}
FX.addShutdownAction(function():Void{saveIntoLibray (PlaylistView.filelist)});
}
public function loadSongs ():String[]{
var songs:String[];
var inp: InputStream = resource.openInputStream();
try {
var reader = new BufferedReader(
new InputStreamReader(inp));
var song:String;
try{
while ((song = reader.readLine()) != null){
insert song into songs;
}
}catch(ex:IOException){
println("error load library: {ex.getMessage()}");
}
} finally {
inp.close();
}
return songs;
}
public function saveIntoLibray (songs:String[]) {
var store_exists = checkStore();
if (store_exists == true)
loadSongs();
var outp: java.io.OutputStream = resource.openOutputStream(true); // override old
try{
//Store data including newline character
var newline:String = "\n";
for (song in songs){
outp.write(song.getBytes());
outp.write(newline.getBytes());
}
}catch(e:IOException){
println("save lib exception : {e.getMessage()}");
}finally{
outp.close();
}
}
public function checkStore() : Boolean {
var ret: Boolean = false;
try {
storage = Storage {
source: "harmonyfxlibrary"
};
resource = storage.resource;
ret = false;
} catch (e : IOException ) {
resource = storage.resource;
ret = true;
}
ret = true;
}
}
Drag & Drop & Binding Java Object from JavaFX
Harmonyfx actually allows more music to be added into it’s library. This is done via drag and drop, where the user is able to drag a entire directory or just single file(s). This is done by dragging items to “client area” of Harmonyfx.
since javafx 1.2 hasn’t include API to handle this task, i decide to wrap swing JPanel and use java API to handle this drag & drop task.
class Background extends SwingComponent{
public var background: JPanel;
public override function createJComponent(){
background = new JPanel();
background.setBackground(java.awt.Color.BLACK);
background.setPreferredSize(new Dimension(width, height));
background.setDropTarget(new DropTarget(background,DnDConstants.ACTION_COPY_OR_MOVE,
dndlistener, true));
background.setRequestFocusEnabled(true);
return background;
}
}
and the drag & drop listener implementation is like this :
package harmonyfx.dndListener;
// file: DnDListener.java
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.File;
import java.util.Observable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.List;
import java.util.Vector;
public class DnDListener extends Observable implements DropTargetListener {
protected boolean acceptableType;
public String[] files;
public DnDListener() {
}
@Override
public void dragEnter(DropTargetDragEvent dtde) {
checkTransferType(dtde);
acceptOrRejectDrag(dtde);
}
@Override
public void dragExit(DropTargetEvent dte) {
}
@Override
public void dragOver(DropTargetDragEvent dtde) {
}
@Override
public void dropActionChanged(DropTargetDragEvent dtde) {
acceptOrRejectDrag(dtde);
}
@Override
public void drop(DropTargetDropEvent dtde) {
if ((dtde.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) != 0) {
dtde.acceptDrop(dtde.getDropAction());
Transferable transferable = dtde.getTransferable();
try {
boolean result = dropFile(transferable);
dtde.dropComplete(result);
} catch (Exception e) {
dtde.dropComplete(false);
System.out.print("exception: "+e.getMessage());
}
}else {
dtde.rejectDrop();
}
}
protected boolean acceptOrRejectDrag(DropTargetDragEvent dtde){
int dropAction = dtde.getDropAction();
int sourceActions = dtde.getSourceActions();
boolean acceptedDrag = false;
if (!acceptableType || (sourceActions & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
dtde.rejectDrag();
} else if ((dropAction & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
dtde.acceptDrag(DnDConstants.ACTION_COPY);
acceptedDrag = true;
} else {
dtde.acceptDrag(dropAction);
acceptedDrag = true;
}
return acceptedDrag;
}
protected void checkTransferType(DropTargetDragEvent dtde) {
acceptableType = dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
}
protected boolean dropFile(Transferable transferable) throws IOException,
UnsupportedFlavorException, MalformedURLException {
List fileList = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor);
int i = 0;
List<String> list = new Vector<String>();
String[] fls = new String[fileList.size()];
for (Object f : fileList){
File file = (File) f;
exploreDir(file, list);
}
if (list != null)
setFiles(list);
return true;
}
void exploreDir(File file, List<String> list){
if (file.isFile()){
if (file.getName().toLowerCase().endsWith(".mp3")){
try{
list.add(file.toURL().toString());
}catch(Exception ex){
System.out.println("Error conversion: "+ex.getMessage());
}
}
}else if (file.isDirectory()){
File fs[] = file.listFiles();
for (File f : fs)
exploreDir(f, list);
}
}
public void setFiles(List<String> f){
files = new String[f.size()];
int i=0;
for (String s: f)
files[i++] = s;
startNotification();
}
private void startNotification(){
setChanged();
notifyObservers();
}
public String[] getFiles(){
return files;
}
}
and because i must bind the files that dropped, i implements (inherit) this class with Observable class, so whenever files/folder dragged, this class will notify the Adapter:
// file: DnDListenerAdapter.fx
import java.util.Observer;
import java.util.Observable;
public class DnDListenerAdapter extends Observer{
public var files:String[];
public-init var dndListener:DnDListener on replace{
dndListener.addObserver(this);
}
public override function update(observable:Observable, arg:Object){
FX.deferAction(
function():Void{
files = dndListener.getFiles();
});
}
}
there are a couple example that help me to implement this one, one from Eric and other from Michael Heinrichs (about binding java object in JavaFx)
Custom ListView
Current Default ListView control can’t display itemview except in string format, so I make custom listview to get better UX when displaying list of song for an album. My ListView Implementation is developed based on Tweeter application example at javafx/samples
Flip & Cover Flow animation with PrespectiveTransform
nothing special with this, i just rewrite the samples codes from javafx samples ;) check this 2 links samples out
1. Displayshelf
2. PageFlip
MP3s / ID3
Harmonyfx relies heavily on ID3 tag metadata that may or may not be available within scanned files. so I had a hunt about and found an excellent free ID3 library for java which is called MyID3 which reads both ID3v1 and ID3v2 tags. It is very easy to use and is obviously included in HarmonyFx.
Here is what it looks like to read a ID3 tag for a given file, where i read mp3 ID3 tag whenever mp3 file locataion is updated using javafx trigger (on replace).
public class Song{
public var album:String;
public var title:String;
public var artist:String;
public var albumArt:Image;
public var url:String on replace{
var id3 = ID3Reader{};
id3.fileName = url.replaceAll("file:/", "");
def albm = id3.getAlbum();
album = if (albm != null ) albm else "";
def ttle = id3.getSongTitle();
title = if (ttle != "") ttle else{
url.substring(url.lastIndexOf("/")+1).replaceAll(".mp3","");
}
artist = id3.getArtist();
};
// another code goes here
Web Service
I Use Last.fm web service to obtain album art for specific album. here this how I use HttpRequest and Pullparser to obtain the album art:
public function getImage (artist:String,album:String) {
def artst = artist.replaceAll(" ", "%20");
def albm = album.replaceAll(" ", "%20");
def url= "http://ws.audioscrobbler.com/2.0/?method=album.search&album={albm}&api_key=1d819e1201e75b96724a818c85e7f730";
var p:PullParser;
var h:HttpRequest;
h = HttpRequest{
location: url
onException: function(exception: Exception) {
print("exception: {exception.getMessage()}");
}
onInput: function(input) {
if (album != ""){
try{
println("album");
var attval : String;
p = PullParser {
documentType: PullParser.XML
input: input
onEvent: function(event) {
if (event.type == PullParser.START_ELEMENT){
if (event.qname.name == "image"){
var qAttr : QName = QName {name : "size"};
attval = event.getAttributeValue(qAttr);
}
}else
if (event.type == PullParser.END_ELEMENT){
if (event.qname.name == "image"){
if (attval == "small"){
//println("small: {event.text}");
}else if (attval == "medium"){
//println("medium: {event.text}");
}else if (attval == "large"){
println("large: {event.text}");
if (event.text != "" and artImage.url == "")
artImage = Image{
url: event.text;
width: 200
height: 200
preserveRatio: true
// placeholder: Image {
// url: "{__DIR__}AlbumArt.png"
// }
}
}else if (attval == "extralarge" and artImage.url == ""){
println("extralarge: {event.text}");
if (event.text != "")
artImage = Image{
url: event.text;
// placeholder: Image {
// url: "{__DIR__}AlbumArt.png"
// }
}
}
}
}
}
};
p.parse();
p.input.close();
}catch(exc:Exception){
println("msg: {exc.getMessage()}");
}finally{
p.input.close();
}
}
}
onDone: function() {
println("DONE!\nartImageurl: {artImage.url}");
}
};
h.start();
}
}
What Do You Think
Anyway that’s it, want to try it by yourself, Download Harmonyfx netbeans project from here: Harmonyfx.zip.
btw, comments are welcome :)
References
1. http://javafx.com/samples/
2. http://blogs.sun.com/michaelheinrichs/entry/binding_java_objects_in_javafx
3. http://www.fightingquaker.com/myid3/
4. http://java.sun.com/javafx/1.2/docs/api/
5. javafx drag and drop






Muhammad Hakim berkata
maaf artikelnya belum selesai, tapi source codenya sudah bisa di download.
lagu2 itu sebenarnya saya sendiri juga kurang suka kecuali beberapa nasyidnya; tapi karena kebutuhan untuk demo webservice, ngambil album art dari last.fm, jadinya kugunakan beberapa lagu barat yang kemungkinan pasti ada album art-nya di sana.
pemula berkata
saya menggunakan netbeans 6.5.1 dan javafx 1.1 SDK
ketika saya double klik FILE.jar pada dist folder muncul tulisan Main class not found.
yang ingin saya tanyakan bagaimana caranya merunning java application tanpa menggunakan netbeans atau menggunakan PC lain yang tidak terinstal netbeans.
Kalau bisa saya minta tutorialnya ya mas..makasih
Muhammad Hakim berkata
sampyan bisa menggunakan command prompt atau membuat file batch
cmd>javafx -jar Harmonyfx.jar