一、 介绍 使用ffmpeg 4.2.2
在数字化浪潮席卷全球的当下,音视频内容犹如璀璨繁星,点亮了人们的生活与工作。从短视频平台上令人捧腹的搞笑视频,到在线课堂中知识渊博的专家授课,再到影视平台上扣人心弦的高清大片,音视频以其直观、生动的特性,成为信息传播与娱乐休闲的重要媒介。而在这繁华音视频世界的幕后,有一位低调而强大的“魔法工匠”——FFmpeg。
FFmpeg 是一款声名远扬的开源音视频处理工具集,凭借其跨平台、功能强大等显著优势,在音视频领域占据着不可撼动的地位。它就像一个拥有无数神奇工具的百宝箱,集音视频的录制、编解码、转换、流媒体传输等众多功能于一身,为音视频内容的创作、传播与消费提供了坚实的技术支撑。
对于开发者来说,FFmpeg 宛如一座取之不尽的宝藏。它提供了丰富且易用的 API 接口,能够无缝集成到各类应用程序中。无论是开发一个简洁实用的音视频播放器,还是打造一个功能全面的专业视频编辑软件,FFmpeg 都能凭借其强大的功能和高效的性能,助力开发者快速实现目标,大大降低开发的难度和成本。
在科研领域,FFmpeg 也发挥着重要作用。研究人员可以利用其先进的编解码算法,探索音视频质量优化的新途径;借助其流媒体传输功能,开展网络带宽利用、视频传输延迟等方面的研究,为提升音视频在不同网络环境下的播放体验提供理论依据和技术支持。
此外,FFmpeg 的开源属性更是为其发展注入了源源不断的活力。全球的开发者们汇聚于此,共同参与项目的开发与维护,不断为其增添新功能、修复漏洞,使得 FFmpeg 始终保持着与时俱进的姿态,适应不断变化的音视频市场需求。
下面为你详细介绍使用 FFmpeg 库在 C++ 里播放 MP3 文件的代码流程,整个流程主要包含初始化、打开文件、查找流信息、查找解码器、打开解码器、重采样、播放音频以及资源释放等步骤。
- 初始化 FFmpeg 库
在使用 FFmpeg 库的功能之前,需要对其进行初始化,主要是注册所有可用的编解码器、格式和协议。
#include <iostream>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
}// 初始化 FFmpeg 库
avformat_network_init();
av_register_all();
- 打开输入文件
使用 avformat_open_input 函数打开 MP3 文件,同时创建 AVFormatContext 结构体来保存文件的格式信息。
AVFormatContext* formatContext = nullptr;
const char* filePath = "example.mp3";
if (avformat_open_input(&formatContext, filePath, nullptr, nullptr) != 0) {std::cerr << "Could not open input file" << std::endl;return -1;
}
- 获取流信息
调用 avformat_find_stream_info 函数获取文件的流信息,例如音频流、视频流等。
if (avformat_find_stream_info(formatContext, nullptr) < 0) {std::cerr << "Could not find stream information" << std::endl;avformat_close_input(&formatContext);return -1;
}
- 查找音频流
通过 av_find_best_stream 函数在文件中查找音频流。
int audioStreamIndex = -1;
AVCodecParameters* codecParameters = nullptr;
for (unsigned int i = 0; i < formatContext->nb_streams; i++) {if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {audioStreamIndex = i;codecParameters = formatContext->streams[i]->codecpar;break;}
}
if (audioStreamIndex == -1) {std::cerr << "Could not find audio stream" << std::endl;avformat_close_input(&formatContext);return -1;
}
- 查找并打开解码器
使用 avcodec_find_decoder 函数查找对应的音频解码器,再用 avcodec_open2 函数打开解码器。
AVCodec* codec = avcodec_find_decoder(codecParameters->codec_id);
if (!codec) {std::cerr << "Could not find codec" << std::endl;avformat_close_input(&formatContext);return -1;
}AVCodecContext* codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {std::cerr << "Could not allocate codec context" << std::endl;avformat_close_input(&formatContext);return -1;
}if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {std::cerr << "Could not copy codec parameters to context" << std::endl;avcodec_free_context(&codecContext);avformat_close_input(&formatContext);return -1;
}if (avcodec_open2(codecContext, codec, nullptr) < 0) {std::cerr << "Could not open codec" << std::endl;avcodec_free_context(&codecContext);avformat_close_input(&formatContext);return -1;
}
- 初始化重采样上下文
由于 MP3 文件的音频格式可能与系统播放设备支持的格式不一致,需要使用 swr_alloc_set_opts 函数进行重采样。
SwrContext* swrContext = swr_alloc_set_opts(nullptr,AV_CH_LAYOUT_STEREO, // 输出声道布局AV_SAMPLE_FMT_S16, // 输出样本格式codecContext->sample_rate, // 输出采样率codecContext->channel_layout, // 输入声道布局codecContext->sample_fmt, // 输入样本格式codecContext->sample_rate, // 输入采样率0, nullptr);if (!swrContext || swr_init(swrContext) < 0) {std::cerr << "Could not initialize resampler" << std::endl;avcodec_free_context(&codecContext);avformat_close_input(&formatContext);return -1;
}
- 读取、解码和重采样音频数据
使用 av_read_frame 函数读取音频数据包,再用 avcodec_send_packet 和 avcodec_receive_frame 函数进行解码,最后使用 swr_convert 函数进行重采样。
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();while (av_read_frame(formatContext, packet) >= 0) {if (packet->stream_index == audioStreamIndex) {if (avcodec_send_packet(codecContext, packet) < 0) {std::cerr << "Error sending packet to decoder" << std::endl;continue;}while (avcodec_receive_frame(codecContext, frame) == 0) {// 重采样uint8_t* outputData[1];int outputSamples = swr_convert(swrContext, outputData, frame->nb_samples,(const uint8_t**)frame->data, frame->nb_samples);// 这里需要将重采样后的数据发送到音频设备进行播放// 不同系统的音频播放 API 不同,例如在 Windows 下可以使用 WaveOut 或 WASAPI}}av_packet_unref(packet);
}
- 释放资源
在音频播放结束后,需要释放之前分配的所有资源。
av_frame_free(&frame);
av_packet_free(&packet);
swr_free(&swrContext);
avcodec_free_context(&codecContext);
avformat_close_input(&formatContext);
整个播放 MP3 文件的流程可以概括为:初始化 FFmpeg 库 → 打开输入文件 → 获取流信息 → 查找音频流 → 查找并打开解码器 → 初始化重采样上下文 → 读取、解码和重采样音频数据 → 释放资源。需要注意的是,上述代码中重采样后的数据需要使用相应系统的音频播放 API 发送到音频设备进行播放,不同操作系统的音频播放 API 不同,例如 Windows 下的 WaveOut、WASAPI,Linux 下的 ALSA、PulseAudio 等。
二、效果
三、读取歌词类
#pragma once#include <QList>
#include <QString>// 歌词条目结构体
struct stLyricsEntry {int m_nTime; // 时间,单位为毫秒std::string m_strText; // 歌词文本
};class CLyricsReader {
public:CLyricsReader() = default;bool LoadLrcFile(const QString& strFilePath);std::string GetCurrentLyrics(int nCurrentTime);private:QList<stLyricsEntry> m_lstLyricsEntries; // 歌词条目列表
};#include "../Include/LyricsReader.h"
#include "QtGui/Include/Conversion.h"
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QTextCodec>
#include <QDebug>bool CLyricsReader::LoadLrcFile(const QString& strFilePath)
{QFile file(strFilePath);if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {qDebug() << "Failed to open LRC file:" << strFilePath;return false;}QByteArray data = file.readAll();QTextCodec* codec = nullptr;if (QTextCodec::codecForUtfText(data)) {codec = QTextCodec::codecForName("UTF-8");}else {// 尝试其他常见编码,如 GBKcodec = QTextCodec::codecForLocale();}QString text = codec->toUnicode(data);QTextStream in(&text);QRegularExpression timeRegex(R"(\[(\d+):(\d+\.\d+)\])");while (!in.atEnd()) {QString line = in.readLine();QRegularExpressionMatchIterator matchIterator = timeRegex.globalMatch(line);while (matchIterator.hasNext()){QRegularExpressionMatch match = matchIterator.next();int minutes = match.captured(1).toInt();double seconds = match.captured(2).toDouble();int time = static_cast<int>((minutes * 60 + seconds) * 1000);QString text = line.mid(match.capturedEnd());if (!text.isEmpty()) {stLyricsEntry entry;entry.m_nTime = time;entry.m_strText = TransUnicode2String(text);m_lstLyricsEntries.append(entry);}}}file.close();// 按时间排序std::sort(m_lstLyricsEntries.begin(), m_lstLyricsEntries.end(), [](const stLyricsEntry& a, const stLyricsEntry& b) {return a.m_nTime < b.m_nTime;});return true;
}std::string CLyricsReader::GetCurrentLyrics(int nCurrentTime)
{for (int i = m_lstLyricsEntries.size() - 1; i >= 0; --i) {if (m_lstLyricsEntries[i].m_nTime<= nCurrentTime){return m_lstLyricsEntries[i].m_strText;}}return "";
}
三、播放类
#include "../Include/AudioPlayer.h"
#include "QtGui/Include/Conversion.h"#include <QIODevice>
#include <QBuffer>
#include <QDebug>extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
}CAudioPlayer::CAudioPlayer(QObject* parent): QThread(parent)
{m_eControlType = E_CONTROL_NONE;m_pAudioOutput = nullptr;m_bRun = false;m_nSeekMs = 0;
}CAudioPlayer::~CAudioPlayer()
{}void CAudioPlayer::Quit()
{m_bRun = false;wait();
}void CAudioPlayer::Play(const std::string& strFilePath)
{m_strFilePath = strFilePath;m_eControlType = E_CONTROL_PLAY;if (!isRunning()){m_bRun = true;start();}
}void CAudioPlayer::Stop()
{if (isRunning()){m_eControlType = E_CONTROL_STOP;}
}void CAudioPlayer::Pause()
{if (isRunning()){m_eControlType = E_CONTROL_PAUSE;}
}void CAudioPlayer::Resume()
{if (isRunning()){m_eControlType = E_CONTROL_RESUME;}
}void CAudioPlayer::Seek(int nMs)
{if (isRunning()){m_eControlType = E_CONTROL_SEEK;m_nSeekMs = nMs;}
}void CAudioPlayer::run()
{if (!InitAudioOutput(44100)){qDebug() << "InitAudioOutput failed";return;}while (m_bRun){switch (m_eControlType){case E_CONTROL_NONE:{msleep(20);}break;case E_CONTROL_PLAY:{m_eControlType = E_CONTROL_NONE;RunPlay();}break;default:m_eControlType = E_CONTROL_NONE;break;}}
}bool CAudioPlayer::InitAudioOutput(int nSampleRate)
{if (m_pAudioOutput){return false;}QAudioFormat format;format.setSampleRate(nSampleRate);format.setChannelCount(2);format.setSampleSize(16);format.setCodec("audio/pcm");format.setByteOrder(QAudioFormat::LittleEndian);format.setSampleType(QAudioFormat::SignedInt);QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());if (!info.isFormatSupported(format)){qDebug() << "Raw audio format not supported by backend, cannot play audio.";return false;}m_pAudioOutput = std::make_unique<QAudioOutput>(format);m_pAudioOutput->setBufferSize(100000);return true;
}bool CAudioPlayer::CheckControlType()
{if (!m_pAudioOutput){return false;}bool bRet = false;if (m_eControlType == E_CONTROL_PAUSE){while (m_eControlType == E_CONTROL_PAUSE){m_pAudioOutput->suspend();msleep(20);}if (m_eControlType == E_CONTROL_RESUME){m_pAudioOutput->resume();}}if (m_eControlType == E_CONTROL_PLAY){bRet = true;if (m_pAudioOutput->state() == QAudio::StoppedState){m_pAudioOutput->stop();}}if (m_eControlType == E_CONTROL_STOP){bRet = true;if (m_pAudioOutput->state() == QAudio::ActiveState){m_pAudioOutput->stop();}}return bRet;
}void CAudioPlayer::DebugError(int nError)
{char cError[1024] = { 0 };av_strerror(nError, cError, sizeof(cError));qDebug() << "Error:" << cError;
}void CAudioPlayer::RunPlay()
{int nRet = 0;int nDestMs = 0;int nCurMs = 0;if (!m_pAudioOutput){qDebug() << "m_pAudioOutput is nullptr";return;}av_log_set_level(AV_LOG_ERROR);avformat_network_init();AVFormatContext* pAvFormatContext = nullptr;if ((nRet = avformat_open_input(&pAvFormatContext, m_strFilePath.c_str(), nullptr, nullptr)) != 0){qDebug() << "Could not open input file";return;}AVDictionary* pOpts = nullptr;av_dict_set(&pOpts, "analyzeduration", "2147483647", 0); // 设置最大分析时长av_dict_set(&pOpts, "probesize", "2147483647", 0); // 设置最大探测大小if ((nRet = avformat_find_stream_info(pAvFormatContext, &pOpts)) < 0){qDebug() << "Could not find stream information";av_dict_free(&pOpts);avformat_close_input(&pAvFormatContext);return;}av_dict_free(&pOpts);int nAudioStreamIndex = -1;nAudioStreamIndex = av_find_best_stream(pAvFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);if (nAudioStreamIndex < 0){qDebug() << "Could not find audio stream";avformat_close_input(&pAvFormatContext);return;}qDebug() << "Audio stream index:" << nAudioStreamIndex;AVCodec *pCodec = avcodec_find_decoder(pAvFormatContext->streams[nAudioStreamIndex]->codecpar->codec_id);AVCodecContext* pCodecContext = avcodec_alloc_context3(pCodec);avcodec_parameters_to_context(pCodecContext, pAvFormatContext->streams[nAudioStreamIndex]->codecpar);nRet = avcodec_open2(pCodecContext, pCodec, nullptr);if (nRet < 0){qDebug() << "Could not open codec";avcodec_free_context(&pCodecContext);avformat_close_input(&pAvFormatContext);return;}SwrContext *pSwrContext = nullptr;pSwrContext = swr_alloc_set_opts(nullptr, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, pCodecContext->sample_rate,pCodecContext->channel_layout, pCodecContext->sample_fmt, pCodecContext->sample_rate, 0, nullptr);swr_init(pSwrContext);nDestMs = av_q2d(pAvFormatContext->streams[nAudioStreamIndex]->time_base) * 1000 * pAvFormatContext->streams[nAudioStreamIndex]->duration;qDebug() << TransString2Unicode("码率:") << pCodecContext->bit_rate;qDebug() << TransString2Unicode("格式:") << pCodecContext->sample_fmt;qDebug() << TransString2Unicode("通道:") << pCodecContext->channels;qDebug() << TransString2Unicode("采样率:") << pCodecContext->sample_rate;qDebug() << TransString2Unicode("时长:") << nDestMs;qDebug() << TransString2Unicode("解码器:") << pCodec->name;Q_EMIT SigDuration(nCurMs, nDestMs);AVPacket* pAvPacket = av_packet_alloc();AVFrame* pAvFrame = av_frame_alloc();m_pAudioOutput->stop();QIODevice* pAudioDevice = m_pAudioOutput->start();while (1){if (CheckControlType()){break;}if (m_eControlType == CAudioPlayer::E_CONTROL_SEEK){av_seek_frame(pAvFormatContext, nAudioStreamIndex, m_nSeekMs / (double)1000.0 / av_q2d(pAvFormatContext->streams[nAudioStreamIndex]->time_base), AVSEEK_FLAG_BACKWARD);m_eControlType = E_CONTROL_NONE;Q_EMIT SigSeekOk();}nRet = av_read_frame(pAvFormatContext, pAvPacket);if (nRet == AVERROR_EOF){// 到达文件末尾,向解码器发送刷新信号nRet = avcodec_send_packet(pCodecContext, nullptr);if (nRet < 0){DebugError(nRet);qDebug() << "Could not send flush packet";}while (nRet >= 0){nRet = avcodec_receive_frame(pCodecContext, pAvFrame);if (nRet == AVERROR(EAGAIN) || nRet == AVERROR_EOF){break;}else if (nRet < 0){DebugError(nRet);break;}// 处理剩余的帧uint8_t* pData[2] = { 0 };int nByteCnt = pAvFrame->nb_samples * 2 * 2;std::unique_ptr<uint8_t[]> pDestBuf(new uint8_t[nByteCnt]);pData[0] = pDestBuf.get();nRet = swr_convert(pSwrContext, &pData[0], pAvFrame->nb_samples,(const uint8_t**)(pAvFrame->data), pAvFrame->nb_samples);while (m_pAudioOutput->bytesFree() < nByteCnt){if (CheckControlType()){break;}msleep(10);}if (!CheckControlType()){pAudioDevice->write((const char*)pDestBuf.get(), nByteCnt);}nCurMs = av_q2d(pAvFormatContext->streams[nAudioStreamIndex]->time_base) * 1000 * pAvFrame->pts;Q_EMIT SigDuration(nCurMs, nDestMs);}qDebug() << "End of file reached";SigDuration(nDestMs, nDestMs);break;}else if (nRet < 0){DebugError(nRet);qDebug() << "Could not read frame";SigDuration(nDestMs, nDestMs);av_packet_unref(pAvPacket);break;}if (pAvPacket->stream_index == nAudioStreamIndex){if (CheckControlType()){av_packet_unref(pAvPacket);break;}// 发送 AVPacket 到解码器nRet = avcodec_send_packet(pCodecContext, pAvPacket);av_packet_unref(pAvPacket); // 发送后释放 AVPacketif (nRet < 0){DebugError(nRet);qDebug() << "Could not send packet";continue;}// 从解码器接收 AVFramewhile (true){nRet = avcodec_receive_frame(pCodecContext, pAvFrame);if (nRet == AVERROR(EAGAIN) || nRet == AVERROR_EOF){break;}else if (nRet < 0){DebugError(nRet);break;}uint8_t* pData[2] = { 0 };int nByteCnt = pAvFrame->nb_samples * 2 * 2;std::unique_ptr<uint8_t[]> pDestBuf(new uint8_t[nByteCnt]);pData[0] = pDestBuf.get();nRet = swr_convert(pSwrContext, &pData[0], pAvFrame->nb_samples,(const uint8_t**)(pAvFrame->data), pAvFrame->nb_samples);while (m_pAudioOutput->bytesFree() < nByteCnt){if (CheckControlType()){break;}msleep(10);}if (!CheckControlType()){pAudioDevice->write((const char*)pDestBuf.get(), nByteCnt);}nCurMs = av_q2d(pAvFormatContext->streams[nAudioStreamIndex]->time_base) * 1000 * pAvFrame->pts;Q_EMIT SigDuration(nCurMs, nDestMs);}}else{av_packet_unref(pAvPacket); // 非音频包,释放}}av_frame_free(&pAvFrame);av_packet_free(&pAvPacket);swr_free(&pSwrContext);avcodec_free_context(&pCodecContext);avformat_close_input(&pAvFormatContext);
}
四、主界面类
#include "../Include/EMusicMainWindow.h"
#include "ui_EMusicMainWindow.h"
#include "QtGui/Include/Conversion.h"
#include <QFileDialog>
#include <QThread>
#include <QDebug>CEMusicMainWindow::CEMusicMainWindow(QWidget* parent): QMainWindow(parent), ui(std::make_unique<Ui::CEMusicMainWindow>())
{ui->setupUi(this);InitUI();
}CEMusicMainWindow::~CEMusicMainWindow()
{m_AudioPlayer.Stop();m_AudioPlayer.Quit();
}void CEMusicMainWindow::InitUI()
{m_bIsPlaying = false;m_bSeeking = false;connect(&m_AudioPlayer, &CAudioPlayer::SigDuration, this, &CEMusicMainWindow::SlotUpdateLyricsAndTime);connect(&m_AudioPlayer, &CAudioPlayer::SigSeekOk, this, &CEMusicMainWindow::SlotSeekOk);
}void CEMusicMainWindow::on_pushButton_StopOrPlay_clicked()
{if (m_strMusicFilePath.isEmpty()){return;}if (m_bIsPlaying){m_bIsPlaying = false;m_AudioPlayer.Pause();ui->pushButton_StopOrPlay->setStyleSheet("image: url(:/Play.png);");}else{m_bIsPlaying = true;m_bSeeking = false;ui->pushButton_StopOrPlay->setStyleSheet("image: url(:/Stop.png);");if (m_strOldMusicFilePath.isEmpty()){m_strOldMusicFilePath = m_strMusicFilePath;m_AudioPlayer.Play(TransUnicode2String(m_strMusicFilePath));}else if (m_strMusicFilePath != m_strOldMusicFilePath){m_strOldMusicFilePath = m_strMusicFilePath;m_AudioPlayer.Play(TransUnicode2String(m_strMusicFilePath));}else if(m_AudioPlayer.GetControlType() == CAudioPlayer::E_CONTROL_PAUSE){m_AudioPlayer.Resume();}else {m_strOldMusicFilePath = m_strMusicFilePath;m_AudioPlayer.Play(TransUnicode2String(m_strMusicFilePath));}}
}void CEMusicMainWindow::SlotUpdateLyricsAndTime(int nCurrentTime, int nDestTime)
{std::string strLyrics = m_LyricsReader.GetCurrentLyrics(nCurrentTime);ui->label_Words->setText(TransString2Unicode(strLyrics));if (nCurrentTime == nDestTime && nCurrentTime != 0){m_bIsPlaying = false;ui->pushButton_StopOrPlay->setStyleSheet("image: url(:/Play.png);");}static int currentMs1 = -1, destMs1 = -1;if (currentMs1 == nCurrentTime && destMs1 == nDestTime){return;}currentMs1 = nCurrentTime;destMs1 = nDestTime;//qDebug() << "onDuration:" << nCurrentTime << nDestTime << m_bSeeking;QString currentTime = QString("%1:%2:%3").arg(currentMs1 / 360000 % 60, 2, 10, QChar('0')).arg(currentMs1 / 6000 % 60, 2, 10, QChar('0')).arg(currentMs1 / 1000 % 60, 2, 10, QChar('0'));QString destTime = QString("%1:%2:%3").arg(destMs1 / 360000 % 60, 2, 10, QChar('0')).arg(destMs1 / 6000 % 60, 2, 10, QChar('0')).arg(destMs1 / 1000 % 60, 2, 10, QChar('0'));ui->label_Process->setText(currentTime + "/" + destTime);if (!m_bSeeking) //未滑动{ui->slider_Seek->setMaximum(nDestTime);ui->slider_Seek->setValue(nCurrentTime);}
}void CEMusicMainWindow::SlotSeekOk()
{m_bSeeking = false;
}void CEMusicMainWindow::on_slider_Seek_sliderPressed()
{m_bSeeking = true;
}void CEMusicMainWindow::on_slider_Seek_sliderReleased()
{m_AudioPlayer.Seek(ui->slider_Seek->value());
}void CEMusicMainWindow::on_pushButton_Select_clicked()
{m_strMusicFilePath = QFileDialog::getOpenFileName(this, TransString2Unicode("选择音乐文件"), "", "Music Files (*.mp3)");if (!m_strMusicFilePath.isEmpty()){m_LyricsReader.LoadLrcFile(m_strMusicFilePath.left(m_strMusicFilePath.lastIndexOf('.')) + ".lrc");}on_pushButton_StopOrPlay_clicked();
}
五、后续完善
完成其他功能