【基于Qt5仿音乐播放器(四)】——播放控制

【基于Qt5仿音乐播放器(四)】------播放控制

  • [1. QMediaPlayer类](#1. QMediaPlayer类)
  • [2. QMediaPlayList类](#2. QMediaPlayList类)
  • [3. 歌曲播放](#3. 歌曲播放)
  • [4. 音量设置](#4. 音量设置)
  • [5. 播放进度条处理](#5. 播放进度条处理)
  • [6. 歌曲信息同步](#6. 歌曲信息同步)
  • [7. 歌词同步](#7. 歌词同步)

歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置⼯作基本完成,接下来重点处理音乐播放,歌曲播放需要用到Qt提供的QMediaPlayer类和QMediaPlaylist类

1. QMediaPlayer类

QMediaPlayer是Qt框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制。使用时需要包含 #include < QMediaPlayer > 头⽂件,并且需要在.pro项目文件中添加媒体库,即: QT += multimedia ,将 multimedia 模块导入到⼯程中,就可以使用该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList等。

【QMediaPlayer::State枚举类型:】

状态枚举名称 枚举值 说明
QMediaPlayer::StoppedState 0 播放停止状态
QMediaPlayer::PlayingState 1 播放状态
QMediaPlayer::PausedState 2 播放暂停状态

【QMediaPlayer::Flag:】

状态枚举名称 枚举值 说明
QMediaPlayer::LowLatency 0 播放未压缩的音频文件,播放表现为低时延,主要播放蜂鸣、手机铃声
QMediaPlayer::StreamPlayback 1 播放给予QIODevice构建的媒体文件,QMediaPlayer或自动选择支持的流进行播放
QMediaPlayer::VideoSurface 2 渲染视频到QAbstractVideoSurface输出

【QMediaPlayer::Error】

状态枚举名称 枚举值 说明
QMediaPlayer::NoError 0 没有错误
QMediaPlayer::ResourceError 1 媒体源无法解析
QMediaPlayer::FormatError 2 媒体源格式不支持,可能会播放,但是没有音频和视频组件
QMediaPlayer::NetworkError 3 网络错误
QMediaPlayer::AccessDeniedError 4 没有媒体源的访问权限
QMediaPlayer::ServiceMissingError 5 没有有效的播放服务,无法继续播放

【QMediaPlayer::MediaStatus】

状态枚举名称 枚举值 说明
QMediaPlayer::UnknownMediaStatus 0 媒体的状态未被定义
QMediaPlayer::NoMedia 1 没有媒体文件,player处于StoppedState
QMediaPlayer::LoadingMedia 2 媒体文件加载中,player可以处于任何状态
QMediaPlayer::LoadedMedia 3 媒体文件已经加载,player处于StoppedState
QMediaPlayer::StalledMedia 4 媒体处于延迟或者暂时中断状态,player处于PlayingState或PauseState
QMediaPlayer::BufferingMedia 5 媒体正在缓冲数据,player处于PlayingState或PauseState
QMediaPlayer::BufferedMedia 6 媒体数据缓冲完成,player处于PlayingState或PauseState
QMediaPlayer::EndOfMedia 7 媒体结束,player处于StoppedState
QMediaPlayer::InvalidMedia 8 ⾮法的媒体文件,player处于StoppedState

【常用属性】

cpp 复制代码
// 部分常⽤属性 
const qint64 duration; // 保存媒体的总播放时间,单位为毫秒 
const QMediaContent currentMedia; // 当前正在播放媒体的媒体内容 
const QString error; // 最近⼀次错误信息 
int volume; // 保存⾳量⼤⼩,范围在0~100之间 
const bool audioAvailable; // ⾳频是否可⽤,audioAvailableChanged
信号⽤于监听其状态 
QMediaPlaylist* playtlist; // 播放列表

【常用函数】

cpp 复制代码
qint64 duration() const; // 获取当前媒体的总时间 
qint64 position() const; // 获取当前媒体的播放位置 
int volume() const; // 获取播放⾳量⼤⼩ 
bool isMuted() const; // 检测是否静⾳ 
State state() const; // 获取当前媒体的播放状态 
QMediaContent currentMedia() const; // 获取当前正在播放的媒体内容 
QString errorString() const // 获取最近的⼀次错误 

【常用槽函数】

cpp 复制代码
void pause(); // 播放媒体 
void play(); // 暂停媒体 
void stop(); // 停⽌播放媒体
void setMuted(bool muted); // 设置是否静⾳,true为静⾳,false为⾮静⾳ 
void setVolume(int volume); // 设置播放⾳量,volume取值范围在0~100之间 
void setPosition(qint64 position); // 设置播放位置,position为要播放的时间,单位毫秒 
void setPlaylist(QMediaPlaylist *playlist);// 设置播放列表,若播放多个媒体需要设置,默认为空 
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr);// 设置媒体源

【常用信号】

cpp 复制代码
void stateChanged(QMediaPlayer::State state); // 播放状态改变时发射该信号 
void durationChanged(qint64 duration); // 播放时⻓改变时发射该状态 
void positionChanged(qint64 position); // 播放位置改变时发射该状态 
void volumeChanged(int volume); // ⾳量改变时发射该信号 
void metaDataAvailableChanged(bool available); // 源数据改变发出 

2. QMediaPlayList类

QMediaPlaylist 类提供了⼀种灵活强大的方式管理媒体文件的播放列表。通过结合QMediaplayer,可以实现顺序播放、循环播放随机播放等多种播放模式,提升用户的媒体播放体验。该类提供了以下功能:

  1. 添加和删除媒体文件
  2. 播放模式设置(列表播放、随机播放、单曲循环)
  3. 控制播放列表(开始,停止,上⼀曲,下⼀曲)
  4. 获取和设置当前媒体文件
  5. 信号槽支持

若播放多个媒体文件,必须使用该类来管理媒体文件,将该列表设置到player上,就可实现更加灵活的播放支持

【枚举方法】

状态枚举名称 枚举值 说明
QMediaPlaylist::CurrentItemOnce 0 单词播放
QMediaPlaylist::CurrentItemInLoop 1 单曲循环
QMediaPlaylist::Sequential 2 从当前选中位置,顺序播放
QMediaPlaylist::Loop 3 循环播放
QMediaPlaylist::Random 4 随机播放

【常见属性】

cpp 复制代码
int currentIndex; // 当前播放的媒体⽂件在媒体列表中的索引 
const QMediaContent currentMedia; // 当前选中的媒体⽂件 
QMediaPlaylist::PlaybackMode playbackMode; // 媒体列表中⽂件的播放模式

【枚举方法】

cpp 复制代码
bool addMedia(const QMediaContent &content); // 向媒体列表中添加单个媒体⽂件 
int mediaCount() const; // 获取播放列表中⽂件的个数 
int currentIndex() const; // 获取当前播放的媒体的索引 
bool clear(); // 清空媒体列表 
QMediaPlaylist::PlaybackMode playbackMode() const; // 获取媒体列表的播放模式 
QMediaContent currentMedia() const; // 获取当前播放的媒体⽂件 
QString errorString() const; // 获取最近⼀次发⽣过的错误 

【常见函数】

cpp 复制代码
void next(); // 下⼀曲 
void previous(); // 上⼀曲 
void setCurrentIndex(int playlistPosition); // 设置当前播放媒体的索引 
void shuffle(); // 媒体顺序打乱,重建媒体索引 

【常见信号】

cpp 复制代码
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);// 列表播放模式⽅法改变时发射 
void currentIndexChanged(int position);// 当前索引发⽣改变时发射 
void currentMediaChanged(const QMediaContent &content);// 当前媒体⽂件改变时发⽣ 

3. 歌曲播放

播放功能都是在widget中的,所有需要再widget中初始化QMediaPlayer和QMediaPlayList。

cpp 复制代码
// widget.h中添加
public:
	void initPlayer();
private:
// 设置播放设置
	QMediaPlayer *mediaPlayer;
	QMediaPlaylist *mediaPlayList;

// widget.cpp中添加
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 初始化界面
    initUi();
    onConnectionSignalAndSlots();
    // 初始化播放器
    initPlayer();
}

void Widget::initPlayer()
{
    mediaPlayer = new QMediaPlayer(this);
    mediaPlayList = new QMediaPlaylist(this);

    // 设置播放列表为默认循环播放
    mediaPlayList->setPlaybackMode(QMediaPlaylist::Loop);
    // 设置播放器列表
    mediaPlayer->setPlaylist(mediaPlayList);
    // 设置默认声音为20
    mediaPlayer->setVolume(20);
}

【播放列表设置】

播放之前,先要将歌曲加入播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到播放列表方法。

cpp 复制代码
// CommonPage.h中添加
#include <QMediaPlaylist>
addmusicToPlayer(MusicList musicList, QMediaPlaylist *mediapPlayList);


// CommonPage.cpp中添加
void CommonPage::addmusicToPlayer(MusicList musicList, QMediaPlaylist *mediapPlayList)
{
    for (auto music : musicList){
        switch(pageType)
        {
        case LIKE_PAGE:
            if (music.getIsLike()){
                mediapPlayList->addMedia(music.getMusicUrl());
            }
            break;
        case LOCAL_PAGE:
            mediapPlayList->addMedia(music.getMusicUrl());
            break;
        case HISTORY_PAGE:
            if (music.getIsHistory()){
                mediapPlayList->addMedia(music.getMusicUrl());
            }
            break;
        default:
            qDebug() << "错误歌曲类型";
            break;
        }
    }
}

【播放和暂停】

当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下,刚开始为停止状态。QMediaPlayer的播放状态有:PlayingState()、PausedState()、StoppedState()。分别对应着void play(); void pause(); void stop();

cpp 复制代码
// widget.cpp 对play控件添加槽函数
void Widget::on_play_clicked()
{
    if (mediaPlayer->state() == QMediaPlayer::PlayingState){
        mediaPlayer->pause();
    }else if (mediaPlayer->state() == QMediaPlayer::PausedState){
        mediaPlayer->play();
    }else if (mediaPlayer->state() == QMediaPlayer::StoppedState){
        mediaPlayer->play();
    }
}

注意:播放时默认是从播放列表索引为0的歌曲开始播放的。

另外播放和暂停时点击的按钮图标也需要进行修改该,QMediaPlayer中的stateChange信号,当播放状态改变时会触发此信号,所以只需要写一个槽函数拦截这个信号即可。

cpp 复制代码
// widget.h中添加
void onPlayerStateChanged();

// widget.cpp中添加
void Widget::initPlayer()
{
	// ....

    connect(mediaPlayer, &QMediaPlayer::stateChanged, this, &Widget::onPlayerStateChanged);
}

void Widget::onPlayerStateChanged()
{
    if (mediaPlayer->state() == QMediaPlayer::PlayingState){
        ui->play->setIcon(QIcon(":/images/play_on.png"));
    }else{
        ui->play->setIcon(QIcon(":/images/play3.png"));
    }
}

【上一曲/下一曲播放切换】

cpp 复制代码
//在widget.cpp中添加
// 依旧是找到上一曲和下一曲的控件,通过右击鼠标点击添加到槽函数按钮
void Widget::on_PlayUp_clicked()
{
    mediaPlayList->previous();
}

void Widget::on_playDown_clicked()
{
    mediaPlayList->next();
}

【播放模式切换】

QMediaPlaylist提供了获取和设置播放模式的方法

cpp 复制代码
QMediaPlaylist::PlaybackMode playbackMode() const; // 获取播放模式 
void setPlaybackMode(QMediaPlaylist::PlaybackMode mode); // 设置播放模式

我们支持的模式有三种:顺序播放,循环单曲播放,随机播放

cpp 复制代码
//在widget.cpp中添加
// 依旧是找到切换模式的控件,通过右击鼠标点击添加到槽函数按钮
void Widget::on_playModel_clicked()
{
    if (mediaPlayList->playbackMode() == QMediaPlaylist::Loop){
        ui->playModel->setToolTip("随机播放");
        mediaPlayList->setPlaybackMode(QMediaPlaylist::Random);
    }else if (mediaPlayList->playbackMode() == QMediaPlaylist::Random){
        ui->playModel->setToolTip("单曲循环");
        mediaPlayList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
    }else if(mediaPlayList->playbackMode() == QMediaPlaylist::CurrentItemInLoop){
        ui->playModel->setToolTip("顺序播放");
        mediaPlayList->setPlaybackMode(QMediaPlaylist::Loop);
    }else{
        qDebug() << "播放模式错误";
    }
}

当然我们进行模式切换的时候,也是需要对播放模式的图案就行切换到,而QMediaPlaylist提供了一个信号playbackModeChanged,当播放模式切换时触发此信号。

cpp 复制代码
//在widget.h中添加
void onPlayBackModelChanged(QMediaPlaylist::PlaybackMode playBackMode);

//在widget.cpp中添加
void Widget::initPlayer()
{
	// ...
	
    //关联播放模式
    connect(mediaPlayList, &QMediaPlaylist::playbackModeChanged, this, &Widget::onPlayBackModelChanged);
}
void Widget::onPlayBackModelChanged(QMediaPlaylist::PlaybackMode playBackMode)
{
    if (playBackMode == QMediaPlaylist::Loop){
        ui->playModel->setIcon(QIcon(":/images/list_play.png"));
    }else if (playBackMode == QMediaPlaylist::Random){
        ui->playModel->setIcon(QIcon(":/images/shuffle_2.png"));
    }else if(playBackMode == QMediaPlaylist::CurrentItemInLoop){
       ui->playModel->setIcon(QIcon(":/images/single_play.png"));
    }else{
        qDebug() << "暂不支持该模式";
    }
}

【播放所有按钮】

播放所有按钮属于CommonPage中的按钮,但是CommonPage中并没有播放音乐的功能,而只有widget有播放音乐的功能,因此这里当我们点击播放全部音乐的按钮时需要给widget发送信号,widget接收到信号后在进行处理。

而由于likePage,localPage,recentPage都是属于CommonPage,所以发送信号的时候需要将页面类型一并发送过去,因为每个页面所放的歌曲列表是不一样的。

cpp 复制代码
// CommonPage.h中添加
signals:
    void void playAll(PageType pageType);

// CommonPage.cpp中添加
// 依旧是找到播放全部的控件,通过右击鼠标点击添加到槽函数按钮
void CommonPage::on_playAllBtn_clicked()
{
    emit playAll(pageType);
}

在widget中给playAll关联槽函数,并播放发送过来的页面所存放的歌曲。

cpp 复制代码
// widget.h中添加
void onPlayAll(PageType pageType)

// widget.cpp中添加
void Widget::initPlayer()
{
	//....

    // 关联播放全部按钮
    connect(ui->likePage, &CommonPage::playAll, this, &Widget::onPlayAll);
    connect(ui->localPabe, &CommonPage::playAll, this, &Widget::onPlayAll);
    connect(ui->recentPage, &CommonPage::playAll, this, &Widget::onPlayAll);
}

void Widget::onPlayAll(PageType pageType)
{
    //  确定是哪一个页面
    CommonPage *currentPage = nullptr;
    switch(pageType)
    {
    case LIKE_PAGE:
        currentPage = ui->likePage;
        break;
    case LOCAL_PAGE:
        currentPage = ui->localPabe;
        break;
    case HISTORY_PAGE:
        currentPage = ui->recentPage;
        break;
    default:
        qDebug() << "扩展";
        break;
    }
    playAllByCurrentPage(currentPage, 0);
}

void Widget::playAllByCurrentPage(CommonPage *commonPage, int index)
{
    // 清空播放列表
    mediaPlayList->clear();
    // 重新将接收到的页面加载歌曲
    commonPage->addmusicToPlayer(musicList, mediaPlayList);
    // 歌曲从索引为0处进行播放
    mediaPlayList->setCurrentIndex(index);
    mediaPlayer->play();
}

【双击歌曲进行播放】

当QListWidget中的项被双击时,会触发 doubleClicked 信号,该信号在QListWidget的基类中定义,有⼀个index参数,表示被双击的QListWidgetItem在QListWidget中的索引,该索引刚好与QMediaPlaylist中歌曲的所以⼀致,被双击时直接播放该首歌曲即可。

cpp 复制代码
// CommonPage.h 中新增 
void playMusicByIndex(CommonPage* page, int index);

// CommonPage.cpp 中新增 
CommonPage::CommonPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::CommonPage)
{
    ui->setupUi(this);
    // 关联双击事件
    connect(ui->pageMusicList, &QListWidget::doubleClicked, this, [=](const QModelIndex &index){
        emit playMusicByIndex(this, index.row());
    });
}

// widget.h 中新增 
void Widget::onPlayMusicByIndex(CommonPage *commonPage, int index)
{
    playAllByCurrentPage(commonPage, index);
}

void Widget::initPlayer()
{
	// ...
	
    // 关联双击事件
    connect(ui->likePage, &CommonPage::playMusicByIndex, this, &Widget::onPlayMusicByIndex);
    connect(ui->localPabe, &CommonPage::playMusicByIndex, this, &Widget::onPlayMusicByIndex);
    connect(ui->recentPage, &CommonPage::playMusicByIndex, this, &Widget::onPlayMusicByIndex);
}

【最近播放同步】

当播放歌曲改变时,即播放的媒体源发生了变化,QMediaPlayer会触发metaDataAvailableChanged信号,QMediaPlaylist也会触发 currentIndexChanged 信号,该信号会带index参数,表示现在是媒体播放列表中的index歌曲被播放,通过index可以获取到当前页面中具体播放的歌曲,将该歌曲对应Music对象的isHistoty属性修改为true,然后更新下rencentPage的歌曲列表,播放过的歌曲就添加到历史播放页面中了。因此widget类需要维护CommonPage*变量currentPage,记录当前正在播放的CommonPage页面,初始时设置为localPage,当播放的页面发生改变时,修改currentPage为当前正在播放页面,其中点击播放所有按钮以及双击QListWidget中项的时候都会引起currentPage的改变。

cpp 复制代码
// widget.h中新增
private:
    CommonPage *currentPage;
void Widget::initUi()
{
    // ....

    currentPage = ui->localPabe;
}
void Widget::playAllByCurrentPage(CommonPage *commonPage, int index)
{
    // ....
    currentPage = commonPage;
}

同步歌曲逻辑如下:

cpp 复制代码
// widget.h中新增
void onCurrentIndexChanged(int index);

// widget.cpp中新增
void Widget::initPlayer()
{
    //....

    // 处理最近播放列表
    connect(mediaPlayList, &QMediaPlaylist::currentIndexChanged, this, &Widget::onCurrentIndexChanged);
}
void Widget::onCurrentIndexChanged(int index)
{
    // 播放列表被清空或索引无效时(如 currentIndexChanged(-1))直接忽略
    if (index < 0){
        return;
    }
    // 查找该歌曲是否已经添加到播放列表中
    QString musicId = currentPage->getCurrentIndexMusicId(index);
    auto it = musicList.findMusicById(musicId);
    if (it != musicList.end()){
        it->setIsHistory(true);
    }
    ui->recentPage->reFresh(musicList);
}


// commmonpage.h 中新增
QString getCurrentIndexMusicId(int index) const;
// commmonpage.cpp 中新增
QString CommonPage::getCurrentIndexMusicId(int index) const
{
    if (index < 0 || index >= musicListOfPage.size()){
        qDebug() << "查无此歌曲";
        return QString();
    }
    return musicListOfPage[index];
}

4. 音量设置

【音量静音设置】

当我们点击静音按钮时,声音静音同时静音的图标也会发生改变,但是由于静音的设置是由QMediaPlayer设置的,而QMediaPlayer实在widget中的,所有这里需要通过VolumeTool以发射信号的方式告诉widget静音。

cpp 复制代码
// volumeTool.h中新增
signals:
    void setSilence(bool isMuted);
private:
    bool isMuted;
    int volume;

// volumeTool.cpp中新增
VolumeTool::VolumeTool(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::VolumeTool),
    isMuted(false)
{
	//...
}
// 依旧是在volumeTool.ui中找到静音控件,然后点击抓到槽函数
void VolumeTool::on_slientBtn_clicked()
{
    isMuted = !isMuted;
    if (isMuted){
        ui->slientBtn->setIcon((QIcon(":/images/silent.png")));
    }else{
        ui->slientBtn->setIcon((QIcon(":/images/volumn.png")));
    }
    emit setSilence(isMuted);
·38}

// widget.h中新增
void Widget::onConnectionSignalAndSlots()
{
    // ...

    connect(volumeTool, &VolumeTool::setSilence, this, &Widget::setMusicIsMuted);
}
void Widget::setMusicIsMuted(bool isMuted)
{
    mediaPlayer->setMuted(isMuted);
}

【鼠标点击/滑动调节音量】

当点击和滑动音量键盘的滑杆时,需要改变sliderBtn,和outSlider的位置。当我们进行点击时滑杆时需要移动sliderBtn,和outSlider的位置,当鼠标抬起时通过outSlider与inSlider的比率计算出音量的大小,然后通过发送信号给widget来进行修改音量操作。当我们鼠标长按滑杆来改变音量时,需要移动sliderBtn,和outSlider的位置并且同步的发送信号,来修改音量。

cpp 复制代码
// VolumeTool.h中新增
public:
    bool eventFilter(QObject *object, QEvent *event);
private:
    void moveSlider();
signals:
    void setMusicVolum(int volume);

// VolumeTool.cpp中新增
bool VolumeTool::eventFilter(QObject *object, QEvent *event)
{
    //qDebug() << "VolumeTool::eventFilter 触发";
    if (object == ui->sliderBox){
        //qDebug() << "声音过滤器触发";
        if (event->type() == QEvent::MouseButtonPress){
            moveSlider();
        }else if (event->type() == QEvent::MouseButtonRelease){
            emit setMusicVolum(volume);
        }else if (event->type() == QEvent::MouseMove){
            moveSlider();
            emit setMusicVolum(volume);
        }
        return true;
    }
    return QObject::eventFilter(object, event);
}

void VolumeTool::moveSlider()
{
    QPoint point = ui->sliderBox->mapFromGlobal(QCursor().pos());

    int hight = point.y();
    hight = hight < 25 ? 25 : hight;
    hight = hight > 205 ? 205 : hight;

    // 移动slider
    ui->outSlider->setGeometry(ui->inSlider->x(), hight, ui->inSlider->width(), 205 - hight);
    // 移动sliderBtn按钮
    ui->sliderBtn->move(ui->sliderBtn->x(), hight - (ui->sliderBtn->height() / 2));
    // 计算音量
    volume = (int)(((205 - hight) / 180.0) * 100);
    //qDebug() << volume;
    ui->volumeRadio->setText(QString::number(volume) + "%");
}


// widget.h中新增
void setPlayerVolume(int volume);

// widget.cpp中新增
void Widget::onConnectionSignalAndSlots()
{
    // ....
    
    connect(volumeTool, &VolumeTool::setMusicVolum, this, &Widget::setPlayerVolume);
}
void Widget::setPlayerVolume(int volume)
{
    mediaPlayer->setVolume(volume);
}

【歌曲播放总时间更新】

歌曲的总播放时长duration可以在Music类中得到,也可通过QMediaPlayer中的duration得到,但是通过这两种方法获得总时长的调用时机不好确定。而QMediaPlayer中有个信号durationChanged,当歌曲的总时长发生改变时会触发这个信号,所以当我们切换歌曲时,由于总时长发生改变就会触发这个信号,进而修改总时长。

cpp 复制代码
// widget.h中新增
void onDurationChanged(qint64 duration);

// widget.cpp中新增
void Widget::initPlayer()
{
	// ....
	connect(mediaPlayer, &QMediaPlayer::durationChanged, this, &Widget::onDurationChanged);
}
void Widget::onDurationChanged(qint64 duration)
{
    // 返回的是以毫秒级别的改变后的总时长
    int mm = duration / 1000 / 60;
    int ss = duration / 1000 % 60;
    ui->totallTime->setText(QString("%1:%2").arg(mm, 2, 10, QChar('0')).arg(ss,2,10, QChar('0')));
}

【歌曲当前播放总时间更新】

在音乐播放的时候,我么希望看到音乐的播放进度,也就是音乐播放的实时播放时间。QMediaPlayer中有一个信号positionChanged,当音乐的播放位置(也就是当前播放时间改变的时候)会触发这个信号,其中会带一个单位为毫秒的参数,这个参数就是当前音乐播放的实时时间。

cpp 复制代码
// widget.h中新增
void onPositionChanged(qint64 duration);

// widget.cpp中新增
void Widget::initPlayer()
{
	// ....
	connect(mediaPlayer, &QMediaPlayer::positionChanged, this, &Widget::onPositionChanged);
}
void Widget::onPositionChanged(qint64 duration)
{
    int mm = duration / 1000 / 60;
    int ss = duration / 1000 % 60;
    ui->currentTime->setText(QString("%1:%2").arg(mm, 2, 10, QChar('0')).arg(ss,2,10, QChar('0')));
}

5. 播放进度条处理

进度条的理解也很简单。进度条的位置需要和音乐当前播放时长进行对应,并且可以通过滑动进度条的方式和点击进度条的方式来改变歌曲播放的位置。期间进度条的位置需要改变,音乐播放的位置也需要改变,同时当前播放时长的显示也需要改变。

【进度条界面显示】

同样是用鼠标进行点击和拖拽。当鼠标进行点击时需要改变进度条的位置。当我们松开鼠标时同样需要发送一个信号给widget让widget修改当前播放音乐的时长。而当我们使用长安鼠标进行滑动式同步的需要修改进度条的位置和发送信号修改播放位置。

cpp 复制代码
// musicslider.h中新增
bool eventFilter(QObject *object, QEvent *event);
void moveSlider();

// musicslider.cpp中新增
MusicSlider::MusicSlider(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MusicSlider)
{
	// 安装事件过滤器
    installEventFilter(this);
}
bool MusicSlider::eventFilter(QObject *object, QEvent *event)
{
    //qDebug() << "声音过滤器触发";
    if (event->type() == QEvent::MouseButtonPress){
        moveSlider();
        return true;
    }else if (event->type() == QEvent::MouseButtonRelease){
        moveSlider();
        emit setMusicSliderPosition(position);
        return true;
    }else if (event->type() == QEvent::MouseMove){
        moveSlider();
        return true;
    }
    return QObject::eventFilter(object, event);
}
void MusicSlider::moveSlider()
{
    QPoint point = this->mapFromGlobal(QCursor().pos());
    int x = point.x();
    x = x > 800 ? 800 : x;
    x = x < 0 ? 0 : x;
    ui->outLine->setGeometry(ui->inLine->x(), ui->inLine->y(), x, ui->inLine->height());
    // 计算当前位置的歌曲时间位置比例
    position = (float)x / (float)(ui->inLine->width());
}

// widget.h中新增
void onSetMusicSliderPosition(float position);

// widget.cpp中新增
void Widget::initPlayer()
{
	// ....
	connect(ui->processBar, &MusicSlider::setMusicSliderPosition, this, &Widget::onSetMusicSliderPosition);
}
void Widget::onSetMusicSliderPosition(float position)
{
    qDebug() << position;
    qint64 duration = (qint64)totallDuration * position;
    int mm = duration / 1000 / 60;
    int ss = duration / 1000 % 60;
    ui->currentTime->setText(QString("%1:%2").arg(mm, 2, 10, QChar('0')).arg(ss,2,10, QChar('0')));
    mediaPlayer->setPosition(duration);
}

【进度条同步播放时间】

当我们歌曲在进行播放的时候,歌曲的当前播放时间持续在变化,进度条也需要同步着跟着变化。此时需要计算当前播放时间与音乐总时间的比例,将这个比例传给musicslider,用这个比例乘以进度条总长度得到当前的进度条位置。

cpp 复制代码
// musicslider.h中新增
void movePosition(float duration);

// musicslider.cpp中新增
void MusicSlider::movePosition(float duration)
{
    int x = duration * ui->inLine->width();
    ui->outLine->setGeometry(ui->inLine->x(), ui->inLine->y(), x, ui->inLine->height());
}

// widget.h中新增
private:
	qint64 totallDuration = 0;
	
// widget.cpp中新增
void Widget::onDurationChanged(qint64 duration)
{
    totallDuration = duration; // 记录总时长
    
    // ....
}
void Widget::onPositionChanged(qint64 duration)
{
    // ....
    
    ui->processBar->movePosition((float)duration / (float)totallDuration);
}

6. 歌曲信息同步

在进行歌曲切换时候,歌曲名称、歌⼿以及歌曲的封面图,也需要更新到界面。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使用"ThumbnailImage"作为参数,注意有些歌曲可能没有封面图,如果没有可以设置一张默认的封面图。由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,只有歌曲文件是有效的才能播放;QMediaPlayer提供了 metaDataAvailableChanged(bool) 信号关联槽函数,当歌曲更换时,完成信息的更新发送信号。

cpp 复制代码
// widget.h中新增
void onMetaDataAvailableChanged(bool available);

// widget.cpp中新增
void Widget::initPlayer()
{
    /....
    
    connect(mediaPlayer, &QMediaPlayer::metaDataAvailableChanged, this, &Widget::onMetaDataAvailableChanged);

}
void Widget::onMetaDataAvailableChanged(bool available)
{
    qDebug() << "歌曲切换";
    // 设置歌手和歌名
    QString musicSinger = mediaPlayer->metaData("Author").toString();
    QString musicName = mediaPlayer->metaData("Title").toString();

    auto it = musicList.findMusicById(currentPage->getCurrentIndexMusicId(currentIndex));
    if (it != musicList.end()){
        musicName = it->getMusicName();
        musicSinger = it->getMusicSinger();
    }

    ui->musicName->setText(musicName);
    ui->musicSinger->setText(musicSinger);

    // 设置图片
    QVariant coverImage = mediaPlayer->metaData("ThumbnailImage");
    if (coverImage.isValid()){
        QImage image = coverImage.value<QImage>();
        ui->musicCover->setPixmap(QPixmap::fromImage(image));

        currentPage->setImageLabel(QPixmap::fromImage(image));
    }else{
        QString path = ":/images/non-music.png";
        ui->musicCover->setPixmap(path);
        currentPage->setImageLabel(path);
    }
    ui->musicCover->setScaledContents(true);
}

7. 歌词同步

播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有标题栏。内部显示当前播放歌曲的歌词,以及歌曲名称和作者。当点击下拉按钮时,窗口会隐藏起来。

【歌词界面布局】

  1. 在qt create中新创建⼀个qt设计师界面,命名为LrcPage,geometry的宽⾼修改为:1020*680。
  2. 拖⼀个Widget到LrcPage中,objectName修改为bgStyle,选中LrcPage,然后点击垂直布局,并将LrcPage的margin和spacing修改为0;
  3. 拖两个Widget到bgStyle中,objectName从上往下分别修改为lrcTop和lrcContent,lrcTop的minimumSize和maximumSize的高修改为50;然后选中bgStyle点击垂直布局,并将bgStyle的margin和spacing修改为0;
  4. 拖⼀个按钮到lrcTop中,objectName修改为hideBtn,minimumSize和maximumSize的宽和高修改为:30*50;拖⼀个Widget到lrcPage中,objectName修改为titleBox;然后选中lrcTop,点击水平布局,并将lrcTop的margin和spacing修改为0;
  5. 拖两个QLabel到titleBox中,objectName从上往下修改为musicSinger和musicName,然后选中titleBox,点击垂直布局,并将titleBox的margin和spacing修改为0;
  6. 拖六个QLabel到lrcContent中,从上往下将objectName依次修改为:line1、line2、line3、lineCenter、line4、 line5、line6,将line1~line6的minimumSize的高度修改为50,font大小修改为15,将lineCenter的minimumSize高度修改为80,font的大小修改为25;拖两个垂直弹簧,⼀个放在line1上,⼀个放在line6下,将所有的QLabel撑到中间
  7. 选中lrcContent,然后点击垂直布局,将lrcContent的margin和spacing修改为0

byStyle

css 复制代码
#bgStyle{
	border-image: url(:/images/bg.png);
}
*{
	color:rgb(255, 0, 127);
}

lineCenter

css 复制代码
#lineCenter{
	color:#1ECE9A;
}

hideBtn

css 复制代码
#hideBtn{
	border: none;
	background-color:rgb(191, 191, 191);
}

【LrcPage显示】

在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏

cpp 复制代码
// lrcpage.h中新增
LrcPage::LrcPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::LrcPage)
{
    ui->setupUi(this);
    // 隐藏标题栏
    this->setWindowFlag(Qt::FramelessWindowHint);
    connect(lrcAnimation, &QPropertyAnimation::finished, this, [=](){
        hide();
    });
    ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
}

在widget中初始化出隐藏界面,并给lrcWord关联槽函数,当点击lrcWord是出现界面

cpp 复制代码
//widget.h中新增
void on_lrcWord_clicked();
private:
	LrcPage *lrcPage;
//widget.cpp中新增
void Widget::initUi()
{
    // ...
    
    // 创建lrc窗口
    lrcPage = new LrcPage(this);
    lrcPage->hide();
}
void Widget::on_lrcWord_clicked()
{
    lrcPage->show();
    lrcAnimation->start();
}

【LrcPage添加动画效果】

我们希望点击歌词是有一个上拉的动画,点击关闭歌词是有一个下拉的动画

cpp 复制代码
// widget.h中新增
QPropertyAnimation *lrcAnimation;
// widget.cpp中新增
void Widget::initUi()
{
    // ....
    
    // 添加歌词入场动画
    lrcAnimation = new QPropertyAnimation(lrcPage, "geometry", this);
    lrcAnimation->setDuration(250);
    lrcAnimation->setStartValue(QRect(10, 10 + lrcPage->height(), lrcPage->width(), lrcPage->height()));
    lrcAnimation->setEndValue(QRect(10, 10, lrcPage->width(), lrcPage->height()));
}
cpp 复制代码
// lrcpage.h中新增
QPropertyAnimation *lrcAnimation;
// lrcpage.cpp中新增
LrcPage::LrcPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::LrcPage)
{
    ui->setupUi(this);
    // 隐藏标题栏
    this->setWindowFlag(Qt::FramelessWindowHint);

    lrcAnimation = new QPropertyAnimation(this, "geometry", this);
    lrcAnimation->setDuration(250);
    lrcAnimation->setStartValue(QRect(10, 10 , width(), height()));
    lrcAnimation->setEndValue(QRect(10, 10 + height(), width(), height()));


    connect(ui->hideBtn, &QPushButton::clicked, this, [=](){
        lrcAnimation->start();
    });
}

【LrcPage歌词解析和同步】

歌词文件的格式为.lrc,歌词的格式如下:

并且通常歌词文件与歌曲文件的名字是一样的,只是后缀名是不一样的,我们在Music中以及知道了每一首音乐的paht,现在只需要通过替换音乐的后缀名就可以得到歌词文件的path。

cpp 复制代码
// music.h中新增
QString getLyricFilePath() const;

// music.cpp中新增
QString Music::getLyricFilePath() const
{
    QString path = musicUrl.toLocalFile();
    path.replace(".mp3", ".lrc");
    path.replace(".flac", ".lrc");
    path.replace(".mpga", ".lrc");
    return path;
}

从歌词格式可以看出,他是一行一行的进存储的,并且由两部分组成(歌词时间段:歌词),于是我们需要将所有歌词进行读取,然后按照歌词的时间段进行显示歌词。

cpp 复制代码
// lrcPage.h中新增
struct LyricLine{
    qint64 time;
    QString text;
    LyricLine(qint64 qtime, QString qtext)
        :time(qtime),
        text(qtext)
    {}
};
private:
    QVector<LyricLine> lyricLine;
    
// lrcPage.cpp中新增
bool LrcPage::parseLrc(const QString lrcPath)
{
    // 清空存储歌词容器
    lyricLine.clear();
    // 打开歌词文件
    QFile lrcFile(lrcPath);
    if (!lrcFile.open(QFile::ReadOnly)){
        qDebug() << "打开文件失败:" <<  lrcPath;
        return false;
    }
    QByteArray data = lrcFile.readAll();
    // 1. 先处理 BOM
    QTextCodec *codec = nullptr;
    if (data.startsWith("\xEF\xBB\xBF")){               // UTF-8 BOM
        data.remove(0, 3);
        codec = QTextCodec::codecForName("UTF-8");
    } else if (data.startsWith("\xFF\xFE")){            // UTF-16 LE BOM
        codec = QTextCodec::codecForName("UTF-16LE");
    } else if (data.startsWith("\xFE\xFF")){            // UTF-16 BE BOM
        codec = QTextCodec::codecForName("UTF-16BE");
    }

    QString content;
    if (codec){
        content = codec->toUnicode(data);
    } else {
        // 2. 无 BOM:用 round-trip 校验判断是否为严格合法 UTF-8
        if (QString::fromUtf8(data).toUtf8() == data){
            content = QString::fromUtf8(data);
        } else {
            // 3. 不是 UTF-8,按 GB18030(兼容 GBK/GB2312)解码
            content = QTextCodec::codecForName("GB18030")->toUnicode(data);
        }
    }

    const QStringList lines = content.split('\n');
    for (const QString &lrcWord : lines){
        // 解析歌词
        int left = lrcWord.indexOf("[");
        int right = lrcWord.indexOf("]");
        // 跳过空行或不含时间标签的行
        if (left == -1 || right == -1){
            continue;
        }

        qint64 lrcTime = 0;
        int start = 0;
        int end = 0;
        QString time = lrcWord.mid(left, right - left - 1);

        // 解析分钟
        start = left + 1;
        end = time.indexOf(":", start);
        lrcTime += lrcWord.mid(start, end - start).toInt()*60*1000;

        // 解析秒
        start = end + 1;
        end = time.indexOf(".", start);
        lrcTime += lrcWord.mid(start, end - start).toInt()*1000;

        // 解析毫秒
        start = end + 1;
        end = time.indexOf(".", start);
        lrcTime += lrcWord.mid(start, end - start).toInt();

        // 解析歌词
        QString word = lrcWord.mid(right + 1).trimmed();

        lyricLine.push_back(LyricLine(lrcTime, word));

        qDebug() << word << ":" << lrcTime;
    }
    return true;
}

【根据歌曲播放位置获取歌词并显示】

当歌曲播放进度改变时候,QMediaPlayer的positionChanged信号会触发,该信号同步播放时间的时候已经在widget类中处理过了,在其槽函数中就能拿到当前歌曲的播放时间,通过播放时间,就能在LrcPage中找到对应行的歌词。

cpp 复制代码
// lrcpage.cpp中新增
int LrcPage::getLineLrcWordIndex(qint64 posTime)
{
    if (lyricLine.isEmpty()){
        return -1;
    }
    if (lyricLine[0].time > posTime){
        return 0;
    }
    for (int i = 1; i < lyricLine.size(); ++i){
        if (posTime >= lyricLine[i - 1].time && posTime < lyricLine[i].time){
            return i - 1;
        }
    }
    return lyricLine.size() - 1;
}

QString LrcPage::getLyricWord(int index)
{
    if (index < 0 || index >= lyricLine.size()){
        return "";
    }
    return lyricLine[index].text;
}

// 根据每段歌词的播放时间显示歌词
void LrcPage::showWord(qint64 time)// 时间为实时播放歌曲时的当前时间
{
    // 获取歌词的索引
    int index = getLineLrcWordIndex(time);
    if (index == -1){
        ui->line1->setText("");
        ui->line2->setText("");
        ui->line3->setText("");
        ui->lineCenter->setText("当前无歌词");
        ui->line4->setText("");
        ui->line5->setText("");
        ui->line6->setText("");
    }else{
        ui->line1->setText(getLyricWord(index - 3));
        ui->line2->setText(getLyricWord(index - 2));
        ui->line3->setText(getLyricWord(index - 1));
        ui->lineCenter->setText(getLyricWord(index));
        ui->line4->setText(getLyricWord(index + 1));
        ui->line5->setText(getLyricWord(index + 2));
        ui->line6->setText(getLyricWord(index + 3));
    }
}

当歌曲发生切换时,需要完成lrc歌词⽂件的解析;当歌曲播放进度发生改变时,根据歌曲的当前播放时间,通过lrcPage找到对应行歌词并显示出来。

cpp 复制代码
// widget.cpp中新增

void Widget::onPositionChanged(qint64 duration)
{
    //...
	
	// 当歌曲的播放位置改变时同步歌词
    if (mediaPlayList->currentIndex() >= 0){
        lrcPage->showWord(duration);
    }
}
void Widget::onMetaDataAvailableChanged(bool available)
{
    //...

    // 同步歌词,当歌曲切换时加载歌词
    if (it != musicList.end()){
        lrcPage->parseLrc(it->getLyricFilePath());
    }

}
相关推荐
2601_958736991 小时前
虚拟电商新手怎么做?从零到第一单的完整流程
1024程序员节
wjkjpcba1 小时前
割草机器人主控板PCBA加工厂家怎么选?MCU贴装、传感器接口与检测
1024程序员节·smt贴片加工·pcba贴片加工厂
茉莉玫瑰花茶1 小时前
线性代数 [ 线性组合·张成的空间与基 ]
线性代数·math·1024程序员节
这是谁的博客?1 小时前
AI 芯片存算一体(PIM/CIM)架构深度解析:从冯·诺依曼瓶颈与近存计算到 RRAM 模拟矩阵乘的 LLM 推理显存墙突破原理
ai·pim·1024程序员节·cim·存算一体·hbm-pim·lpddr5x-pim
WebKoalaBoy1 小时前
一台电脑同时配置多个 Git 账号:从原理到实战
1024程序员节
啊哈一半醒1 小时前
Redis 三大缓存问题:缓存穿透、缓存击穿、缓存雪崩
运维·redis·1024程序员节
啵啵啵鱼1 小时前
用 Claude Code 重构遗留系统:从评估到落地的完整实践
数据结构·1024程序员节
I Am a robert girl1 小时前
当扩散编辑器“看不见”参考图:从 RefGAP 拆解参考注意力分配问题
计算机视觉·注意力机制·扩散模型·1024程序员节·参考引导编辑·refgap·免训练方法
jinbao_x1 小时前
关于git提交代码时的LF、CRLF提示的问题
1024程序员节