说明
此项目是基于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包,因此也就没有实现,以后如果还有兴致,我可以会添加支持