本文以实现MP3音乐播放器为例。一般来说,音乐播放器播放MP3会包含以下操作:
- 从头开始播放 (选中歌曲播放)
- 从指定位置开始播放 (暂停/恢复或者拖动/点击播放进度条)
- 播放结束后播放下一首 (自动切换)
- 播放过程中播放下一首 (手动点击下一首)
本文使用的依赖包为 jlayer
,maven
依赖如下:
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
| 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 Hz
,48K Hz
和 32K 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); 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); }); 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(); } } }
|