domenica 15 giugno 2014

Creating the AudioPlayer class for playing music in java

Now that our User Interface it's almost done let's focus on making a class that implements all our player functionality, we'll call this class AudioPlayer. Basically, we are gonna crate a wrapper around a BasicPlayer object, if you don't know basicplayer api, check this page.
BasicPlayer layer is the simple player API of jlGui. These classes are designed to be used in any application that needs simple features (play, stop, pause, resume, seek) to play audio file or stream. It's a high-level API over JavaSound API. 
First off, let's start by defining our class properties, so that our goals are clear, we want to have access to most of this kind of information
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 //Extension of BasicPlayer
 private static AudioPlayer instance = null;
 private ArrayList<String> playlist = new ArrayList<String>();
 private int index = 0;
 private boolean paused = true;
 private boolean opened = false;
 private boolean isSeeking = false;
 
 //Current Audio Properties
 private float audioDurationInSeconds = 0;
 private int audioFrameSize = 0;
 private float audioFrameRate = 0;
 //Stream info/status
 private byte[] cpcmdata;
 private long csms = 0; //Current Song microseconds
 private int lastSeekMs = 0; //Every time we seek, basic player returns microseconds are resetted
 //we need a var to mantain the position we seeked to
Now let's create the constructor, the class it's an extension from BasicPlayer class, since I'm wrapping it. I've used a singleton here because it's pattern sweets the AudioPlayer propose, we'll need and control only one AudioPlayer object.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 public class AudioPlayer extends BasicPlayer{  
      //Want to use a singleton  
      private AudioPlayer() {  
           super();  
           //Wanna give the AudioPlayer class a basic behaviour  
           this.addBasicPlayerListener(new BasicPlayerListener() {  
                @Override  
                public void stateUpdated(BasicPlayerEvent event) {  
                     if(event.getCode() == BasicPlayerEvent.EOM)  
                     {  
                          lastSeekMs = 0;  
                          paused = true; //reset player state  
                          opened = false;  
                          log("EOM event catched, player resetted.");  
                     }  
                     if(event.getCode() == BasicPlayerEvent.SEEKING)  
                          isSeeking = true;  
                     if(event.getCode() == BasicPlayerEvent.SEEKED)  
                          isSeeking = false;  
                }  
                @Override  
                public void setController(BasicController arg0) {  
                     //No need to use this  
                }  
                @Override  
                public void progress(int bytesread, long microseconds, byte[] pcmdata, Map properties) {  
                     csms = microseconds;  
                     cpcmdata = pcmdata;  
                }  
                @Override  
                public void opened(Object stream, Map properties) {  
                     log("Opened event caught");  
                     Object[] e = properties.entrySet().toArray();  
                     Object[] k = properties.keySet().toArray();  
                     String line = "Stream properties:";  
                     for(int i = 0; i<properties.size(); i++){  
                          line += "\n\t" + k[i] + ":" + e[i];  
                     }  
                     log(line);  
                     //Set Audio Properties  
                     File file = new File(playlist.get(index));  
                  long audioFileLength = file.length();  
                     int frameSize = (int) properties.get("mp3.framesize.bytes");  
                  float frameRate = (float) properties.get("mp3.framerate.fps");  
                  audioFrameSize = frameSize;  
                  audioFrameRate = frameRate;  
                  audioDurationInSeconds = (audioFileLength / (frameSize * frameRate));  
                  log("\tframesize " + frameSize + " framerate " + frameRate);  
                  log("\tAudio File lenght in seconds is: " + audioDurationInSeconds);  
                }  
           });  
      }  
      public static AudioPlayer getInstance(){  
           if(instance == null)  
                instance = new AudioPlayer();  
           return instance;  
      }  
      /////////////////////////////////////  
 }  
While creating the object, I gave the player some of it's basic functionality, by adding to him a behaviour, I did this by adding a BasicListener with this.addBasicPlayerListener. Function in the listener are automatically called every one in a while by the BasicPlayer class (callback functions). Now follows the other implemented methods.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
 @Override
 public void play() throws BasicPlayerException {
  if(playlist.size() == 0)
   return;
  if(!paused || !opened){
   File f = new File(playlist.get(index));
   log("Opening file... " + f.getAbsolutePath());
   open(f);
   opened = true;
   super.play();
  }
  if(paused)
   super.resume();
  paused = false;
 }
 
 @Override
 public void pause() throws BasicPlayerException {
  log("Paused");
  paused = true;
  super.pause();
 }
 
 @Override
 public void stop() throws BasicPlayerException {
  paused = false;
  super.stop();
 }
 
 public boolean isPaused(){ return paused; }
 
 public boolean isOpenFile() { return opened; }
 
 public ArrayList<String> getPlaylist(){ return playlist; }
 
 public int getIndexSong(){ return index; }
 
 public void setIndexSong(int index){ this.index = index; lastSeekMs = 0; }
 
 public boolean isSeeking() { return isSeeking; }
 
 /**
  * goes to the next song in playlist and plays it
  * @throws BasicPlayerException
  */
 public void nextSong() throws BasicPlayerException{
  if(playlist.size() == 0)
   return;
  lastSeekMs = 0;
  paused = false;
  index = (index+1)%playlist.size();
  play();
 }
 
 /**
  * goes to the previous song and plays it
  * @throws BasicPlayerException
  */
 public void prvSong() throws BasicPlayerException{
  if(playlist.size() == 0)
   return;
  lastSeekMs = 0;
  paused = false;
  index = (index-1)%playlist.size();
  play();
 }
 
 /**
  * Adds a song to the playlist
  * @param songPath
  */
 public void addSong(String songPath) {
  playlist.add(songPath);
 }
 
 /**
  * Remove a song by index
  * @param index
  */
 public void removeSong(int index) {
  playlist.remove(index);
 }
 /**
  * Remove a song by songPath
  * @param songPath
  */
 public void removeSong(String songPath)
 {
  playlist.remove(songPath);
 }
 
 public byte[] getPcmData(){return cpcmdata;}
 
 public long getProgressMicroseconds(){return csms+lastSeekMs;}
 
 public float getAudioDurationSeconds() {return audioDurationInSeconds;}
 
 public float getAudioFrameRate() { return audioFrameRate; }
 
 public float getAudioFrameSize() { return audioFrameSize; }
 
 /**
  * Remembers what's the last position relative to the playing song
  * when seeking
  */
 public void setLastSeekPositionInMs(int seekMs)
 {
  lastSeekMs = seekMs;
 }
 
 
 /**
  * For logging
  * @param line
  */
 private void log(String line)
 {
  System.out.println("AudioPlayer] " + line);
  MainView.stf.addText("AudioPlayer] " + line);
 }
//End of class
As you have already noticed the code it's not that complex since most of the work is done by BasicPlayer, one of the BasicPlayer class though is that after seeking the returned progress in microsecond of the song is reset, as you can see in the code, this problem has been fixed with the use of one variable.