前文
本次课设的要求是通过GEC6818开发板,使用C++编程语言以及Qt工具在开发板上运行多媒体播放器。
具体要求:
1. 实现上一曲、下一曲、播放、暂停,通过触摸屏控制播放器播放音频。
2. 实现音量调节功能,实时调节音量。
3. 实现播放模式选择功能,可以实现列表循环和随机播放。
4. 实时显示播放时间以及播放进度,调节进度条可以调节播放进度和时间。
5. 实现歌词实时显示,当前播放的歌词高亮显示。
6. 实现视频播放功能。
7. 界面友好,操作流畅,功能明确。
配置环境
这次是需要在ARM开发板上运行,所以主要是配好交叉编译链和ARM开发板上的QT环境,Ubuntu上面也需要QT运行的环境,方便我们观察代码的功能实现情况,我用的都是QT5.9的环境,并且环境都基本上已经配好,无需自己配,所以这一段省略。
逻辑
一开始我用的是QT中QtMultimedia来实现播放功能,但是后面发现我用的ARM开发板上不支持这个模块,所以后来改用mplayer,通过调用进程的方式实现播放功能。
UI界面
首先是UI界面的设计,我设计的比较简单,就选择歌曲列表、播放模式、上一首、播放/暂停、下一首、音量这六个按键,还有进度条、显示歌曲总时间和当前时间的QLabel标签和两个分别显示歌曲列表和歌词的QListWidget,以及一个调节音量的进度条。
弄了一点布局和背景,大概长这样
布局在这里设置
背景则是点击需要改变背景的控件,在鼠标右键
接着选择其中的改变样式表
可以敲代码也可以点击上方自行选择。
我的按钮图标是资源文件加进去的,资源文件在这
直接新建一个C++的资源文件夹就行了,把需要的图标放进去就好。
代码
代码中有注释,因此不再过多解释
初始化
我的代码其实有点乱,槽函数有自己写的也有直接ui设计界面转的,所以其实在代码上是不全的,这是我的构造和析构函数的代码
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget), // 初始化UI界面
mplayer(new QProcess(this)), // 初始化用于媒体播放的QProcess
currentIndex(0), // 初始化播放列表中当前的索引
isPaused(false), // 初始暂停状态为false
currentPosition(0), // 初始化当前播放位置
totalDuration(0), // 初始化媒体总时长
playMode(Sequential) // 设置初始播放模式为顺序播放
{
ui->setupUi(this); // 设置用户界面
playbackTimer.setInterval(100); // 设置播放定时器的间隔
connect(&playbackTimer, &QTimer::timeout, this, &Widget::updateCurrentPosition); // 将定时器连接到更新当前位置的槽函数
// 将QProcess的各个信号连接到Widget类中对应的槽函数
connect(mplayer, &QProcess::started, this, &Widget::onStarted);
connect(mplayer, QOverload<QProcess::ProcessError>::of(&QProcess::errorOccurred), this, &Widget::onError);
connect(mplayer, &QProcess::readyReadStandardOutput, this, &Widget::onReadyReadStandardOutput);
connect(mplayer, QOverload<int>::of(&QProcess::finished), this, &Widget::onFinished);
connect(ui->playCourseSlider, &QSlider::sliderReleased, this, &Widget::on_playCourseSlider_sliderReleased);
connect(ui->playCourseSlider, &QSlider::sliderMoved, this, &Widget::on_playCourseSlider_sliderMoved);
connect(ui->playCourseSlider, &QSlider::sliderPressed, this, &Widget::on_playCourseSlider_sliderPressed);
playbackTimer.start(); // 开启播放定时器
loadPlaylist(); // 加载播放列表
ui->playerVolumeSlider->setVisible(false); // 将播放器音量滑块初始设为不可见
ui->btn_Player->setIcon(QIcon(":/ICON/start.png")); // 设置播放按钮的图标
lyricTimer.setInterval(1000); // 设置歌词定时器的间隔
connect(&lyricTimer, &QTimer::timeout, this, &Widget::syncLyrics); // 将定时器连接到同步歌词的槽函数
connect(ui->playerVolumeSlider, &QSlider::valueChanged, this, &Widget::on_playerVolumeSlider_valueChanged); // 将音量滑块数值变化连接到相应的槽函数
}
Widget::~Widget()
{
delete ui;
}
这段的主要作用就是做一些初始化以及槽函数的连接。
因为要实现歌词动态实时显示,所以需要定时器。
播放功能
播放功能主要是播放和暂停功能、上一首和下一首的切换功能以及双击歌曲列表实现播放的功能,这几块我认为是一体的,所以放在了一块
void Widget::on_btn_Player_clicked()
{
if (mplayer->state() == QProcess::Running) {
if (!isPaused) {
mplayer->write("pause\n");
playbackTimer.stop();
lyricTimer.stop(); // 停止歌词同步
if (!mplayer->waitForBytesWritten()) {
return;
}
isPaused = true;
ui->btn_Player->setIcon(QIcon(":/ICON/start.png")); // 设置为播放图标
} else {
mplayer->write("pause\n");
if (!mplayer->waitForBytesWritten()) {
return;
}
playbackTimer.start();
lyricTimer.start(); // 恢复歌词同步
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png")); // 设置为暂停图标
}
}
else {
// 播放器不在运行状态,播放歌曲
playSong();
playbackTimer.start();
lyricTimer.start(); // 启动歌词同步
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png")); // 设置为暂停图标
}
}
void Widget::on_btn_LeftMove_clicked()
{
if (playlist.isEmpty())
return;
// 先将状态设置为播放状态
if (isPaused) {
isPaused = false;
playbackTimer.start();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
currentIndex = (currentIndex - 1 + playlist.size()) % playlist.size();
playSong();
}
void Widget::on_btn_RightMove_clicked()
{
if (playlist.isEmpty())
return;
// 先将状态设置为播放状态
if (isPaused) {
isPaused = false;
playbackTimer.start();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
currentIndex = (currentIndex + 1) % playlist.size();
playSong();
}
void Widget::on_listWidget_itemDoubleClicked(QListWidgetItem *item)
{
// 先将状态设置为播放状态
if (isPaused) {
isPaused = false;
playbackTimer.start();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
currentIndex = ui->listWidget->row(item);
playSong();
}
我把播放按钮和暂停按钮合二为一了,播放时点击则会暂停,暂停时点击则会播放,因此图标也需要修改。
上一首和下一首的按键功能主要是改变下标,其他的和播放功能相同。
播放按钮因为集合了暂停的功能,所以显得代码有点多,其实就两个逻辑判断,一个是进程在没在运行,就是说如果进程没在运行,意味着此时没有歌曲播放,所以点击播放按钮会开始播放歌曲。
另外一个则是如果歌曲已经在播放了,此时做的判断则是暂停还是播放,并完成对应的操作。
接下来是整个播放器的核心代码
void Widget::playSong()
{
// 停止当前正在播放的歌曲
if (mplayer->state() == QProcess::Running) {
mplayer->kill();
mplayer->waitForFinished();
}
// 清除当前歌词
lyricEntries.clear();
ui->lyricWidget->clear();
ui->playCourseSlider->setValue(0);
ui->labCurTime->setText("00:00");
ui->labAllTime->setText("00:00");
// 检查 currentIndex 是否有效
if (currentIndex < 0 || currentIndex >= playlist.size()) {
return;
}
if (playMode == Random) {
// 随机选择一首歌曲
int randomIndex = qrand() % playlist.size();
currentIndex = randomIndex;
}
QString song = playlist.at(currentIndex);
// 如果是视频文件,加载视频
if (song.endsWith(".mp4", Qt::CaseInsensitive)) {
QStringList args;
args << "-slave" << "-quiet" << "-idle" << "-wid" << QString::number(ui->lyricWidget->winId())
<< "-geometry"<<"280:10"<<"-zoom"<<"-x"<<"461"<<"-y"<< "300" << song;
mplayer->start("mplayer", args);
qDebug() << "正在播放视频,索引:" << currentIndex;
// 停止歌词同步
lyricTimer.stop();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
isPaused = false;
// 高亮显示当前播放的视频
highlightCurrentSong();
return;
}
// 启动 MPlayer 播放音乐
QStringList args;
args << "-slave" << "-quiet" << "-idle" << song;
mplayer->start("mplayer", args);
qDebug() << "正在播放歌曲,索引:" << currentIndex;
// 加载歌词
QString lyricPath = "/home/source/Lyric/" + QFileInfo(song).baseName() + ".lrc";
qDebug() << "加载歌词:" << lyricPath;
loadLyrics(lyricPath);
// 设置按钮图标为暂停状态
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
// 高亮显示当前播放的歌曲
highlightCurrentSong();
// 启动歌词同步
lyricTimer.start();
// 更新播放器界面信息
updatePlayerUI();
}
这段的主要作用就是实现播放功能,具体操作都在这个函数里面实现,其他功能需要播放歌曲都是调用这个函数。
歌名和歌词显示
歌名显示我写了两段功能相同的函数,纯属多余,其实可以用一个函数实现。
功能都是扫描路径,把歌名加进UI界面显示。
void Widget::loadPlaylist()
{
// 清空原歌单列表
ui->listWidget->clear();
playlist.clear();
// 扫描指定路径下的音乐文件
QDir directory("/home/source/Music");
QStringList musicFiles = directory.entryList(QStringList() << "*.mp3" << "*.wav" << "*.mp4", QDir::Files);
foreach (QString musicFile, musicFiles) {
playlist.append(directory.absoluteFilePath(musicFile));
ui->listWidget->addItem(musicFile); // 在 listWidget 中显示歌曲名称
}
// 重置当前播放索引
currentIndex = 0;
}
void Widget::loadPlaylist(const QString &folderPath)
{
// 清空原歌单列表
ui->listWidget->clear();
playlist.clear();
playbackTimer.stop();
lyricTimer.stop(); // 停止歌词同步
mplayer->write("pause\n");
// 扫描指定路径下的音乐文件
QStringList musicFiles = scanMusicFiles(folderPath);
foreach (const QString &musicFile, musicFiles) {
playlist.append(musicFile);
ui->listWidget->addItem(musicFile); // 在 listWidget 中显示歌曲名称
}
// 重置当前播放索引
currentIndex = 0;
}
歌词显示则由这四段代码完成
逻辑是先把歌词全部显示出来,然后根据时间戳进行对应,实现动态高亮显示
void Widget::highlightCurrentSong()
{
for (int i = 0; i < ui->listWidget->count(); i++) {
QListWidgetItem* item = ui->listWidget->item(i);
if (i == currentIndex) {
item->setBackgroundColor(Qt::yellow);
} else {
item->setBackgroundColor(Qt::white);
}
}
}
void Widget::updateLyrics(QProcess::ProcessState newState)
{
if (newState == QProcess::NotRunning) {
// 播放器停止后清除歌词
ui->lyricWidget->clear();
// 清空歌词列表
lyricEntries.clear();
// 断开更新歌词的连接
disconnect(mplayer, &QProcess::stateChanged, this, &Widget::updateLyrics);
}
}
void Widget::syncLyrics()
{
int currentTime = static_cast<int>(currentPosition); // 获取当前播放时间
// 遍历歌词列表并显示歌词
for (int i = 0; i < lyricEntries.size(); ++i) {
int currentLyricTime = lyricEntries[i].timestamp.minute() * 60 + lyricEntries[i].timestamp.second();
int nextLyricTime = (i + 1 < lyricEntries.size()) ?
lyricEntries[i + 1].timestamp.minute() * 60 + lyricEntries[i + 1].timestamp.second() :
totalDuration;
// 添加歌词项到歌词列表
QListWidgetItem *item = new QListWidgetItem(lyricEntries[i].text);
ui->lyricWidget->addItem(item);
// 如果当前时间在当前歌词时间范围内,则高亮显示该歌词
if (currentTime >= currentLyricTime && currentTime < nextLyricTime) {
QFont font = item->font();
font.setBold(true);
item->setFont(font);
ui->lyricWidget->setCurrentRow(i); // 滚动显示当前歌词
}
}
}
void Widget::displayLyrics()
{
// 清空歌词列表
ui->lyricWidget->clear();
// 显示歌词
foreach(const LyricEntry &entry, lyricEntries) {
QListWidgetItem *item = new QListWidgetItem(entry.text);
ui->lyricWidget->addItem(item);
}
}
void Widget::loadLyrics(const QString &lyricPath)
{
QFile file(lyricPath); // 创建一个 QFile 对象,用于操作歌词文件
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
// 如果无法打开歌词文件,则输出错误信息并返回
qDebug() << "Failed to open lyric file";
return;
}
lyricEntries.clear(); // 清空现有的歌词条目
ui->lyricWidget->clear(); // 清空界面上的歌词显示
QTextStream in(&file); // 创建一个 QTextStream 对象,用于读取文件内容
while (!in.atEnd()) { // 循环读取文件,直到文件末尾
QString line = in.readLine(); // 读取一行歌词
QRegExp rx("\\[(\\d{2}):(\\d{2})\\.(\\d{2})\\](.*)"); // 定义正则表达式,用于解析歌词的时间戳和内容
if (rx.indexIn(line) != -1) { // 如果正则表达式匹配成功
int min = rx.cap(1).toInt(); // 提取分钟部分
int sec = rx.cap(2).toInt(); // 提取秒钟部分
int msec = rx.cap(3).toInt() * 10; // 提取毫秒部分,并转换为实际时间
QTime timestamp(0, min, sec, msec); // 创建一个 QTime 对象表示时间戳
QString text = rx.cap(4).trimmed(); // 提取并修剪歌词文本
// 将时间戳和歌词文本作为一个条目添加到歌词列表中
lyricEntries.append({ timestamp, text });
}
}
file.close(); // 关闭歌词文件
displayLyrics(); // 显示歌词
}
进度条
void Widget::on_playCourseSlider_sliderMoved(int position)
{
// 计算分钟和秒钟部分
int m = position / 60;
int s = position % 60;
// 在界面上更新当前播放时间显示
ui->labCurTime->setText(QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0')));
// 更新歌词同步
int currentTime = position;
for (int i = 0; i < lyricEntries.size(); ++i) {
// 计算当前歌词时间范围
int currentLyricTime = lyricEntries[i].timestamp.minute() * 60 + lyricEntries[i].timestamp.second();
int nextLyricTime = (i + 1 < lyricEntries.size()) ?
lyricEntries[i + 1].timestamp.minute() * 60 + lyricEntries[i + 1].timestamp.second() :
totalDuration;
if (currentTime >= currentLyricTime && currentTime < nextLyricTime) {
// 如果当前时间在当前歌词时间范围内,则设置该歌词为加粗显示,并将其滚动到视图中央
QFont font = ui->lyricWidget->item(i)->font();
font.setBold(true);
ui->lyricWidget->item(i)->setFont(font);
ui->lyricWidget->setCurrentRow(i);
} else {
// 如果不是当前歌词范围内的时间,则取消加粗显示
QFont font = ui->lyricWidget->item(i)->font();
font.setBold(false);
ui->lyricWidget->item(i)->setFont(font);
}
}
}
void Widget::on_playCourseSlider_sliderReleased()
{
playbackTimer.start();
// 获取滑动条的值,即歌曲的时间位置
int position = ui->playCourseSlider->value();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
// 发送设置歌曲播放时间的命令给播放器进程
QString setPositionCmd = QString("seek %1 2\n").arg(position);
mplayer->write(setPositionCmd.toUtf8());
// 更新当前播放位置
currentPosition = position;
// 同步歌词显示
syncLyrics();
}
void Widget::on_playCourseSlider_sliderPressed()
{
playbackTimer.stop();
}
void Widget::on_btn_model_clicked()
{
// 切换播放模式
if (playMode == Sequential) {
playMode = Random;
ui->btn_model->setIcon(QIcon(":/ICON/Random.png")); // 设置随机播放图标
} else {
playMode = Sequential;
ui->btn_model->setIcon(QIcon(":/ICON/Loop.png")); // 设置顺序播放图标
}
}
进度条功能主要也和定时器有关,获取当前时间和总时间然后进行显示以及进度条的调整。这里之前有个bug,就是拖动进度条会抖动,很难拖动,还会跳回去。后面发现是拖动和定时器冲突了,两个信号都想改变进度条的值,所以后来在拖动进度条的时候开关一下定时器就好了。
音量
void Widget::on_btn_Volume_clicked()
{
// 显示或隐藏音量滑块
ui->playerVolumeSlider->setVisible(!ui->playerVolumeSlider->isVisible());
}
void Widget::on_playerVolumeSlider_valueChanged(int value)
{
// 调节音量(如果播放器正在运行)
if (mplayer->state() == QProcess::Running) {
QString volCmd = QString("volume %1 1\n").arg(value);
mplayer->write(volCmd.toUtf8());
}
}
这个和拖动进度条的逻辑其实差不多,只是不需要和定时器做对抗那么复杂。
选择歌曲列表
QStringList Widget::scanMusicFiles(const QString &folderPath)
{
QDir directory(folderPath);
QStringList supportedFormats = {"*.mp3", "*.wav", "*.mp4"};
return directory.entryList(supportedFormats, QDir::Files);
}
void Widget::loadPlaylist(const QString &folderPath)
{
// 清空原歌单列表
ui->listWidget->clear();
playlist.clear();
playbackTimer.stop();
lyricTimer.stop(); // 停止歌词同步
mplayer->write("pause\n");
// 扫描指定路径下的音乐文件
QStringList musicFiles = scanMusicFiles(folderPath);
foreach (const QString &musicFile, musicFiles) {
playlist.append(musicFile);
ui->listWidget->addItem(musicFile); // 在 listWidget 中显示歌曲名称
}
// 重置当前播放索引
currentIndex = 0;
}
这个也很简单
全部代码
还有些代码没有展现出来,现在一并展示。我的工程也已经打包到Github上面,可以下载查看效果https://github.com/THUANUAA/MusicPlayer
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QProcess>
#include <QStringList>
#include <QDir>
#include <QListWidgetItem>
#include <QStringList>
#include <QTimer>
#include <QTime>
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
// 自定义结构体来存储歌词的时间戳和文本
struct LyricEntry {
QTime timestamp;
QString text;
};
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_btn_Res_clicked();
void on_btn_model_clicked();
void on_btn_LeftMove_clicked();
void on_btn_Player_clicked();
void on_btn_RightMove_clicked();
void on_btn_Volume_clicked();
void on_playerVolumeSlider_valueChanged(int value);
void on_listWidget_itemDoubleClicked(QListWidgetItem *item); // 添加这一行
void loadPlaylist(const QString &folderPath);
void loadLyrics(const QString &lyricPath);
QStringList scanMusicFiles(const QString &folderPath);
void syncLyrics();
void displayLyrics();
void updateLyrics(QProcess::ProcessState newState);
void updateCurrentPosition(); // 添加这一行
void highlightCurrentSong();
void onError(QProcess::ProcessError error);
void onStarted();
void onReadyReadStandardOutput();
void onFinished(int exitCode);
void on_playCourseSlider_sliderReleased();
void on_playCourseSlider_sliderPressed();
void on_playCourseSlider_sliderMoved(int position);
void updatePlayerUI();
void updateSongInfo();
private:
void loadPlaylist();
void playSong();
Ui::Widget *ui;
QProcess *mplayer;
QStringList playlist;
QString currentSong;
QTimer playbackTimer; // 声明 playbackTimer
int currentIndex;
bool isPaused;
QStringList lyricPaths;
int currentPosition; // 存储当前播放的位置(单位:秒)
int totalDuration; // 存储当前歌曲的总时长(单位:秒)
QTimer lyricTimer; // 声明 lyricTimer 对象
QList<LyricEntry> lyricEntries; // 声明lyricEntries
QMap<QString, QString> lyricFilePaths; // 存储歌曲文件名和对应的歌词文件路径
private:
enum PlayMode { Sequential, Random };
PlayMode playMode;
};
#endif // WIDGET_H
main.cpp
#include "widget.h"
#include <QApplication>
#include <QTextCodec>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置全局文本编码器为UTF-8
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
Widget w;
w.show();
return a.exec();
}
widget.cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget), // 初始化UI界面
mplayer(new QProcess(this)), // 初始化用于媒体播放的QProcess
currentIndex(0), // 初始化播放列表中当前的索引
isPaused(false), // 初始暂停状态为false
currentPosition(0), // 初始化当前播放位置
totalDuration(0), // 初始化媒体总时长
playMode(Sequential) // 设置初始播放模式为顺序播放
{
ui->setupUi(this); // 设置用户界面
playbackTimer.setInterval(100); // 设置播放定时器的间隔
connect(&playbackTimer, &QTimer::timeout, this, &Widget::updateCurrentPosition); // 将定时器连接到更新当前位置的槽函数
// 将QProcess的各个信号连接到Widget类中对应的槽函数
connect(mplayer, &QProcess::started, this, &Widget::onStarted);
connect(mplayer, QOverload<QProcess::ProcessError>::of(&QProcess::errorOccurred), this, &Widget::onError);
connect(mplayer, &QProcess::readyReadStandardOutput, this, &Widget::onReadyReadStandardOutput);
connect(mplayer, QOverload<int>::of(&QProcess::finished), this, &Widget::onFinished);
connect(ui->playCourseSlider, &QSlider::sliderReleased, this, &Widget::on_playCourseSlider_sliderReleased);
connect(ui->playCourseSlider, &QSlider::sliderMoved, this, &Widget::on_playCourseSlider_sliderMoved);
connect(ui->playCourseSlider, &QSlider::sliderPressed, this, &Widget::on_playCourseSlider_sliderPressed);
playbackTimer.start(); // 开启播放定时器
loadPlaylist(); // 加载播放列表
ui->playerVolumeSlider->setVisible(false); // 将播放器音量滑块初始设为不可见
ui->btn_Player->setIcon(QIcon(":/ICON/start.png")); // 设置播放按钮的图标
lyricTimer.setInterval(1000); // 设置歌词定时器的间隔
connect(&lyricTimer, &QTimer::timeout, this, &Widget::syncLyrics); // 将定时器连接到同步歌词的槽函数
connect(ui->playerVolumeSlider, &QSlider::valueChanged, this, &Widget::on_playerVolumeSlider_valueChanged); // 将音量滑块数值变化连接到相应的槽函数
}
Widget::~Widget()
{
delete ui;
}
void Widget::playSong()
{
// 停止当前正在播放的歌曲
if (mplayer->state() == QProcess::Running) {
mplayer->kill();
mplayer->waitForFinished();
}
// 清除当前歌词
lyricEntries.clear();
ui->lyricWidget->clear();
ui->playCourseSlider->setValue(0);
ui->labCurTime->setText("00:00");
ui->labAllTime->setText("00:00");
// 检查 currentIndex 是否有效
if (currentIndex < 0 || currentIndex >= playlist.size()) {
return;
}
if (playMode == Random) {
// 随机选择一首歌曲
int randomIndex = qrand() % playlist.size();
currentIndex = randomIndex;
}
QString song = playlist.at(currentIndex);
// 如果是视频文件,加载视频
if (song.endsWith(".mp4", Qt::CaseInsensitive)) {
QStringList args;
args << "-slave" << "-quiet" << "-idle" << "-wid" << QString::number(ui->lyricWidget->winId())
<< "-geometry"<<"280:10"<<"-zoom"<<"-x"<<"461"<<"-y"<< "300" << song;
mplayer->start("mplayer", args);
qDebug() << "正在播放视频,索引:" << currentIndex;
// 停止歌词同步
lyricTimer.stop();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
isPaused = false;
// 高亮显示当前播放的视频
highlightCurrentSong();
return;
}
// 启动 MPlayer 播放音乐
QStringList args;
args << "-slave" << "-quiet" << "-idle" << song;
mplayer->start("mplayer", args);
qDebug() << "正在播放歌曲,索引:" << currentIndex;
// 加载歌词
QString lyricPath = "/home/source/Lyric/" + QFileInfo(song).baseName() + ".lrc";
qDebug() << "加载歌词:" << lyricPath;
loadLyrics(lyricPath);
// 设置按钮图标为暂停状态
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
// 高亮显示当前播放的歌曲
highlightCurrentSong();
// 启动歌词同步
lyricTimer.start();
// 更新播放器界面信息
updatePlayerUI();
}
void Widget::updatePlayerUI()
{
// 更新播放器界面信息,如当前播放时间、总时长等
ui->playCourseSlider->setValue(static_cast<int>(currentPosition)); // 更新进度条位置
int m = static_cast<int>(currentPosition) / 60;
int s = static_cast<int>(currentPosition) % 60;
ui->labCurTime->setText(QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0'))); // 更新当前时间显示
ui->playCourseSlider->setMaximum(static_cast<int>(totalDuration)); // 更新进度条最大值
m = static_cast<int>(totalDuration) / 60;
s = static_cast<int>(totalDuration) % 60;
ui->labAllTime->setText(QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0'))); // 更新总时长显示
}
void Widget::highlightCurrentSong()
{
for (int i = 0; i < ui->listWidget->count(); i++) {
QListWidgetItem* item = ui->listWidget->item(i);
if (i == currentIndex) {
item->setBackgroundColor(Qt::yellow);
} else {
item->setBackgroundColor(Qt::white);
}
}
}
void Widget::updateLyrics(QProcess::ProcessState newState)
{
if (newState == QProcess::NotRunning) {
// 播放器停止后清除歌词
ui->lyricWidget->clear();
// 清空歌词列表
lyricEntries.clear();
// 断开更新歌词的连接
disconnect(mplayer, &QProcess::stateChanged, this, &Widget::updateLyrics);
}
}
void Widget::syncLyrics()
{
int currentTime = static_cast<int>(currentPosition); // 获取当前播放时间
// 遍历歌词列表并显示歌词
for (int i = 0; i < lyricEntries.size(); ++i) {
int currentLyricTime = lyricEntries[i].timestamp.minute() * 60 + lyricEntries[i].timestamp.second();
int nextLyricTime = (i + 1 < lyricEntries.size()) ?
lyricEntries[i + 1].timestamp.minute() * 60 + lyricEntries[i + 1].timestamp.second() :
totalDuration;
// 添加歌词项到歌词列表
QListWidgetItem *item = new QListWidgetItem(lyricEntries[i].text);
ui->lyricWidget->addItem(item);
// 如果当前时间在当前歌词时间范围内,则高亮显示该歌词
if (currentTime >= currentLyricTime && currentTime < nextLyricTime) {
QFont font = item->font();
font.setBold(true);
item->setFont(font);
ui->lyricWidget->setCurrentRow(i); // 滚动显示当前歌词
}
}
}
void Widget::displayLyrics()
{
// 清空歌词列表
ui->lyricWidget->clear();
// 显示歌词
foreach(const LyricEntry &entry, lyricEntries) {
QListWidgetItem *item = new QListWidgetItem(entry.text);
ui->lyricWidget->addItem(item);
}
}
void Widget::on_btn_Res_clicked()
{
// 选择音乐文件夹
QString folderPath = QFileDialog::getExistingDirectory(this, tr("选择音乐文件夹"), QDir::homePath());
if (!folderPath.isEmpty()) {
// 加载选择的音乐文件夹中的歌曲
loadPlaylist(folderPath);
}
}
void Widget::loadPlaylist(const QString &folderPath)
{
// 清空原歌单列表
ui->listWidget->clear();
playlist.clear();
playbackTimer.stop();
lyricTimer.stop(); // 停止歌词同步
mplayer->write("pause\n");
// 扫描指定路径下的音乐文件
QStringList musicFiles = scanMusicFiles(folderPath);
foreach (const QString &musicFile, musicFiles) {
playlist.append(musicFile);
ui->listWidget->addItem(musicFile); // 在 listWidget 中显示歌曲名称
}
// 重置当前播放索引
currentIndex = 0;
}
QStringList Widget::scanMusicFiles(const QString &folderPath)
{
QDir directory(folderPath);
QStringList supportedFormats = {"*.mp3", "*.wav", "*.mp4"};
return directory.entryList(supportedFormats, QDir::Files);
}
void Widget::on_btn_Player_clicked()
{
if (mplayer->state() == QProcess::Running) {
if (!isPaused) {
mplayer->write("pause\n");
playbackTimer.stop();
lyricTimer.stop(); // 停止歌词同步
if (!mplayer->waitForBytesWritten()) {
return;
}
isPaused = true;
ui->btn_Player->setIcon(QIcon(":/ICON/start.png")); // 设置为播放图标
} else {
mplayer->write("pause\n");
if (!mplayer->waitForBytesWritten()) {
return;
}
playbackTimer.start();
lyricTimer.start(); // 恢复歌词同步
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png")); // 设置为暂停图标
}
}
else {
// 播放器不在运行状态,播放歌曲
playSong();
playbackTimer.start();
lyricTimer.start(); // 启动歌词同步
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png")); // 设置为暂停图标
}
}
void Widget::on_btn_LeftMove_clicked()
{
if (playlist.isEmpty())
return;
// 先将状态设置为播放状态
if (isPaused) {
isPaused = false;
playbackTimer.start();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
currentIndex = (currentIndex - 1 + playlist.size()) % playlist.size();
playSong();
}
void Widget::on_btn_RightMove_clicked()
{
if (playlist.isEmpty())
return;
// 先将状态设置为播放状态
if (isPaused) {
isPaused = false;
playbackTimer.start();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
currentIndex = (currentIndex + 1) % playlist.size();
playSong();
}
void Widget::loadPlaylist()
{
// 清空原歌单列表
ui->listWidget->clear();
playlist.clear();
// 扫描指定路径下的音乐文件
QDir directory("/home/source/Music");
QStringList musicFiles = directory.entryList(QStringList() << "*.mp3" << "*.wav" << "*.mp4", QDir::Files);
foreach (QString musicFile, musicFiles) {
playlist.append(directory.absoluteFilePath(musicFile));
ui->listWidget->addItem(musicFile); // 在 listWidget 中显示歌曲名称
}
// 重置当前播放索引
currentIndex = 0;
}
void Widget::loadLyrics(const QString &lyricPath)
{
QFile file(lyricPath); // 创建一个 QFile 对象,用于操作歌词文件
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
// 如果无法打开歌词文件,则输出错误信息并返回
qDebug() << "Failed to open lyric file";
return;
}
lyricEntries.clear(); // 清空现有的歌词条目
ui->lyricWidget->clear(); // 清空界面上的歌词显示
QTextStream in(&file); // 创建一个 QTextStream 对象,用于读取文件内容
while (!in.atEnd()) { // 循环读取文件,直到文件末尾
QString line = in.readLine(); // 读取一行歌词
QRegExp rx("\\[(\\d{2}):(\\d{2})\\.(\\d{2})\\](.*)"); // 定义正则表达式,用于解析歌词的时间戳和内容
if (rx.indexIn(line) != -1) { // 如果正则表达式匹配成功
int min = rx.cap(1).toInt(); // 提取分钟部分
int sec = rx.cap(2).toInt(); // 提取秒钟部分
int msec = rx.cap(3).toInt() * 10; // 提取毫秒部分,并转换为实际时间
QTime timestamp(0, min, sec, msec); // 创建一个 QTime 对象表示时间戳
QString text = rx.cap(4).trimmed(); // 提取并修剪歌词文本
// 将时间戳和歌词文本作为一个条目添加到歌词列表中
lyricEntries.append({ timestamp, text });
}
}
file.close(); // 关闭歌词文件
displayLyrics(); // 显示歌词
}
void Widget::on_listWidget_itemDoubleClicked(QListWidgetItem *item)
{
// 先将状态设置为播放状态
if (isPaused) {
isPaused = false;
playbackTimer.start();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
currentIndex = ui->listWidget->row(item);
playSong();
}
void Widget::on_btn_Volume_clicked()
{
// 显示或隐藏音量滑块
ui->playerVolumeSlider->setVisible(!ui->playerVolumeSlider->isVisible());
}
void Widget::on_playerVolumeSlider_valueChanged(int value)
{
// 调节音量(如果播放器正在运行)
if (mplayer->state() == QProcess::Running) {
QString volCmd = QString("volume %1 1\n").arg(value);
mplayer->write(volCmd.toUtf8());
}
}
void Widget::updateCurrentPosition()
{
if (mplayer->state() == QProcess::Running) {
mplayer->write("get_time_pos\n");
}
}
void Widget::onStarted()
{
qDebug() << "准备播放";
// 获取播放文件的长度以毫秒为单位
mplayer->write("get_time_length\n");
}
void Widget::onError(QProcess::ProcessError error)
{
qDebug() << "播放进程出错:" << error;
}
void Widget::onReadyReadStandardOutput()
{
while (mplayer->bytesAvailable()) {
QByteArray arr = mplayer->readLine();
QString str = QString::fromLocal8Bit(arr);
if (str.contains("ANS_LENGTH")) {
str.remove("\n").remove("\r").remove("ANS_LENGTH=");
totalDuration = str.toDouble(); // 改为 double 以处理小数部分
int m = static_cast<int>(totalDuration) / 60;
int s = static_cast<int>(totalDuration) % 60;
ui->labAllTime->setText(QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0')));
ui->playCourseSlider->setMaximum(static_cast<int>(totalDuration));
}
if (str.contains("ANS_TIME_POSITION")) {
str.remove("\n").remove("\r");
currentPosition = QString(str.split("=").at(1)).toDouble(); // 改为 double 以处理小数部分
int m = static_cast<int>(currentPosition) / 60;
int s = static_cast<int>(currentPosition) % 60;
ui->labCurTime->setText(QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0')));
ui->playCourseSlider->setValue(static_cast<int>(currentPosition));
syncLyrics();
}
}
}
void Widget::onFinished(int exitCode)
{
// 如果退出码为 0(正常退出),则播放下一首歌曲
if (exitCode == 0) {
// 重置当前播放位置为0
currentPosition = 0;
ui->playCourseSlider->setValue(0);
ui->labCurTime->setText("00:00");
// 如果当前处于暂停状态,则切换为播放状态
if (isPaused) {
isPaused = false;
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
}
// 更新歌曲信息
updateSongInfo();
playSong();
}
}
void Widget::updateSongInfo()
{
if (currentIndex >= 0 && currentIndex < playlist.size()) {
QString song = playlist.at(currentIndex);
QString lyricPath = "/home/source/Lyric/" + QFileInfo(song).baseName() + ".lrc";
if (!song.endsWith(".mp4", Qt::CaseInsensitive)) {
// 如果不是视频文件,加载歌词
loadLyrics(lyricPath);
}
// 更新列表显示
highlightCurrentSong();
}
}
void Widget::on_playCourseSlider_sliderMoved(int position)
{
// 计算分钟和秒钟部分
int m = position / 60;
int s = position % 60;
// 在界面上更新当前播放时间显示
ui->labCurTime->setText(QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0')));
// 更新歌词同步
int currentTime = position;
for (int i = 0; i < lyricEntries.size(); ++i) {
// 计算当前歌词时间范围
int currentLyricTime = lyricEntries[i].timestamp.minute() * 60 + lyricEntries[i].timestamp.second();
int nextLyricTime = (i + 1 < lyricEntries.size()) ?
lyricEntries[i + 1].timestamp.minute() * 60 + lyricEntries[i + 1].timestamp.second() :
totalDuration;
if (currentTime >= currentLyricTime && currentTime < nextLyricTime) {
// 如果当前时间在当前歌词时间范围内,则设置该歌词为加粗显示,并将其滚动到视图中央
QFont font = ui->lyricWidget->item(i)->font();
font.setBold(true);
ui->lyricWidget->item(i)->setFont(font);
ui->lyricWidget->setCurrentRow(i);
} else {
// 如果不是当前歌词范围内的时间,则取消加粗显示
QFont font = ui->lyricWidget->item(i)->font();
font.setBold(false);
ui->lyricWidget->item(i)->setFont(font);
}
}
}
void Widget::on_playCourseSlider_sliderReleased()
{
playbackTimer.start();
// 获取滑动条的值,即歌曲的时间位置
int position = ui->playCourseSlider->value();
ui->btn_Player->setIcon(QIcon(":/ICON/stop.png"));
// 发送设置歌曲播放时间的命令给播放器进程
QString setPositionCmd = QString("seek %1 2\n").arg(position);
mplayer->write(setPositionCmd.toUtf8());
// 更新当前播放位置
currentPosition = position;
// 同步歌词显示
syncLyrics();
}
void Widget::on_playCourseSlider_sliderPressed()
{
playbackTimer.stop();
}
void Widget::on_btn_model_clicked()
{
// 切换播放模式
if (playMode == Sequential) {
playMode = Random;
ui->btn_model->setIcon(QIcon(":/ICON/Random.png")); // 设置随机播放图标
} else {
playMode = Sequential;
ui->btn_model->setIcon(QIcon(":/ICON/Loop.png")); // 设置顺序播放图标
}
}
总结
这次课设其实没花什么心思和时间,因此代码写的十分乱以及可能还有些许小bug,不过基本的功能都还是可以实现,谢谢观看。