Java使用jlayer播放MP3

本文以实现MP3音乐播放器为例。一般来说,音乐播放器播放MP3会包含以下操作:

  • 从头开始播放 (选中歌曲播放)
  • 从指定位置开始播放 (暂停/恢复或者拖动/点击播放进度条)
  • 播放结束后播放下一首 (自动切换)
  • 播放过程中播放下一首 (手动点击下一首)

本文使用的依赖包为 jlayermaven 依赖如下:

1
2
3
4
5
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>mp3spi</artifactId>
<version>1.9.5.4</version>
</dependency>

初探

jlayer 提供了 Player 对象,可以直接播放MP3:

1
2
3
4
5
6
7
8
public class MP3PlayDemo {
public static void main(String[] args) throws Exception {
String mp3Path = "Sister's Noise.mp3";
Player player = new Player(new FileInputStream(mp3Path));
player.play();
System.out.println("end");
}
}

运行后会发现,player.play() 方法是同步执行,也就是必须等到播放结束才会输出 end。因此,需要将播放逻辑放在单独线程中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MP3PlayDemo {
// 结束标记
private static volatile boolean stop = false;

public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
try {
String mp3Path = "Sister's Noise.mp3";
Player player = new Player(new FileInputStream(mp3Path));
player.play();
stop = true;
} catch (Exception e) {
e.printStackTrace();
}
});
thread.setDaemon(true);
thread.start();
System.out.println("start");
// 阻塞主线程 等待播放线程结束
while (!stop) {
}
System.out.println("end");
}
}

jlayer 还提供了另一个播放类 AdvancedPlayer,该类的使用和 Player 类似,但是提供了更多方法,接下去的功能会使用该类实现。

播放暂停

有很多种方案可以实现播放与暂停的功能,比如暂停调用 wait() 使播放线程进入等待状态,恢复播放调用 notifyAll() 唤醒播放线程;也可以将 wait()/notifyAll() 方法替换为 park()/unpack() 方法,但这种方案都是对线程操作。

此处采用另一种方案,暂停时结束播放线程,同时记录已播放的时间,恢复播放时重新创建播放线程,并且从指定时间开始播放。

记录暂停时间

因为需要记录已播放的时间,先定义全局变量:

1
private int pausedOnMillisecond = 0; // 毫秒

通过 AdvancedPlayer.setPlayBackListener(PlaybackListener) 设置回调方法,PlaybackListener 会在播放开始和播放终止 (调用 stop() 方法) 时调用:

1
2
3
4
5
6
7
8
// PlaybackListener from javazoom.jl.player.advanced
public abstract class PlaybackListener
{
// 在播放开始时调用
public void playbackStarted(PlaybackEvent evt){}
// 在播放终止时调用
public void playbackFinished(PlaybackEvent evt){}
}

只需要实现 playbackFinished()

1
2
3
4
5
6
private PlaybackListener playbackListener = new PlaybackListener() {
@Override
public void playbackFinished(PlaybackEvent evt) {
pausedOnMillisecond = evt.getFrame() + pausedOnMillisecond; // 毫秒值
}
};

可以通过 PlaybackEvent.getFrame() 获取已经播放的毫秒数,但是需要注意,假设指定从1000毫秒开始播放,在1500毫秒时结束,那么 PlaybackEvent.getFrame() 返回的是500,而非1500。也就是 PlaybackEvent.getFrame() 返回的是实际播放的毫秒数,并不包含跳过的毫秒数。

从指定时间开始播放

获取到暂停时间后,就要从暂停时间点开始播放,可以通过 AdvancedPlayer.play(start, end) 实现,需要注意,参数为帧数,而非毫秒数。

因此,播放逻辑就变成了:

1
2
3
4
// 从头开始播放
advancedPlayer.play(0, Integer.MAX_VALUE);
// 从指定帧开始播放
advancedPlayer.play(pausedOnFrame, Integer.MAX_VALUE);

计算MP3帧时长

首先,MP3每一帧的采样数是1152,是一个固定值。

然后,MP3支持的采样率有3种,44.1K Hz48K Hz32K Hz,而平时常见的MP3采样率,多数都是44.1KHz

因此,每一个采样时长为 1/44100 秒。

可以计算得每一帧的时长为:

1
2
3
4
5
每一帧的时长 
= 每一帧的采样个数 * 每个采样的时长
= 1152 * 1/44100
≈ 0.026秒
≈ 26毫秒

完整的播放逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将毫秒转为帧
int pausedOnFrame = (int) (pausedOnMillisecond / 26);
try (FileInputStream fi = new FileInputStream(new File(url));
BufferedInputStream bufferedInputStream = new BufferedInputStream(fi)) {

advancedPlayer = new AdvancedPlayer(bufferedInputStream);
// 设置回调方法
advancedPlayer.setPlayBackListener(playbackListener);
// 从指定帧开始播放,默认第0帧,即从头开始
advancedPlayer.play(pausedOnFrame, Integer.MAX_VALUE);
} catch (IOException | JavaLayerException e) {
LogUtils.LOGGER.error("MP3音频播放失败. 文件: `{}`. 时间: `{}`.", url, millisecond, e);
throw new AudioPlayException("MP3音频播放失败");
}

暂停

暂停的逻辑直接调用 AdvancedPlayer.stop() 方法即可。

切换

切换下一首,可以看成是 播放 -> 暂停 -> 播放 逻辑,也就是调用 stop() 方法后,将 pausedOnMillisecond 重置为 0,然后再次调用 play() 方法。

完整代码

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
public class MP3AudioPlayer implements AudioPlayer {

private int pausedOnMillisecond = 0;

private AdvancedPlayer advancedPlayer;

private PlaybackListener playbackListener = new PlaybackListener() {
@Override
public void playbackFinished(PlaybackEvent evt) {
pausedOnMillisecond = evt.getFrame() + pausedOnMillisecond; // 毫秒值
}
};

@Override
public void play(String url, int millisecond) {

this.pausedOnMillisecond = millisecond;

int pausedOnFrame = (int) (pausedOnMillisecond / 26.122);
try (FileInputStream fi = new FileInputStream(new File(url));
BufferedInputStream bufferedInputStream = new BufferedInputStream(fi)) {

advancedPlayer = new AdvancedPlayer(bufferedInputStream);
advancedPlayer.setPlayBackListener(playbackListener);

advancedPlayer.play(pausedOnFrame, Integer.MAX_VALUE);
} catch (IOException | JavaLayerException e) {
LogUtils.LOGGER.error("MP3音频播放失败. 文件: `{}`. 时间: `{}`.", url, millisecond, e);
throw new AudioPlayException("MP3音频播放失败");
}
}

@Override
public int pause() {
advancedPlayer.stop();
return pausedOnMillisecond;
}

@Override
public void stop() {
if (advancedPlayer != null) {
advancedPlayer.stop();
}
}
}

状态标识

对于音乐播放器的实现,需要记录播放状态 (播放,结束 (暂停))。对于第二种状态,如果是暂停,则正常结束播放线程即可;如果是结束,说明是当前音乐播放完成,需要准备播放下一首。

对于播放线程:

1
2
3
4
5
6
7
Thread t = new Thread(() -> {
mp3AudioPlayer.play(url, millisecond);
// 在主线程调用了stop()方法,应该修改状态为 暂停 还是 结束?
});
t.setName("t-player");
t.setDaemon(true);
t.start();

在调用 play() 方法之前,记录下 status 当前的值 v1,在 play() 方法后做一次比较,如果从 status 中取出来的值为 v1,那就说明播放过程中没有暂停操作,是属于完整播放结束;而如果中间有暂停操作,会修改 status 的值。

播放方法:

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
public class SimplePlayer {
// 结束/暂停状态
private int stop = 0;
// 播放中
private int playing = 1;
// 状态记录值
private AtomicStampedReference<Integer> status = new AtomicStampedReference<>(stop, 1);

// ........

private synchronized void play(String url, int millisecond) {
// 状态修改:停止->播放中 成功时,新启动线程播放
if (status.compareAndSet(stop, playing, status.getStamp(), status.getStamp() + 1)) {
Thread t = new Thread(() -> {
// 播放前记录当前版本号
final int version = status.getStamp();
mp3AudioPlayer.play(url, millisecond);
// 设置成功,说明版本号未变更,属于正常播放完成
if (this.status.compareAndSet(playing, stop, version, status.getStamp() + 1)) {
LogUtils.LOGGER.info("prepare next song...");
// 正常结束,需要准备播放下一首
}
});
t.setName("t-player-" + status.getStamp());
t.setDaemon(true);
t.start();
} else {
// 先停止播放线程
mp3AudioPlayer.stop();
// 修改播放状态
this.status.compareAndSet(playing, stop, status.getStamp(), status.getStamp() + 1);
// 播放
play(url, millisecond);
}
}

public synchronized void pause() {
if (status.compareAndSet(playing, stop, status.getStamp(), status.getStamp())) {
this.pausedOnMillisecond = mp3AudioPlayer.pause();
}
}
}