本文以实现MP3音乐播放器为例。一般来说,音乐播放器播放MP3会包含以下操作:
- 从头开始播放 (选中歌曲播放)
- 从指定位置开始播放 (暂停/恢复或者拖动/点击播放进度条)
- 播放结束后播放下一首 (自动切换)
- 播放过程中播放下一首 (手动点击下一首)
本文使用的依赖包为 jlayer
,maven
依赖如下:
1 | <dependency> |
初探
jlayer
提供了 Player
对象,可以直接播放MP3:
1 | public class MP3PlayDemo { |
运行后会发现,player.play()
方法是同步执行,也就是必须等到播放结束才会输出 end
。因此,需要将播放逻辑放在单独线程中执行:
1 | public class MP3PlayDemo { |
jlayer
还提供了另一个播放类 AdvancedPlayer
,该类的使用和 Player
类似,但是提供了更多方法,接下去的功能会使用该类实现。
播放暂停
有很多种方案可以实现播放与暂停的功能,比如暂停调用 wait()
使播放线程进入等待状态,恢复播放调用 notifyAll()
唤醒播放线程;也可以将 wait()/notifyAll()
方法替换为 park()/unpack()
方法,但这种方案都是对线程操作。
此处采用另一种方案,暂停时结束播放线程,同时记录已播放的时间,恢复播放时重新创建播放线程,并且从指定时间开始播放。
记录暂停时间
因为需要记录已播放的时间,先定义全局变量:
1 | private int pausedOnMillisecond = 0; // 毫秒 |
通过 AdvancedPlayer.setPlayBackListener(PlaybackListener)
设置回调方法,PlaybackListener
会在播放开始和播放终止 (调用 stop()
方法) 时调用:
1 | // PlaybackListener from javazoom.jl.player.advanced |
只需要实现 playbackFinished()
:
1 | private PlaybackListener playbackListener = new PlaybackListener() { |
可以通过 PlaybackEvent.getFrame()
获取已经播放的毫秒数,但是需要注意,假设指定从1000毫秒开始播放,在1500毫秒时结束,那么 PlaybackEvent.getFrame()
返回的是500,而非1500。也就是 PlaybackEvent.getFrame()
返回的是实际播放的毫秒数,并不包含跳过的毫秒数。
从指定时间开始播放
获取到暂停时间后,就要从暂停时间点开始播放,可以通过 AdvancedPlayer.play(start, end)
实现,需要注意,参数为帧数,而非毫秒数。
因此,播放逻辑就变成了:
1 | // 从头开始播放 |
计算MP3帧时长
首先,MP3每一帧的采样数是1152,是一个固定值。
然后,MP3支持的采样率有3种,44.1K Hz
,48K Hz
和 32K Hz
,而平时常见的MP3采样率,多数都是44.1KHz
。
因此,每一个采样时长为 1/44100
秒。
可以计算得每一帧的时长为:
1 | 每一帧的时长 |
完整的播放逻辑:
1 | // 将毫秒转为帧 |
暂停
暂停的逻辑直接调用 AdvancedPlayer.stop()
方法即可。
切换
切换下一首,可以看成是 播放 -> 暂停 -> 播放
逻辑,也就是调用 stop()
方法后,将 pausedOnMillisecond
重置为 0
,然后再次调用 play()
方法。
完整代码
1 | public class MP3AudioPlayer implements AudioPlayer { |
状态标识
对于音乐播放器的实现,需要记录播放状态 (播放,结束 (暂停))。对于第二种状态,如果是暂停,则正常结束播放线程即可;如果是结束,说明是当前音乐播放完成,需要准备播放下一首。
对于播放线程:
1 | Thread t = new Thread(() -> { |
在调用 play()
方法之前,记录下 status
当前的值 v1
,在 play()
方法后做一次比较,如果从 status
中取出来的值为 v1
,那就说明播放过程中没有暂停操作,是属于完整播放结束;而如果中间有暂停操作,会修改 status
的值。
播放方法:
1 | public class SimplePlayer { |