先附上我实现的JavaFX音乐播放器的下载地址和码云地址。
下载地址(懒人需花费1CB,并且还存在一些缺陷,在码云中都修复了):https://download.****.net/download/xss13/10691621
码云地址:https://gitee.com/com_shisan/MyUtils
爬虫地址:https://gitee.com/com_shisan/music-lib
基于JavaFX+WebView实现的音乐播放器,这里主要谈谈遇到的一些坑。
- WebView的缺陷
- load 本地资源的解决方案
- 没找到一篇相关的JAudioTagger对MP3文件的完整操作的文章
- JavaMod并没有一篇成型的API文档,虽然官网说 how to use javamod as api?
- 想要支持全格式但还要兼顾电平脉冲(Javafx自带的脉冲效果很水),JavaMod不支持aac,mp4,m4a等格式
WebView的缺陷
WebView实际上是有很多坑和bug等着你去踩的,当初遇到的第一个问题是程序点击关闭按钮后仍然在后台运行,经过使用jconsole等工具观察,发现是WebView页面存在着gif动图,在关闭时该播放线程没有被销毁导致的,我在onCloseRequest的时候加上了webEngine.loadContent("")才解决了这个问题。除此之外,有时候css样式并不能完全支持,有些瑕疵,最坑的是如果你希望在WebView中使用h5的最新且在试验阶段的API,那就别想了,更何况AudioContext这样的对象都不存在。更多坑等你自己发觉。
为了简化webview与java通讯,本人也是费了很大周折最后决定使用了json+注解的方式传输,依赖了阿里的fastjson和jdk.internal.org.objectweb.asm包下的类扫描工具(话说这些工具类好像在JDK9及之后版本就访问不到了)。
load 本地资源的解决方案
作为一个内嵌在Java内部的浏览器,怎么会不支持load local source?与常规浏览器一样会报not allow load local source的错误,经过在*上一番折腾,终于发现一个可以有效解决次问题的解决方案:自定义protocol。
public class ClassPathURLStreamHandlerFactory implements URLStreamHandlerFactory {
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
if("classpath".equals(protocol)){
return new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
try{
String path = u.getPath();
if(path.startsWith("/")) path = path.substring(1);
URL url = (Scanner.getUrlLoader()==null?getClass().getClassLoader():Scanner.getUrlLoader()).getResource(path);
return url.openConnection();
}catch (IOException e){
throw e;
}catch (Exception e){
}
return null;
}
};
}
return null;
}
}
然后在你的程序启动类上加个static块注册以一就OK了,如果你希望读取local资源你可以自定义URLConnection就可以解决这个额问题。
static{
URL.setURLStreamHandlerFactory(new ClassPathURLStreamHandlerFactory());
}
没找到一篇相关的JAudioTagger对MP3文件的完整操作的文章
JAudioTagger无疑是一个相当不错的Java版本的音频文件标签信息解析工具,它不光支持读取还支持修改,只是不知道为什么网上的文章里没有一个相关的修改操作的文章。早在14年之前我就尝试用它获取过mp3文件信息,只是当时水平有限,既不会读既有的api和demo,且debugger源码的功底也不深厚再加上当时对这个需求不是很强烈,导致一直使用的是之前从网上copy出来的一段很不规范的代码来读取MP3音频文件。这次对源码的读和写都做了较深入的debugger跟踪,加上查看了官网给的文档,实现这个功能并不难。
先说明以下JAudioTagger自带了对字符串编码猜测功能,只不过在某一行有个小小的bug,我对源码做了小小修改(加上之前一个****哥们写的内容,时间太久远记不起来是谁了),实现了自动猜测字符串编码功能,详见我的 com.music.player模块内的com.music.util.ReadMp3Info类的guessAndCovertText方法。
//获取Mp3音频标签
try {
MP3File mp3F = new MP3File(path);
AbstractID3v2Tag d32 = mp3F.getID3v2Tag();
if (d32 != null) {
title = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_TITLE));
author = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_ARTIST));
special = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_ALBUM));
date = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_YEAR));
if(StringUtil.isNull(date)){
date = guessAndConvertText(d32.getFirst(ID3v23Frames.FRAME_ID_V3_TYER));
}
node = guessAndConvertText(d32.getFirst(ID3v23Frames.FRAME_ID_V3_UNSYNC_LYRICS));
AbstractID3v2Frame imgData = d32.getFirstField("APIC");
if(imgData != null){
AbstractTagFrameBody imgDataBody = imgData.getBody();
if(imgDataBody instanceof FrameBodyAPIC){
picByte = ((FrameBodyAPIC) imgDataBody).getImageData();
picBase64 = Base64.getEncoder().encodeToString(picByte);
String picType = checkImage(picByte);
if(picType!=null) this.picType = picType.toLowerCase();
}
}
}
ID3v1Tag d31 = mp3F.getID3v1Tag();
if (d31 != null) {
if (StringUtil.isNull(title)) {
title = guessAndConvertText(d31.getFirstTitle());
}
if (StringUtil.isNull(author)) {
author = guessAndConvertText(d31.getFirstArtist());
}
if (StringUtil.isNull(special)) {
special = guessAndConvertText(d31.getFirstAlbum());
}
if (StringUtil.isNull(date)) {
date = d31.getFirstYear();
if (StringUtil.isNull(date)) {
date = "";
}
}
}
} catch (Exception ex) {
}
对于MP3标签的修改,是读取的逆向过程,这里又遇到了一个坑,那就是AbstractID3v2Frame类的setContent方法。刚开始我以为对title等普通信息直接setContent就可以了,但是总是报相同的错误,查明原因是:
/**
* Sets the content of the field.
*
* @param content fields content.
*/
public void setContent(String content)
{
throw new UnsupportedOperationException("Not implemented please use the generic tag methods for setting content");
}
所有的实现类中没有一个类实现了该方法!所以我们只能调用setBody方法来实现信息的写入了:
Map<String,Object> otherInfo = musicInfo.getOtherInfo();
ImageUtil.handleMp3InfoPic(otherInfo,cacheDir);
String title = otherInfo==null?
musicInfo.getName():(String)otherInfo.getOrDefault("displayName",musicInfo.getName());
//将专辑等信息写入到mp3
MP3File mp3File = new MP3File(targetFile);
AbstractID3v2Tag id3v2Tag = mp3File.getID3v2Tag();
if(id3v2Tag == null){
id3v2Tag = new ID3v23Tag();
}
AbstractTagFrameBody body;
//title 标题
AbstractID3v2Frame abstractID3v2Frame = new ID3v24Frame(ID3v24Frames.FRAME_ID_TITLE);
abstractID3v2Frame.setEncoding("UTF-8");
body = new FrameBodyTIT2(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),title);
abstractID3v2Frame.setBody(body);
id3v2Tag.setFrame(abstractID3v2Frame);
//artist 艺术家(本应用称作作者)
String info;
info = (String) otherInfo.get("author");
if(StringUtil.isNotNull(info)){
abstractID3v2Frame = new ID3v24Frame(ID3v24Frames.FRAME_ID_ARTIST);
abstractID3v2Frame.setEncoding("UTF-8");
body = new FrameBodyTPE1(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),info);
abstractID3v2Frame.setBody(body);
id3v2Tag.setFrame(abstractID3v2Frame);
}
//album 专辑
info = (String) otherInfo.get("albumName");
if(StringUtil.isNotNull(info)){
abstractID3v2Frame = new ID3v24Frame(ID3v24Frames.FRAME_ID_ALBUM);
abstractID3v2Frame.setEncoding("UTF-8");
body = new FrameBodyTALB(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),info);
abstractID3v2Frame.setBody(body);
id3v2Tag.setFrame(abstractID3v2Frame);
}
//pic 图片
info = (String) otherInfo.get("img");
if(StringUtil.isNotNull(info)){
abstractID3v2Frame = new ID3v24Frame("APIC");
FrameBodyAPIC picBody = new FrameBodyAPIC();
picBody.setPictureType((byte)1);
picBody.setTextEncoding(TextEncoding.getInstanceOf().getIdForValue("ISO-8859-1").byteValue());
//data:image/jpeg;base64,xxxxx
picBody.setMimeType(info.substring(info.indexOf(":")+1,info.indexOf(";")));
picBody.setImageData(Base64.getDecoder().decode(info.substring(info.indexOf(",")+1)));
picBody.setDescription("");
abstractID3v2Frame.setBody(picBody);
id3v2Tag.setFrame(abstractID3v2Frame);
}
//lrc 歌词
if(readLRC!=null && StringUtil.isNotNull(readLRC.getLrcStr())){
String lrc = readLRC.getLrcStr();
abstractID3v2Frame = new ID3v23Frame(ID3v23Frames.FRAME_ID_V3_UNSYNC_LYRICS);
abstractID3v2Frame.setEncoding("UTF-8");
body = new FrameBodyUSLT(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),"","",lrc);
abstractID3v2Frame.setBody(body);
id3v2Tag.setFrame(abstractID3v2Frame);
}
mp3File.setID3v2Tag(id3v2Tag);
//save()与commit()做相同的事情,commit()调用了save()
mp3File.commit();
这里还有两个坑:
- 不要自以为是的把某个属性设置为null,不然会报空指针异常让你摸不着头脑,你可以使用空字符串替代
- 每一种信息都对应了自己的AbstractID3v2Frame实现类,这里给个参考吧:
支持的编码有四种,自己体会:
自行查看AbstractID3v2Frame的所有实现类看有哪些需要自己去修改的,如果头晕对不上,就去官网看文档,这里给个截图和文档传送门:(写这片博客时网页改版了)
http://www.jthink.net/jaudiotagger/tagmapping.html
JavaMod并没有一篇成型的API文档,虽然官网说 how to use javamod as api?
先上一张JavaMod图:
着是国外一个大佬从2006年起开始做的音乐播放器,
没有API相关文档,Java版本的用Swing实现,效果还不错,给个传送门:http://www.javamod.de/
这里我对他的部分代码做了简单的抽取就直接拿来用了,他的软件还是有个小bug,就是你设置的volumn和blance每次播放心下一首都会被清0,你还得重新去调,就是音乐音量和平衡每次播放都会被重新重置为默认值,你的设置失效了,而界面上没有做回馈,我修改了他的这一块的源码已解决这个问题。
想要支持全格式但还要兼顾电平脉冲(Javafx自带的脉冲效果很水),JavaMod不支持aac,mp4,m4a等格式
javafx自己本身就有音频相关解码器,mp4,m4a,mp3,aac等格式不在话下都是支持的,但是它提供的音乐可视化效果不尽人意,所以还是希望使用大佬们的三方插件来对aac,m4a和mp4做支持,实在是找不到对应的解码包我也懒得找了,JAVE是个不错的选择,拿来做格式转换然后再播放。JAVE功能很强大 ,几乎全格式支持,并且转换后音乐的标签也不会丢失。
再说说爬虫吧
我做的这个音乐播放器,爬取了酷狗,千千和虾米的搜索页面,酷狗(最简单)和千千简单,虾米相当费劲,虾米的音频url使用了恺撒加密,本人也是花了近一周的时间去解,到目前还是有低概率解析不出来,不过不是很影响因为加密结果是动态生成的,本次不成功可以再搜一次再解析。
写在最后
研究这些开源的软件,本着一个可以自定义界面的兴趣去做的,如果你有心思去做,那当然都是有办法去实现的。