目录
对于qt的初学者来说,音乐播放器可以作为第一个练手的项目,语言基础:已经学完了面向对象部分的c++,软件基础:Linux操作系统,mplayer播放器,面向Linux的qt软件。
首先,我们要对于mplayer播放器有一定的了解。
1、mplayer简介
mplayer是一款开源多媒体播放器,以GNU通用公共许可证发布。此款软件可在各主流操作系统使用,例如Linux和其他类Unix系统、Windows及Mac OS X系统。支持播放多种主流的音视频格式。 mplayer有两种工作模式,一种普通模式,一种 slave模式。
(1)普通模式: mplayer -quiet 音视频文件名即为 mplayer -quiet x.mp3
mplayer -quiet x.mp3
显示如下情况则为成功
在普通模式运行下的其他指令,这些指令要在mplayer运行的前提下才能使用
left or right 向后/向前搜索10秒
up or down 向后/向前搜索1分钟
p or SPACE 暂停播放(按任意键继续)
q or ESC 停止播放并退出
注意: up down left right 是键盘的功能区的按钮且只能在普通模式下使用
(2)slave 模式
在普通模式上加上参数 -slave mplayer -quiet -slave 路径名 音视频文件名
在这种模式下可以从标准输入设备获取命令来控制播放,也可以在程序中发送指定来
控制播放。
mplayer -quite -slave /mnt/hgfs/share 1.mp3
//把路径和歌曲名字改为自己的
在该模式下也有一些指令来控制mplayer,与普通模式不一样的是在控制台输入指令进行控制的,常见指令如下:
注意:mplayer不能识别带空格的文件名,所以使用时请将本地文件改为不带空格的名字
loadfile newfilename //会结束正在播放的文件,播放新文件。
// lodafine lovestory.mp3
volume x 1 //设置音量 中间的x为音量的大小。
//volume 20 1
// volume 80 1
mute 1/0 //静音开关 , 1:静音;0:取消静音。
pause //暂停/取消暂停
//注意:在暂停的状态下,发送任何指令都会继续播放
quit 退出MPlayer。
get_time_length //返回值是播放文件的长度,以秒为单位。
//回复格式: ANS_LENGTH=273.00
//注意对于其他格式转码成MP3格式的时间会出现错误
get_percent_pos //返回播放进度的百分比(0--99)
//回复格式: ANS_PERCENT_POSITION=30
get_time_pos //返回当前播放位置的时间,单位是秒,采用浮点数
//回复格式: ANS_TIME_POSITION=118.1
seek value type 跳转到指定位置进行播放
type 为0时, value表示相对目前所在位置的秒数,可正可负
type 为1时, value表示百分比,定位到 value% 处播放
type 为2时, value表示绝对时间,如: value为50,表示定位到第50秒处播放
//其他指令
get_file_name //打印出当前文件名
get_meta_album //打印出当前文件的'专辑'的元数据
get_meta_artist //打印出当前文件的'艺术家'的元数据
get_meta_comment //打印出当前文件的'评论'的元数据
get_meta_genre //打印出当前文件的'流派'的元数据
get_meta_title //打印出当前文件的'标题'的元数据
get_meta_year //打印出当前文件的'年份'的元数据
2、简易的音乐播放器
了解mplayer后,我们就可以利用这些指令,制作属于我们的简易音乐播放器
(1)显示歌曲列表
此函数用来筛选文件夹中MP3或者其他格式音频的文件,这里用MP3文件展示
(i)加入头文件定义自己的歌曲文件路径(根据自己的情况来确定推荐使用相对路径这里我用绝对路径来展示),并且定义showMusciName()函数
#include<QString>
#include<QDir>
#include<QStringList>
public:
void showMusicName();
private:
#define DEFAULT_PATH "/mnt/hgfs/share/mus" //根据实际情况来定
(ii)在.c文件中实现showMusipaly()函数,这里有2种实现方法,一种时一个一个的加入到歌曲列表中,另外一个全部统一加入到歌曲列表中
//单个加入
musciplay::showMusicName()
{
ui->listWidget_name->clear();//清除原来的歌单
QDir dir(music_path);//实例化一个目录对象
QStringList x;
x <<"*.mp3";//筛选MP3格式文件
QStringList list=dir.entryList(x);
for(int i=0;i<list.size();i++){
ui->listWidget_name->addItem(list.at(i));
}
songSize = music_name_list.size();
}
//全部整体加入
musciplay::showMusicName()
{
ui->listWidget_name->clear();
QDir dir(music_path);
QStringList x;
x <<"*.mp3";
music_name_list=dir.entryList(x);
ui->listWidget_name->addItems(music_name_list);
}
(iii)在构造函数中调用showMusicName()函数,并且对路径music_path进行初始化
//musicplay.h文件
private:
QString music_path;//用来保存当前音乐文件所在路径
QStringList music_name_list;//把读取到的文件先保存在这个列表里面然后加入到ui界面上去
//musicplay.c文件
musciplay::musciplay(QWidget *parent) :
QWidget(parent),
ui(new Ui::musciplay)
{
ui->setupUi(this);
music_path=DEFAULT_PATH;
showMusicName();
}
(iiii)展示效果
(2)导入本地文件按钮
(i)在ui界面中加入button按钮,并且命名为导入,单机导入按钮转到槽,选择clicked()
注意:因为是转到槽函数,这里不需要对其进行额外的函数声明
(ii)加入头文件,并且完善该槽函数
#include<QDebug>//用来调试输出到终端上显示
#include<QFileDialog>
void musicplay::on_pushButton_clicked()
{
QString path=QFileDialog::getExistingDirectory();//弹出文件对话框,选择一个目录并返回目录的绝对路径,中途取消返回空字符串
if(!path.isEmpty())
{
music_path=path;
}
showMusicName();
}
(iii)效果展示
(3)播放与暂停按钮
(i)与导入一样在ui界面选择button并且命名为播放,右击转到槽,依旧选择clicked,这里就不做展示
(ii)定义一个play函数,在每次点击播放按钮时,来调用play函数进行播放,对于play函数,我们的想法是ui界面接受播放信息时,就会启动进程,向终端发送-slave格式的播放信息。
#include<QFileDialog>
#include<QProcess>
public:
void play();
private:
int index;//当前正在播放的音乐下标记
QProcess *process;
//我们在析构函数这统一结束进程
musciplay::~musciplay()
{
delete ui;
process->close();
}
void musciplay::play()
{
//file保存当前要播放的音乐的完整路径名
QString file=QString("%1/%2").arg(music_path).arg(music_name_list[index]);//%1代表路径,%2代表名字
QStringList arg;
arg<<"-quiet"<<"-slave"<<file;//利用file保存播放音乐的指令代码,然后发送到终端
process->setArguments(arg);
process->start();//启动进程
}
(iii) 对于槽函数on_pushButton_play_clicked()有2种情况进行分析,先判断进程是否在进行,如果在进行就暂停,如果没有在进行就继续播放,同时在播放时点击,播放就会变成暂停利用setText实现。反之,依然。
void musciplay::on_pushButton_play_clicked()
{
if(ui->pushButton_play->text()=="播放")
{
if(process->state()!=QProcess::Running)
{
play();
}//正在运行的情况
else
{
process->write("pause\n");
}
ui->pushButton_play->setText("暂停");
}
else
{
ui->pushButton_play->setText("播放");
process->write("pause\n");
}
}
(4)上一曲与下一曲的实现
上一曲与下一曲的实现方法有异曲同工之妙,这里我详细介绍下一曲的实现方法和步骤,仅展示上一曲的源码
(i)同前面一样,我们在点击下一曲按钮时,系统会向终端发出指令切换歌曲。所有我们槽函数也是选择clicked。
(ii)与上文一样,也是向终端发送切换歌曲的指令,然后改变播放按钮的文本
//下一曲
void musciplay::on_pushButton_next_clicked()
{
index++;
if(index==music_name_list.size())
{
index=0;
}//当歌曲到达最后一首时回到第一首
if(process->state()!=QProcess::Running)
{
play();
}
else
{
//正在播放中,点了下一曲
QString cmd =QString("loadfile %1/%2\n").arg(music_path).arg(music_name_list[index]);
process->write(cmd.toUtf8());
}
if(ui->pushButton_play->text()=="播放")
{
ui->pushButton_play->setText("暂停");
}
}
//下一曲,只改变了index的计算方法,与下一曲几乎一样
void musciplay::on_pushButton_previous_clicked()
{
if (index == 0)
{
index = music_name_list.size() - 1;
}
else
{
index--;
}
if (process->state()!= QProcess::Running)
{
play();
}
else// 正在播放中,加载上一曲
{
QString cmd = QString("loadfile %1/%2\n").arg(music_path).arg(music_name_list[index]);
process->write(cmd.toUtf8());
}
if(ui->pushButton_play->text()=="播放")
{
ui->pushButton_play->setText("暂停");
}
}
(5)曲毕,自动播放下一曲
(i) 先判断进程是否结束,来判断音乐是否播放完,播放完就调用play函数
musicplay.h文件
private slots:
void onFinished();
muscicpaly.cpp文件
musicplay::musicplay(QWidget *parent) :
QWidget(parent),
ui(new Ui::musicplay)
{
connect(process,SIGNAL(finished(int)),this,SLOT(onFinished()));
}
void musicplay::onFinished()
{
qDebug()<<"mplayer播放结束了";
index++;
if(index==music_name_list.size())
{
index=0;
}
else
{
play();
}
}
(6)双击歌单切换歌曲
在熟悉了上一曲,下一曲的播放原理后,双击切换歌曲的原理与之差不多,可以很好的进行类比。
(i)找到ui界面,点击歌单列表右击点到槽函数,选择doubleCliced
(ii)因为c++的语法,自定义的类里面会自带一个this指针,用this->index=index.row()//第一个this是我们自己定义的index来记录当前播放在第几曲了,第二个为槽函数的index,用index.row()来获取当前点击的第几行。
void musciplay::on_listWidget_name_doubleClicked(const QModelIndex &index)
{
this->index=index.row();//返回单击的行数
if(process->state()!= QProcess::Running) {
play();
ui->pushButton_play->setText("暂停");
} else
{
QString cmd = QString("loadfile %1/%2\n").arg(music_path).arg(music_name_list[this->index]);
process->write(cmd.toUtf8());
}
if(ui->pushButton_play->text()=="播放")
{
ui->pushButton_play->setText("暂停");
}
}
(7)音量调节
mplayer播放器自带一个volume指令可以用来调剂音量,我们连接slider就可以实现对于音量的就行调节。但是在选择槽函数时我们该做如何选择呢?
(i)先用label标签,来标识音量,如何选择slier转到槽,选择valueChange(int),因为音量是一个动态变化的,所有要实时监控音量的变化值
(ii)音量变化的核心思想也是把命令用字符串储存起来,如何利用write函数写到终端进行命令控制。
musicplay.cpp 文件
void musciplay::on_horizontalSlide_volume_valueChanged(int value)
{
QString cmd = QString("volume %1 1\n").arg(value);
process->write(cmd.toUtf8());
}
(iii)到此基本功能已经实现了,但是有一个新问题,我们的初始音量不是0,但是打开进程,初始音量显示0,那么我们该如何解决呢? 我们选择在ui界面显示上做功夫,对于音量的初始进行设定
#include<QTextStream>
//该文件可以对ui界面进行文字流的输入
#include<QScrollBar>
//该文件主要是对于滚动条进行调节
musicplay::musicplay(QWidget *parent) :
QWidget(parent),
ui(new Ui::musicplay)
{
ui->horizontalSlider_volunme->setValue(80);//开始音量
}
(8)进度条调节
经过对于音量调节的学习,相必大家先想到的就是实时监测进度条值的变化,那么仅仅如此吗。大家可以试一下,这里不做演示。
因为进度条要根据歌曲的播放进度是实时变化的,所有我们需要一个定时器,每过一段时间就向终端进行询问,“现在到哪了”。来监测音乐的播放进度条。
//插入头文件
musciplay.h 文件
#include<QTimer>
musciplay.h 文件
private:
QTimer *timer;
musicplay.cpp文件
musicplay::musicplay(QWidget *parent) :
QWidget(parent),
ui(new Ui::musicplay)
{
timer=new QTimer(this);
timer->setInterval(1);//设置间隔时间
timer->start(); //开启定时器
}
(i)首先我们需要获取歌曲的文件长度,查阅前文发现,用 get_time_length 指令来获取文件长度,回复格式: ANS_LENGTH=273.00。get_time_pos 指令来获取播放位置的时间,单位是秒,采用浮点数,回复格式: ANS_TIME_POSITION=118.1。因为slider是百分比的,所以我们采用get_percent_pos 指令获取播放进度的百分比(0--99)回复格式:ANS_PERCENT_POSITION=30 ,我们定义一个sumtime标签,每个歌词文件获取后利用ui文本写入,即可显示出来。并且在ui界面上添加slider控件
此外我们将定义一个ontimeput函数,每次计时器开始时都会发送指令给终端来获取我们需要的数据。利用timeout()信号选择器,可以方便的得到。
musciplay::musciplay(QWidget *parent) :
QWidget(parent),
ui(new Ui::musciplay)
{
connect(timer,SIGNAL(timeout()),this,SLOT(onTimeput()));
}
musciplay::onTimeput()
{
process->write("get_time_length\n");
process->write("get_percent_pos\n");
process->write("get_time_pos\n");
}
(ii) 定义一个onReadyRead()函数,来多次询问终端获取总时间和实时时间,每次指令获取我们需要去处不需要的文字部分,只保留自己需要的部分,并且在构造函数中进行连接,因为我们需要在接收完终端数据后开始调用函数,所以采用信号接受时采用readyread(),单我们在切歌曲时没有进行调用,所以我们直接在play()里面进行调用,因为在暂停状态下恢复播放,我们没有调用play()函数所以我们也要在上一曲,下一曲,双击切歌的第二个选项里面,同时因为在暂停时我们的计时器是关闭的,所以我们也需要打开计时器。
private slots:
void onReadyRead();
musciplay::musciplay(QWidget *parent) :
QWidget(parent),
ui(new Ui::musciplay)
{
connect(process,SIGNAL(readyRead()),this,SLOT(onReadyRead()));
}
void musciplay::onReadyRead()
{
while(1)
{
//只要mplayer回复数据给process就会执行该槽函数
QString s=process->readLine();
//qDebug()<<s;
if(s.isEmpty()) break;//读完跳出循环
if(s.contains("ANS_LENGTH="))//回复总时长
{
qDebug()<<s;
s.remove(0,11);//去除从0开始的连续11个字符
s.chop(4);//去除最后4个
s = s.trimmed(); //去除头尾空格
ui->label_sumtime->setText(s);
}
else if(s.contains("ANS_PERCENT_POSITION="))
{
s.remove(0,21);
ui->horizontalSlider->setValue(s.toInt());
}
else if(s.contains("ANS_TIME_POSITION="))
{
s.remove(0,18);
s.chop(1);
ui->label_nowtime->setText(s);
}
}
}
void musciplay::on_pushButton_next_clicked()
{
if(process->state()!=QProcess::Running)
{
play();
}
else
{
timer->start();
onTimeput();
}
}
void musciplay::on_pushButton_previous_clicked()
{
if (process->state()!= QProcess::Running)
{
play();
}
else// 正在播放中,加载上一曲
{
timer->start();
onTimeput();
}
}
void musciplay::on_listWidget_name_doubleClicked(const QModelIndex &index)
{
if(process->state()!= QProcess::Running)
{
play();
ui->pushButton_play->setText("暂停");
}
else
{
timer->start();
onTimeput();
}
}
(iii) 上文中我们基本实现了现在歌曲进度与歌曲总时间的显示,但是进度条的拖动还是没有实现,同时我们遇到一个问题那就是播放键失灵了,无法暂停与播放了,我们该如何做呢。因为定时器每隔一秒就会发送指令,我们的暂停指令刚开始就被覆盖了,那我们想,那就在点击暂停的时候停止计时器,这样是不是就可以解决了呢。停止后我们也需要及时的回复,这样我们的实时进度才会继续。
void musciplay::on_pushButton_play_clicked()
{
if(ui->pushButton_play->text()=="播放")
{
if(process->state()!=QProcess::Running)
{
play();
}
else
{
timer->start();
process->write("pause\n");
}
ui->pushButton_play->setText("暂停");
}
else
{
timer->stop();
ui->pushButton_play->setText("播放");
process->write("pause\n");
}
}
我们点击slider转到槽函数,选择Released()和pressed (),为什么要选择2个呢,因为我们需要在调节进度条时,把计时器关闭,松开后我们将恢复计时器。
void musciplay::on_horizontalSlider_sliderReleased()
{
timer->start();
int x=ui->horizontalSlider->value();
if(ui->pushButton_play->text()=="播放")
{
ui->pushButton_play->setText("暂停");
}
//seek命令
QString cmd=QString("seek %1 1\n").arg(x);
process->write(cmd.toUtf8());
onTimeput();
}
void musciplay::on_horizontalSlider_sliderPressed()
{
timer->stop();
}
(9)歌词显示与歌词滚动
歌词的显示我们可以仿照歌单一样建立一个列表,当歌词显示大于7的时候我们将进行实时滚动,另外一种方法时采用七条标签,不断的进行歌词的提取,显示,高亮,滚动。这里我将采用列表的形式进行演示。我们要实现找到与歌曲匹配的lrc文件,这里可以在百度上搜索,歌词文件名与歌曲名一致。
(i)歌词显示
新建一个ListWidget,我们命名为lrc
我们首先要提取歌词到列表上,所以我们要去除歌词前面的时间戳,去处前我们需要提取时间戳,用来在后面滚动歌词时用到。我们建立一个time_list的列表,用来保存。因为我们实时获取的歌曲进度时以秒为单位,所以我们将时间戳转换为秒。我在网上找到时间戳格式为[00:01.330],数组的1、2为小时位,4、5为分钟位,7、8、9为秒位。
#include<QFile>
private:
QList<double> time_list;
//在歌词显示之前我们需要先判断,是否能正常打开歌词文件,歌词路径采用歌曲一样的路径
void musciplay::onShowLyric()
{
//清空控件中的歌词内容
ui->listWidget_lyric->clear();
//清空时间列表
time_list.clear();
//每次显示歌词的时候都回到第一行进行显示
ui->listWidget_lyric->verticalScrollBar()->setSliderPosition(0);
//获取当前播放的歌曲的歌词路径名
QString lrc_path = music_path+ "/" + music_name_list[index];
// lrc_path.replace(".mp3",".lrc");
lrc_path.remove(lrc_path.length()-3,3);
lrc_path += "lrc";
//实例化一个文件对象
QFile file(lrc_path);
if(!file.exists())
{
//如果歌词文件不存在,直接返回,不显示歌词
qDebug() << lrc_path << " is not exists!";
return;
}
//打开文件
if(!file.open(QFile::ReadOnly))
{
//歌词文件打开失败
qDebug() << "open " << lrc_path << " error:" << file.errorString();
return;
}
//实例化一个文本流对象
QTextStream in(&file);
//设置编码格式
//in.setCodec("GBK");
//读取歌词
while (1)
{
if(in.atEnd()) break;
//读一行歌词
QString line = in.readLine();
//提取歌词前面的时间
float time = (line.mid(1,2).toFloat()*60)+ line.mid(4,4).toFloat();
qDebug()<<time;
//将提取出来的时间保存到容器中去
time_list.append(time);
//去除歌词前面的时间
if(line.at(0) == '[' and line.at(10) == ']')
{
//这行歌词前面有时间,删除时间
line.remove(0,11);
}
//歌词居中显示
QListWidgetItem *item = new QListWidgetItem(line);
item->setTextAlignment(Qt::AlignCenter);
//将歌词显示到控件上去
ui->listWidget_lyric->addItem(item);
}
//关闭歌词文件
file.close();
}
(ii)歌词滚动
歌词滚动只需要简单的大于7,setSliderPosition(lrc_index-7)如果条件为真,这行代码会设置滚动条的位置。它将滚动条移动到 lrc_index - 7
的位置。这样做的效果是将当前的歌词项调整为可见,确保在 lrc_index
为8时,滚动条位置被设置为1,这样第8项就能在视图中可见,因为前7项已被滚动出视野。
if(lrc_index > 7)
{
ui->listWidget_lyric->verticalScrollBar()->setSliderPosition(lrc_index-7);
}
观察常用音乐软件可以发现,我们还需要将当前歌词红色显示,我们需要匹配时间戳与当前时间,当二者匹配时,我们将当前行数标红,上一个红色的我们标为黑色,就可以实现了。
void musciplay::inStepLrc(float time)
{
//去歌词的时间列表中匹配某个时间点的行号
int lrc_index = time_list.indexOf(time);
//如果找到这个时间点了,就返回这个时间点的行号
//如果没找到,就返回-1
if(lrc_index == -1)
{
//没有匹配到这个时间点的歌词
return;
}
//将前一行的歌词颜色恢复为黑色
if(lrc_index >0)
{
ui->listWidget_lyric->item(lrc_index-1)->setTextColor(QColor(0,0,0));
}
//将匹配到的这行歌词高亮显示(显示红色)
ui->listWidget_lyric->item(lrc_index)->setTextColor(QColor(0xFF,0,0));
QListWidgetItem *currentItem = ui->listWidget_lyric->item(lrc_index);
currentItem->setTextColor(QColor(0xFF,0,0));
//居中显示当前歌词
ui->listWidget_lyric->scrollToItem(currentItem, QAbstractItemView::PositionAtCenter);
//歌词自动滚屏
if(lrc_index > 7)
{
ui->listWidget_lyric->verticalScrollBar()->setSliderPosition(lrc_index-7);
}
}
(iii)合理的放置函数
onShowLyric()与上文的放置发送指令的函数一样,在play,上一曲,下一曲,双击播放中。而歌词滚动函数只要放在获取时间的函数之中即可时间。
void musciplay::onReadyRead()
{
while(1)
{
else if(s.contains("ANS_TIME_POSITION="))
{
inStepLrc(s.toFloat());
}
}
}
void musciplay::play()
{
onShowLyric();
}
void musciplay::on_pushButton_next_clicked()
{
else
{
onShowLyric();
}
}
void musciplay::on_pushButton_previous_clicked()
{
i
else// 正在播放中,加载上一曲
{
onShowLyric();
}
}
void musciplay::on_listWidget_name_doubleClicked(const QModelIndex &index)
{
else
{
onShowLyric();
}
}
3、最终演示视频
完整演示视频
好了至此已经完整的完成了基于linux的音乐播放器。可能会有一些小bug