音乐播放器

说明

此项目是基于JavaSwing实现的一个简单的音乐播放器。涉及的知识点主要有 JavaGUI 多线程 JavaSound(与音频有关的原生API)
全文思维导图
在这里插入图片描述

一、最终效果与功能

1.效果

图片效果

2.功能

见思维导图

二、主要功能的实现

1.菜单栏的显示及文件选择

(1)菜单栏

此处主要是使用MenuBar组件,这是一个类似于JPanel面板的"大组件",点击后可显示器包含的Menu组件,此处我添加了About组件,其又可以展开为MenuItem(即此处的Author),这里我是为了显示作者信息,因此又涉及 JDialog 组件,该组件有多种类型,主要取决于该消息框是否需要自带选项(如OK等),【也可以自己加按钮选项】。给其绑定点击事件,用于显示Dialog组件,该组件不能够直接显示字符串信息,但可以添加JLabel组件,把消息设置在JLabel中。

MenuBar menubar = new MenuBar();
Menu menuAbout = new Menu("About");
MenuItem Author = new MenuItem("Author");
menuAbout.add(Author);
JDialog AboutDialog = new JDialog(this,"Author",false);
JLabel AuthorMessage = new JLabel("The Author is Windx ^_^");
AboutDialog.add(AuthorMessage);
menuAbout.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        AboutDialog.setSize(200, 100);//设置其大小
        AboutDialog.setLocation(420, 320);//设置其位置
        AboutDialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);//设置关闭模式
        AboutDialog.setVisible(true);//设置可见
    }
});
menubar.add(menuAbout);
setMenuBar(menubar);

(2)文件选择

此处涉及到上面提到的MenuBar组件,FileDialog 类型的消息框(不选择某个目录就不会退出)。获取后将该目录内容均添加到另一个组件 list 列表中。 注意:此处虽然使用add()将歌曲名添加到了列表中,但还是需要另外处理。

 //设置菜单,用来选文件
MenuBar menubar = new MenuBar();
Menu menufile = new Menu("File");
MenuItem menuOpen = new MenuItem("Open", new MenuShortcut(KeyEvent.VK_0));
menufile.add(menuOpen);
menufile.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        open();
        list.setPreferredSize(new Dimension(200, 100));//设置列表的大小
        String[] arr = new String[songs.size()];
        list.setListData(songs.toArray(arr));  //设置选项数据(内部自动封装为ListModel)

        list.setVisible(true); //当列表中有内容加载时,就设置可见
            status = true;
            listbtn.setText("隐藏");
            }
        });
menubar.add(menufile);

//打开文件,选择播放目录
void open() {
    //打开文件
    FileDialog dialog = new FileDialog(this, "Open", 0);
    dialog.setVisible(true);
    filepath = dialog.getDirectory();
    if (filepath != null) {
        labelfilepath.setText("当前播放目录:" + filepath);
        //显示文件列表
        list.removeAll();
        File filedir = new File(filepath);  //创建一个文件夹对象
        File[] filelist = filedir.listFiles();  //listFiles()方法可返回一个抽象路径名数组,表示由该抽象路径名表示的目录中的文件。
        for (File file : filelist) {
            String filename = file.getName().toLowerCase();
            if (filename.endsWith(".mp3") || filename.endsWith(".wav")) {
                songs.add(filename);  //在文件列表中添加所遍历目录中的音乐文件名
            }
        }
    }
}

2.列表

列表的难点主要有两个,一个是如何控制列表中的数据项(包括数据项的增删改查等),一个是如何将列表放到JScrollPanel中,从而实现可以滚动。
此处我只解决了前者,后者在 待解决的问题 中再聊。

列表的数据项控制

列表中数据项的控制主要通过其ListModel实现,每个列表有其对应的ListModel,可以通过getModel()方法拿到。也可以为其设置ListModel。
添加数据项,需要在选择音乐目录时就将其下的音乐添加到列表中,如下

list.setPreferredSize(new Dimension(200, 100));//设置列表的大小
String[] arr = new String[songs.size()];
list.setListData(songs.toArray(arr));  //设置选项数据(内部自动封装为ListModel)

defaultListModel.addAll(songs);  //songs是一个包含音乐文件名的ArrayList对象
list.setModel(defaultListModel);
从上面可以看出,一方面要使用list.setListData(Collection)设置数据源,另一方面又要设置ListModel,使用addAll(String[]) 添加到模型。

**获取被选中的数据项 **,如只需获取其索引号,只需使用 list.getSelectedIndex(); 要选择索引号对应的字符串,则需要使用其ListModel的方法,即 list.getModel().getElementAt(index);
修改被选中的数据项 , list.setSelectedIndex(current); //将文件索引替换为上一首歌索引

3.音乐相关

(1)音乐播放

此处音乐播放使用的是基于JavaSound原生音频API实现的,其落脚点还是在于读取文件,不过读取的是音频文件而已。但要注意:此处涉及线程,因为在音乐播放的同时,不能干扰用户对程序其他部分的控制。【也正是这里为后面线程的使用埋下了隐患,我有一点解决思路,但没有成功】
总之,先放代码

public void play() {
            try {
                System.out.println(""); //空一行
                File file = new File(filepath + filename); // 获取当前选中歌曲的路径,创建文件对象
                System.out.println(" 开始播放:" + filename);  //打印播放当前所选的歌曲

                labelfileName.setText("当前播放音乐:" + filename); //将当前播放文件名赋值给标签内容,显示出来
                //取得文件输入流
                audioInputStream = AudioSystem.getAudioInputStream(file);
                //从提供的输入流中获取音频输入流。 流必须指向有效的音频文件数据。 该方法的实现可能需要多个解析器来检查流以确定它们是否支持它。
                audioFormat = audioInputStream.getFormat();

                DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat,
                        AudioSystem.NOT_SPECIFIED);
                frameSize = audioInputStream.getFormat().getFrameSize();//获取每个帧对应的字节数
                WholeTime = audioInputStream.getFrameLength() / audioFormat.getFrameRate();  //获取歌曲总时长(帧数/帧速率=秒数)
                sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
                sourceDataLine.open(audioFormat); //打开输入流()
                sourceDataLine.start(); //打开管道

                //try 设置音量
                setVolumn(Volumn);

                //创建独立线程进行播放
                isStop = false;//mark

                if (playthread != null) {
                    playthread.stop();
                }

                playthread = new PlayThread(); //将原本为空对象的playThread实例化一个进程对象
                playthread.start(); //并开启进程(在暂停时,就直接取消掉进程)
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

关于上面使用的一些变量,如AudioInputStream , AudioFormat 等都属于JavaSound;而关于JavaSound,可以参考这篇文章
Java Sound初探 【以防它挂掉,多留一个链接 】
Java Sound初探(备份)
此处简述一下关键信息吧。上面代码中先获取音频文件流,到AudioInputStream流对象中,然后获取其格式AudioFormat ,后面有用。
然后获取音频的DataLIneInfo

DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat,AudioSystem.NOT_SPECIFIED);

再获取对应音频的DateLine

sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);

最后打开管道

sourceDataLine.open(audioFormat); //打开输入流()
sourceDataLine.start(); //打开管道

打开管道并不是开始播放音乐,只是为播放做了初步的准备,相当于是一个缓冲通道,打开后就已经缓冲好了。后面是字节读取的工作,是在线程里实现的。
线程从管道中读取字节流

 class PlayThread extends Thread {
            byte tempBuffer[] = new byte[20];

            public void run() {
                try {
                    int cnt;
                    // 读取数据到缓存数据
                    System.out.println("windx------" + Thread.currentThread().getId());
                    if (!hasStop) {  //如果不是按继续开始的且当前时间不为0
                        audioInputStream.skip(current_time);
                    }
                    while ((cnt = audioInputStream.read(tempBuffer, 0, tempBuffer.length)) != -1) {
                        current_time += cnt;

                        if (isStop) {  //如果当前进程的状态是暂停状态,就直接退出去,不进行数据写入(即播放)
                            System.out.println("进程" + Thread.currentThread().getId() + "跳出去了");
                            break;
                        }
                        if (cnt > 0) {
                            //写入缓存数据
                            sourceDataLine.write(tempBuffer, 0, cnt);
                        }
                        showTime(); //写入时就显示当前时间进度
                    }
                    //Block 等待临时数据被输出为空
                    sourceDataLine.drain();
                    sourceDataLine.close();
                    System.out.println(Thread.currentThread().getId() + "进程结束了");

                    temp = Time;  //在此处获取时间,才不会错过暂停后进程的运行

                } catch (Exception e) {
                    e.printStackTrace();
                    System.exit(0);
                }
            }
        }

使用while(true)不间断读取,实现音乐一直播放,但也注意判断暂停的标志,若已暂停,就立即跳出循环。详见下面的(3)音乐暂停

(2)音量调节

音量主要由floatVoiceControl 控制,该变量又可由sourceDataLine 按固定方法得到,见下:

//设置音量
        public void setVolumn(float volumn) {  //音量范围为 -80~6
            floatVoiceControl = (FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
//           System.out.println("设置前的音量" + floatVoiceControl.getValue());
            floatVoiceControl.setValue(volumn);
        }

音量的控制又涉及到JSlider滑动条,见下:

//添加音量进度条
volumeSlider = new JSlider(SwingConstants.VERTICAL, 0, 100, 50);
volumeSlider.addChangeListener(new ChangeListener() {
    @Override
    public void stateChanged(ChangeEvent e) {
        Volumn = (float) (0.86 * ((JSlider) e.getSource()).getValue() - 80);  //注意浮点数取值!!
        setVolumn(Volumn);
        //                    System.out.println("当前volumn的值:" + Volumn);
        //                    System.out.println("当前音量:" + ((JSlider) e.getSource()).getValue());
    }
});
volumeSlider.setBounds(860, 400, 10, 200);
add(volumeSlider);

注意:①此处由于我创建的是一个竖的进度条,所以在new JSlier 的时候第一个参数传入的是SwingConstants._VERTICAL _。②音量大小范围为 (-80,6)【包括端点】,浮点数,所以我做了进度条显示与音量的一个转换。

(3)音乐暂停

音乐暂停的基本原理:设置一个标志量(如我设置的isStop和hasStop),然后利用其来中断音乐播放线程,让线程提前结束,并记录已读取的字节数(我是用currentTime记录的)。
当音乐继续时,就重新开一个线程,让新的线程在从输入流AudioInputStream读取字节之前就跳过currentTime个字节。从而实现音乐在原来的地方继续播放。

4.简单图片轮播

说明:此处的轮播图并没有在JavaWeb中记录的那么精细。就是实现简单的定时图片切换而已。
实现方法有二,但本质其实都是利用线程实现。
方法一:使用Timer.schedule(TimerTask,delay,period)形式执行图片切换,其中TimerTask中需要重写run()方法,将要重复执行的代码放在里面。delay是从多长时间后开始执行TimerTask,period是再每隔多久执行一次。【注:①delay和period的单位都是毫秒;②每次schedule一次就相当于创建了一个线程】。 代码如下:

//添加显示图片
            JLabel image = new JLabel(new ImageIcon("D:\\Project\\ideaProject\\BlackHorseJavaWeb\\heima_jdbc\\src\\Schoolwork\\音乐播放器\\image\\b1.png"));
            image.setBounds(200, 10, 550, 340);
            add(image);

            TimerTask tt = new TimerTask() {
                int count = 2; //用来标记是第几张图

                @Override
                public void run() {
                    image.setIcon(new ImageIcon("D:\\Project\\ideaProject\\BlackHorseJavaWeb\\heima_jdbc\\src\\Schoolwork\\音乐播放器\\image\\b" + count + ".png"));
                    count += 1;
                    count = count > 5 ? 1 : count;
                }
            };

            Timer timer = new Timer();
            timer.schedule(tt, 0, 2300);  //每使用一次schedule函数,就相当于创建一个线程,参数为 task 和 delaytime,最后面的参数表示每过多久执行一次

方法二:自定义一个线程,让其定时睡眠,也就实现了间隔执行,代码如下:
package music;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.GridLayout;
import java.util.concurrent.TimeUnit;
import java.util.jar.Pack200;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class JavaMain {
	public static void main(String[] args) {
		new MyFrame();
	}
}

class MyFrame extends JFrame {
	// 构造函数
	public MyFrame() {
	
		// 创建一个窗体并初始化
		this.setTitle("多线程实例");
		this.setSize(800, 800);
		// 设置为绝对布局
		this.setLayout(null);
		// 初始化窗体布局
		initComponents();
		// 设置窗体可见
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setVisible(true);
	}

	// 普通成员函数
	public void initComponents() {
		// 创建标签
		JLabel labels = new JLabel();		
		this.add(labels);
		labels.setBounds(0, 0, 775, 291);

		// 将标签传递给线程1,使滚动的数字能在标签上显示
		MyThread t1 = new MyThread(labels);
		t1.start();
	}
}

// 第一种方法:继承Thread类
class MyThread extends Thread {
	// 定义成员变量
	private JLabel Image=new JLabel();
	private int nIndex = 0;
	// 构造函数
	public MyThread(JLabel labels) {
		this.Image = labels;
	}
	// 线程运行
	@Override
	public void run() {
		// 在这里可以写一个死循环
		while (true) {
			// 更换图片路径
			int n = ++nIndex % 5;
			String Picturepath = "D:\\我的\\各种课\\Java\\Java-program\\test\\src\\music\\images\\b"+(n+1)+".png";
			ImageIcon icon = Image.setIcon(Picturepath);
			Image = new JLabel(icon);
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}



二、所有源码

见资源部分

注意:上面代码中使用的图片路径是绝对路径((〃>皿<)我在Idea里面使用相对路径失败了)。素材可随便找啦~

三、待解决的问题

1.歌曲切换时对线程的处理

因为在切换上一首下一首或者选择其它歌曲时,要先将上一首未播放完的歌的进程给结束掉,否则实测会出现杂音干扰。我之前是使用interrupt()方法来中断线程的,但发现根本没用。
只有在暂停时起了作用,原因是设置的标志量起作用了,会让之前创建的所有线程一次性都结束
掉。但当我试图把暂停里(即我的control()方法)的实现单拿出来做一个函数实现线程终止时,却只能实现间隔播放成功。即播放一首,下一首就播放不了这样。
在尝试若干次均告失败后,我还是选择了使用过时的不安全的stop()来终止线程,结果立刻成功。
以后对线程学的更深入的时候我会再来记录新的办法。

2.列表的显示

列表显示有一个小问题,即我没办法把它放到JScrollPanel中,放进入后列表的内容无法正常显示到我的窗口JFrame中。因此也就无法一次载入过多的歌曲了,因为那样就显示不下去了。这个应该有办法解决,但是由于时间原因,我不想再花时间在上面了。(如果你知道,欢迎告诉我_

3.对更多音乐格式的支持

此项目暂时只能播放wav等未压缩格式音乐。而MP3这样的压缩文件暂时没办法用原生API解决。我搜索到一些方法是导入第三方jar包,可以实现解码,但是由于老师要求不能使用第三方jar包,因此也就没有实现,以后如果还有兴致,我可以会添加支持

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值