本地音乐播放器-附源码(JavaFX-SoPlayer)

大二学java一个多月了,正好2.14情人节想做的点什么东西,于是心血来潮写了个粉嫩粉嫩的播放器布局(这个也放github里面了),后来一发不可收拾不断改进,前后近三个星期遂步进新世界。

基于JavaFX soplayer 音乐播放器

界面清新简洁,代码注释详实,适合作为入门项目

采用JavaFX组件完成的一款小巧、界面精美的本地音乐播放器,支持添加本地音乐、歌曲及歌词文件解析、歌词滚动、歌单列表、频谱图展示、歌词海报显示、自定义背景、系统托盘控制、快捷键等。同时使用.ini文件记录应用设置信息。

(soplayer 源码Github地址) 如果觉得不错,欢迎star~ 这也是对我的肯定和鼓励,谢谢了。
(soplayer 文档地址)

先上图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

主要功能清单

 支持系统托盘控制和全屏控制
 界面简洁而精美且支持自定义背景
 支持播放的音乐格式:.mp3 文件、.wav 文件、.aac 文件
 支持解析歌词并展示(.lrc文件)
 支持解析歌词文件(缩略图、专辑、时长等)
 支持频谱图动效
 支持拖动添加文件
 支持歌单列表控制

说明

documentation content
在这里插入图片描述
function exposition
在这里插入图片描述

layout panel description
在这里插入图片描述

communication

在此想给大家分享一下实现思路,交流学习,实现过程也经历了挺多坑,花了很多时间从stackOverflow、官方文档、csdn翻阅资料。JavaFX在csdn资源挺少的,建议可以多逛逛stackOverflow。

部分实现核心代码及思路

首先我们来分析对象,驱动最核心的播放模块,需要的是歌曲、歌词(.lrc)两种文件对象。我们创建个库存储,为拓展数据库提供方便的接口
在这里插入图片描述


播放模块:
歌曲文件放进mediaPlayer的对象mPlayer中即可。既然是播放器,那就有很多首歌可以放进mediaPlayer,在这里我们把media文件放进容器数组里,这样我们就可以通过index引索来访问它们了!

  //填充mediaPlayer
    private void fillPlayer()  {
        try{
            SongFile sF=mList.get(index);
            Media curMedia=sF.use();
            //填充来自Database得media
//            mPlayer.stop();
            mPlayer = new MediaPlayer(curMedia);
            //异常处理事件
            mPlayer.setOnError(()->System.out.println("media error"
                    + mPlayer.getError().toString()));
            //填充歌曲文件之后填充(匹配)歌词文件
            setAutoPlay();   //recommend
            }catch (Exception e){
                e.printStackTrace();
        }
    }

音乐的播放和暂停都有对应方法,具体通过事件激活即可。我们谈一下上一曲、下一曲的实现。围绕引索展开,首先设置两个标记,我这里用MARK来表示。如果不添加 mPlayer.dispose(); ,切歌的时候会有重叠声音,所以事先dispose()当前播放。判断标记,下一曲则index加一,反之减。

//计算下一首的index
    private void getPlayListIndex(){
        if (mPlayer != null) {
            mPlayer.dispose();
            System.gc();
            //控制下一首和后一首
            if (MARK==Mark.NEXT) {
                index++;
                if (index >= mList.size()) {
                    index = 0;
                }
            }
            else if(MARK==Mark.LAST){
                index--;
                if (index < 0) {
                    index = mList.size() - 1;
                }
            }
        }
        else {
            //如果此前无播放(程序刚刚初始化)则定义从第一首开始放
            index = 0;
        }
    }

获取歌曲数据,可以解析到歌曲海报、歌名、专辑等,以hash表形式存储在下面代码的map里。
在mPlayer的监听事件里,

    mPlayer.setOnReady(new Runnable() {
                       @Override
                       public void run() {
						 ObservableMap<String,Object> map=mList.get(index).use().getMetadata();
					}
	}

获取播放的时间更新播放文本、进度条,到时则根据当前播放模式决定index怎么变
还是在mPlayer的监听事件里,

 mPlayer.currentTimeProperty().addListener(new ChangeListener<Duration>() {
            //                    ContinuousAudioDataStream
            @Override
            public void changed(ObservableValue<? extends Duration> observable, Duration oldValue, Duration newValue) {
                if(!mouse){
                    sPlayBack.setValue(newValue.toSeconds());
                    current.refurbish(mPlayer.getCurrentTime());
                    total.refurbish(mPlayer.getTotalDuration());
                    //歌曲时间文本
                    TimeOfPlayBack.timeShowStart(text,current,total);
                    //歌词滚动

                    timeOfPlayBack.lyricsStart(installLyrics.getLyricText(),current,installLyrics.getLyrics());
                }
                //歌曲结束,下一步做什么依赖此时播放模式
                double exp=10E-2;
                if(Math.abs(newValue.toSeconds()-mPlayer.getStopTime().toSeconds()) < exp ){
                    playPattern();
                }
            }
        });

三种播放模式功能,单曲就调用mPlayer方法 无限制循环,循环播放默认index一直加一,随机播放有两种实现方式(用shuffle算法打乱播放队列,index取随机数)

  //循环    单曲    随机播放
    private void playPattern(){
        if(PLAYPATTERN==PlayPattern.LOOP) loop();
        else if(PLAYPATTERN==PlayPattern.SINGLELOOP) singleLoop();
        else if(PLAYPATTERN==PlayPattern.RANDOM) randomPlay();
    }

    //因为数据结构暂时采用实现了Collections的数组容器,随机播放可以采用shuffle算法
    private void randomPlay(){
//      int i= (int)Math.floor(Math.random()*(mList.size()+1));
        int i=0;
        while(true) {
            i = (int) (Math.random() * mList.size());
            //不可重复播放同一首歌,size等于1则可以重复播放同一首歌
           if(i!=index || mList.size()==1) break;
        }
//        Collections.shuffle(mList);
        //计算随机数
        initMusic_Index(i);
    }

    private void singleLoop(){
        //无限循环,当前时间的监听事件的一个参数,不以媒体结束时间为上限,可用来计算播放该音乐的总时间
        mPlayer.setCycleCount(MediaPlayer.INDEFINITE);
        mPlayer.play();
    }

    private void loop(){
        MARK=Mark.NEXT;
        initMusic();
    }

频谱图通过事件监听获取频谱数组magnitudes,是一个size约有150的值-60到0浮点数组。一般将值进行处理后,建矩形实现频谱动态效果,也可以加动画效果。

   mPlayer.setAudioSpectrumListener(new AudioSpectrumListener() {
            @Override
            public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {
                //更新高度数据并绘制矩形阵频谱    showPic为false代表展示频谱,不展示海报
                chartsSpectrumData.refurbishRecChart(magnitudes,showPic);
                //更新高度数据并绘制环形阵频谱   showPic为false代表展示频谱,不展示海报
                chartsSpectrumData.refurbishCirChart(magnitudes,showPic);
            }
        });

频谱图刷新示例

  /**
     * 刷新环形图表
     * @param magnitudes 频谱
     * @param showPic 代表海报是否显示
     */
    public void refurbishCirChart(float[] magnitudes,boolean showPic){
        //更新高度数据并绘制环形阵频谱   showPic为false代表展示频谱,不展示海报
        //一层if
        if(showPic) {
            for (int i = 0; i < rectangle_num; i++) {
                rectangles2[i].setHeight(0);
            }
            return;
        }
        for(int i=0;i<rectangle_num;i++){
            rectangles2[i].setHeight(ChartsSpectrumData.handleMagnitudes(magnitudes[i])*IniConfig.getD("heightRatio"));
        }
    }

歌单列表的抽屉动画效果,menuBox是VBox,放菜单的面板。
时间轴动画实现,凭借AnchorPane随意设置的特点移动面板


 /**
     * 菜单创建
     * @param ap 创建的位置
     */
    public void createMenu(AnchorPane ap){
        menuBox=new VBox();
        menuBox.setVisible(true);
        menuBox.setPrefWidth(IniConfig.getI("menuBoxPrefWidth"));
        menuBox.setPrefHeight(IniConfig.getI("menuBoxPrefHeight"));
        menuBox.setTranslateY(IniConfig.getI("menuBoxTranslateY"));
        menuBox.setBackground(new Background(new BackgroundFill(Color.GREY,null,null)));
        menuBox.setTranslateX(-menuBox.getPrefWidth());
        AnchorPane.setTopAnchor(menuBox,IniConfig.getD("menuBoxTop"));
        AnchorPane.setBottomAnchor(menuBox,IniConfig.getD("menuBoxBottom"));
        ap.getChildren().addAll(menuBox);
    }
 /**
     * 显示菜单
     * @param btList 列表控制按钮
     * @param pListPic 列表控制按钮图片
     */
    private void showMenu(Button btList,ImageView pListPic){
        btList.setDisable(true);
        menuBox.setTranslateX(-menuBox.getPrefWidth());
        Timeline t1=new Timeline();
        KeyValue kv=new KeyValue(menuBox.translateXProperty(),0, Interpolator.EASE_IN);
        KeyFrame kf=new KeyFrame(Duration.seconds(IniConfig.getD("AnimationTimeSeconds")),kv);
        t1.getKeyFrames().add(kf);
        t1.setOnFinished(e->{
            isShow=true;
            btList.setGraphic(pListPic);
            btList.setDisable(false);
        });

        t1.play();
    }

    /**
     * 隐藏菜单
     * @param btList 列表控制按钮
     * @param pListRPic 列表控制按钮图片
     */
    private void hideMenu(Button btList,ImageView pListRPic){
        btList.setDisable(true);
        Timeline t1=new Timeline();
        KeyValue kv=new KeyValue(menuBox.translateXProperty(),-menuBox.getPrefWidth());
        KeyFrame kf=new KeyFrame(Duration.seconds(IniConfig.getD("AnimationTimeSeconds")),kv);
        t1.getKeyFrames().add(kf);
        t1.setOnFinished(e->{
            isShow=false;
            btList.setGraphic(pListRPic);
            btList.setDisable(false);
        });
        t1.play();
    }

歌词显示是一个小难点
通过遍历去匹配歌曲的合适歌词文件,以 GBK 编码格式读取歌词文件,用正则处理歌 词并存储入 TreeMap(按键值升序排列的映射) 显示歌词则比对 key 和播放时间即可。
这是先匹配到歌词文件,再用正则匹配歌词:

public void fillLyrics(SongFile songFile, ArrayList<LyricsFile> lFile) {
         //Text initiate-------------------------
         try {
             //词曲匹配
             File f = matchLyricsFile(songFile,lFile);
             //删除之前存放的歌词
             killLyrics(lyrics);
             //放置null指针错误
             if(f==null) return;
             //读取文件
             FileInputStream fis = new FileInputStream(f);
             InputStreamReader isr = new InputStreamReader(fis,"gbk"); //指定以gbk编码读入
             BufferedReader br = new BufferedReader(isr);
             String thisLine;
             //正则
             Pattern pattern = Pattern.compile("\\d{2}:\\d{2}.\\d{2}");
//             BufferedReader br = new BufferedReader(new FileReader(f));

             while ((thisLine = br.readLine()) != null) {
                 handleLyricsFile(thisLine, pattern);  //URLDecoder.decode( thisLine, "GBK" )
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
         //test result:    result 不能填装key为空的歌词
         //利用迭代器刷新余下文本
         iter = lyrics.keySet().iterator();
         //initiate mediaPlayer on Ready  && lyric introduce successfully---------------------------
         for (int i = 3; i < nn; i++) {
             if (iter.hasNext()) {
                 Object o = iter.next();
                 lyricText[i].setText(lyrics.get(o));
             }
         }
         //至此完成nn个文本的全部初始化刷新
    }

这里为刷新歌词文本显示的核心代码:

  /**
     *  刷新歌词文本
     * @param lyricText 歌词文本
     */
     void  open(Text[] lyricText) throws  Exception{
        Long key=this.getTimeScroll();
        if(key==null || !lyrics.containsKey(key) )  //key重复或未查询到key,则退出函数
            return ;
        String center = lyrics.get(key);   //value
        lyricText[1].setText(center);  //初始化高亮核心层
        if (lyrics.lowerKey(key) != null) {      //刷新高亮层的上一层的text
            lyricText[0].setText(lyrics.get(lyrics.lowerKey(key)));
        }
        if (lyrics.higherKey(key) != null) {    //刷新高亮层的下三层的text
            try {
                SortedMap<Long, String> map =lyrics.tailMap(lyrics.higherKey(key));   //剩余未播放的歌词填入排序map

//                WeakHashMap<> whm=lyrics.tailMap(lyrics.higherKey(key));
                System.gc();
                     //遍历刷新三个text
                int index = 2;
                int limit=map.size();   //剩余未播放的歌词数量
                for (Map.Entry entry : map.entrySet()) {
                    if (index > 4) break;     //只能刷新2、3、4三层
                    String value = (String) entry.getValue();
                    if(limit>=index) {
                        lyricText[index].setText(value);
                    }
                    else {    //拒绝剩余刷新歌词  因为foreach循环遍历 map.entrySet()
                             lyricText[index].setText("");
                    }
                    //                    int key = (int) entry.getKey();
                         index++;
                }
            }catch (ArrayIndexOutOfBoundsException e){
                e.printStackTrace();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


文件拖动添加功能实现,调用面板四个监听,再用拖拽板接收文件。进而把文件传进数据库类的对象

   /**
     * 歌曲歌词文件拖动添加
     * @param pane 支持的面板区域
     * @param menuService 操作歌曲列表菜单
     */
    public void invokeFileDrag(Pane pane, MenuService menuService){

        pane.setOnDragEntered(new EventHandler<DragEvent>() {
            @Override
            public void handle(DragEvent event) {
                //提供视觉效果 下同
                pane.setBorder(new Border(new BorderStroke(Paint.valueOf("#FFF0F5"), BorderStrokeStyle.SOLID,new CornerRadii(0),new BorderWidths(1))));
            }
        });

        pane.setOnDragExited(new EventHandler<DragEvent>() {
            @Override
            public void handle(DragEvent event) {
                pane.setBorder(new Border(new BorderStroke(Paint.valueOf("#FFF0F500"), BorderStrokeStyle.SOLID,new CornerRadii(0),new BorderWidths(1))));
            }
        });

        pane.setOnDragOver(new EventHandler<DragEvent>() {
            @Override
            public void handle(DragEvent event) {
                event.acceptTransferModes(TransferMode.COPY);
            }
        });

        pane.setOnDragDropped(new EventHandler<DragEvent>(){
            @Override
            public void handle(DragEvent event) {
                pane.setBorder(new Border(new BorderStroke(Paint.valueOf("#FFF0F500"), BorderStrokeStyle.SOLID,new CornerRadii(0),new BorderWidths(1))));
//                event.acceptTransferModes(TransferMode.COPY);
                //拖拽板
                Dragboard db=event.getDragboard();
                //收集文件,对文件进行处理
                if(db.hasFiles()) {
//                    System.out.println("666");
                    List<File> rawFiles = db.getFiles();
//                    List<File> files = filter(rawFiles);
//                    FileHandler fileHandler=new FileHandler();
                    FileHandler.effect(rawFiles,database,menuService);    //过滤+去重
                }

            }
//        return null;
        });
    }

系统托盘实现是套用awt,javafx引入awt有个线程问题,用这个子线程方法解决报错

   Platform.runLater(new Runnable() {
               @Override
               public void run() {
               //你的线程
               }
   }

太晚了,暂时先写到这里了,以后有想到再补充。


关于类的设计:
建议播放器主类(Player)模块继承实现。

发布了23 篇原创文章 · 获赞 10 · 访问量 4983

猜你喜欢

转载自blog.csdn.net/Fzidx/article/details/104696814
今日推荐