基于QT的ARM音乐播放器

前文

本次课设的要求是通过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,不过基本的功能都还是可以实现,谢谢观看。

  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值