FFmpeg开发 把32位采样的MP3转换为16位的PCM音频完整指南
概览流程
环境准备 → FFmpeg配置 → 解码MP3 → 重采样转换 → 编码PCM → 验证输出
环境准备
FFmpeg开发环境配置
// 包含必要的头文件
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/samplefmt.h>
#include <libavutil/channel_layout.h>
#include <libswresample/swresample.h>
#ifdef __cplusplus
}
#endif
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
完整转换代码实现
1. MP3到16位PCM转换类
// mp3_to_pcm_converter.h
#ifndef MP3_TO_PCM_CONVERTER_H
#define MP3_TO_PCM_CONVERTER_H
#include <string>
class MP3ToPCMConverter {
private:
AVFormatContext* format_ctx;
AVCodecContext* codec_ctx;
SwrContext* swr_ctx;
int audio_stream_index;
public:
MP3ToPCMConverter();
~MP3ToPCMConverter();
bool convertMP3ToPCM(const std::string& input_file, const std::string& output_file);
void printAudioInfo();
private:
bool openInputFile(const std::string& filename);
bool openOutputFile(const std::string& filename);
bool setupResampler();
bool processAudioFrames(const std::string& output_file);
void cleanup();
};
#endif // MP3_TO_PCM_CONVERTER_H
// mp3_to_pcm_converter.cpp
#include "mp3_to_pcm_converter.h"
#include <fstream>
#include <iostream>
MP3ToPCMConverter::MP3ToPCMConverter()
: format_ctx(nullptr), codec_ctx(nullptr), swr_ctx(nullptr), audio_stream_index(-1) {
// 初始化FFmpeg库
avformat_network_init();
}
MP3ToPCMConverter::~MP3ToPCMConverter() {
cleanup();
avformat_network_deinit();
}
bool MP3ToPCMConverter::convertMP3ToPCM(const std::string& input_file, const std::string& output_file) {
std::cout << "开始转换: " << input_file << " -> " << output_file << std::endl;
// 打开输入文件
if (!openInputFile(input_file)) {
std::cerr << "打开输入文件失败" << std::endl;
return false;
}
// 打印音频信息
printAudioInfo();
// 设置重采样器
if (!setupResampler()) {
std::cerr << "设置重采样器失败" << std::endl;
return false;
}
// 处理音频帧并输出PCM
if (!processAudioFrames(output_file)) {
std::cerr << "处理音频帧失败" << std::endl;
return false;
}
std::cout << "转换完成!" << std::endl;
return true;
}
bool MP3ToPCMConverter::openInputFile(const std::string& filename) {
int ret;
// 打开输入文件
ret = avformat_open_input(&format_ctx, filename.c_str(), nullptr, nullptr);
if (ret < 0) {
char err_buf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, err_buf, sizeof(err_buf));
std::cerr << "无法打开文件: " << filename << ", 错误: " << err_buf << std::endl;
return false;
}
// 获取流信息
ret = avformat_find_stream_info(format_ctx, nullptr);
if (ret < 0) {
std::cerr << "无法获取流信息" << std::endl;
return false;
}
// 查找音频流
audio_stream_index = av_find_best_stream(format_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (audio_stream_index < 0) {
std::cerr << "未找到音频流" << std::endl;
return false;
}
// 获取解码器
AVStream* audio_stream = format_ctx->streams[audio_stream_index];
AVCodecParameters* codecpar = audio_stream->codecpar;
const AVCodec* decoder = avcodec_find_decoder(codecpar->codec_id);
if (!decoder) {
std::cerr << "未找到解码器" << std::endl;
return false;
}
// 创建解码器上下文
codec_ctx = avcodec_alloc_context3(decoder);
if (!codec_ctx) {
std::cerr << "无法分配解码器上下文" << std::endl;
return false;
}
// 复制参数到解码器上下文
ret = avcodec_parameters_to_context(codec_ctx, codecpar);
if (ret < 0) {
std::cerr << "无法复制编解码器参数" << std::endl;
return false;
}
// 打开解码器
ret = avcodec_open2(codec_ctx, decoder, nullptr);
if (ret < 0) {
std::cerr << "无法打开解码器" << std::endl;
return false;
}
return true;
}
void MP3ToPCMConverter::printAudioInfo() {
if (!codec_ctx) return;
std::cout << "音频信息:" << std::endl;
std::cout << " 编解码器: " << avcodec_get_name(codec_ctx->codec_id) << std::endl;
std::cout << " 采样率: " << codec_ctx->sample_rate << " Hz" << std::endl;
std::cout << " 声道数: " << codec_ctx->ch_layout.nb_channels << std::endl;
std::cout << " 采样格式: " << av_get_sample_fmt_name(codec_ctx->sample_fmt) << std::endl;
std::cout << " 比特率: " << codec_ctx->bit_rate / 1000 << " kbps" << std::endl;
}
bool MP3ToPCMConverter::setupResampler() {
if (!codec_ctx) return false;
// 创建重采样上下文
swr_ctx = swr_alloc();
if (!swr_ctx) {
std::cerr << "无法分配重采样上下文" << std::endl;
return false;
}
// 设置输入参数(MP3解码后的格式)
av_opt_set_int(swr_ctx, "in_channel_layout", codec_ctx->ch_layout.u.mask, 0);
av_opt_set_int(swr_ctx, "in_sample_rate", codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", codec_ctx->sample_fmt, 0);
// 设置输出参数(16位PCM)
av_opt_set_int(swr_ctx, "out_channel_layout", codec_ctx->ch_layout.u.mask, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
// 初始化重采样上下文
int ret = swr_init(swr_ctx);
if (ret < 0) {
std::cerr << "初始化重采样上下文失败" << std::endl;
return false;
}
return true;
}
bool MP3ToPCMConverter::processAudioFrames(const std::string& output_file) {
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
std::ofstream output_stream(output_file, std::ios::binary);
if (!packet || !frame || !output_stream.is_open()) {
std::cerr << "分配内存或打开输出文件失败" << std::endl;
av_packet_free(&packet);
av_frame_free(&frame);
return false;
}
int64_t total_samples = 0;
// 读取和处理音频包
while (av_read_frame(format_ctx, packet) >= 0) {
if (packet->stream_index == audio_stream_index) {
// 发送包到解码器
int ret = avcodec_send_packet(codec_ctx, packet);
if (ret < 0) {
std::cerr << "发送包到解码器失败" << std::endl;
av_packet_unref(packet);
continue;
}
// 接收解码后的帧
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
std::cerr << "接收解码帧失败" << std::endl;
break;
}
// 重采样转换为16位
uint8_t** converted_data = nullptr;
int converted_linesize = 0;
// 分配输出缓冲区
int ret = av_samples_alloc_array_and_samples(
&converted_data, &converted_linesize,
frame->ch_layout.nb_channels,
frame->nb_samples,
AV_SAMPLE_FMT_S16, 0);
if (ret < 0) {
std::cerr << "分配输出缓冲区失败" << std::endl;
av_frame_unref(frame);
av_packet_unref(packet);
continue;
}
// 执行重采样
ret = swr_convert(
swr_ctx,
converted_data, frame->nb_samples,
(const uint8_t**)frame->data, frame->nb_samples);
if (ret > 0) {
// 写入PCM数据到文件
int data_size = av_samples_get_buffer_size(
nullptr, frame->ch_layout.nb_channels,
ret, AV_SAMPLE_FMT_S16, 1);
output_stream.write(reinterpret_cast<const char*>(converted_data[0]), data_size);
total_samples += ret;
}
// 释放输出缓冲区
if (converted_data) {
av_freep(&converted_data[0]);
av_freep(&converted_data);
}
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
// 刷新解码器
int ret = avcodec_send_packet(codec_ctx, nullptr);
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
break;
}
// 重采样和写入(同上)
uint8_t** converted_data = nullptr;
int converted_linesize = 0;
int ret_convert = av_samples_alloc_array_and_samples(
&converted_data, &converted_linesize,
frame->ch_layout.nb_channels,
frame->nb_samples,
AV_SAMPLE_FMT_S16, 0);
if (ret_convert >= 0) {
ret_convert = swr_convert(
swr_ctx,
converted_data, frame->nb_samples,
(const uint8_t**)frame->data, frame->nb_samples);
if (ret_convert > 0) {
int data_size = av_samples_get_buffer_size(
nullptr, frame->ch_layout.nb_channels,
ret_convert, AV_SAMPLE_FMT_S16, 1);
output_stream.write(reinterpret_cast<const char*>(converted_data[0]), data_size);
total_samples += ret_convert;
}
if (converted_data) {
av_freep(&converted_data[0]);
av_freep(&converted_data);
}
}
av_frame_unref(frame);
}
std::cout << "总共处理了 " << total_samples << " 个采样点" << std::endl;
std::cout << "输出文件大小: " << output_stream.tellp() << " 字节" << std::endl;
// 清理资源
output_stream.close();
av_packet_free(&packet);
av_frame_free(&frame);
return true;
}
void MP3ToPCMConverter::cleanup() {
if (swr_ctx) {
swr_free(&swr_ctx);
}
if (codec_ctx) {
avcodec_free_context(&codec_ctx);
}
if (format_ctx) {
avformat_close_input(&format_ctx);
}
}
2. 主函数实现
// main.cpp
#include "mp3_to_pcm_converter.h"
#include <iostream>
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "用法: " << argv[0] << " <输入MP3文件> <输出PCM文件>" << std::endl;
std::cout << "示例: " << argv[0] << " input.mp3 output.pcm" << std::endl;
return 1;
}
std::string input_file = argv[1];
std::string output_file = argv[2];
MP3ToPCMConverter converter;
if (converter.convertMP3ToPCM(input_file, output_file)) {
std::cout << "转换成功!" << std::endl;
return 0;
} else {
std::cout << "转换失败!" << std::endl;
return 1;
}
}
使用FFmpeg命令行验证
1. 命令行转换验证
# 使用FFmpeg命令行验证转换结果
# 转换MP3到16位PCM
ffmpeg.exe -i input.mp3 -f s16le -acodec pcm_s16le output_ffmpeg.pcm
# 比较两个PCM文件
# 可以使用文件比较工具或计算MD5校验和
certutil -hashfile output.pcm MD5
certutil -hashfile output_ffmpeg.pcm MD5
2. PCM文件信息查看
# 查看PCM文件信息
ffmpeg.exe -f s16le -ar 44100 -ac 2 -i output.pcm -f null -
Visual Studio项目配置
1. 项目属性配置
// 项目属性设置:
// C/C++ -> General -> Additional Include Directories
// 如果使用vcpkg:
C:\vcpkg\installed\x64-windows\include
// C/C++ -> Preprocessor -> Preprocessor Definitions
_CRT_SECURE_NO_WARNINGS
// Linker -> General -> Additional Library Directories
// 如果使用vcpkg:
C:\vcpkg\installed\x64-windows\lib
// Linker -> Input -> Additional Dependencies
avformat.lib
avcodec.lib
avutil.lib
swresample.lib
2. CMake配置文件
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MP3ToPCMConverter)
set(CMAKE_CXX_STANDARD 17)
# FFmpeg配置
find_package(PkgConfig REQUIRED)
# 或者直接指定路径
set(FFMPEG_ROOT "C:/vcpkg/installed/x64-windows")
# 包含目录
include_directories(${FFMPEG_ROOT}/include)
# 库目录
link_directories(${FFMPEG_ROOT}/lib)
# 源文件
set(SOURCES
main.cpp
mp3_to_pcm_converter.cpp
)
# 可执行文件
add_executable(${PROJECT_NAME} ${SOURCES})
# 链接库
target_link_libraries(${PROJECT_NAME}
avformat
avcodec
avutil
swresample
)
# 复制DLL文件到输出目录
file(GLOB FFMPEG_DLLS "${FFMPEG_ROOT}/bin/*.dll")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${FFMPEG_DLLS}
$<TARGET_FILE_DIR:${PROJECT_NAME}>
)
PCM文件格式说明
1. 16位PCM格式特点
// 16位PCM格式说明:
// - 每个采样点使用16位(2字节)有符号整数表示
// - 数据按小端序存储
// - 单声道:采样点连续存储
// - 立体声:左右声道交替存储 (L-R-L-R...)
// - 无文件头信息
// 计算文件大小:
// 文件大小 = 采样率 × 声道数 × 2字节 × 时间(秒)
// 例如:44.1kHz, 立体声, 10秒 = 44100 × 2 × 2 × 10 = 1,764,000 字节
2. PCM数据读取示例
// pcm_reader.cpp - PCM文件读取示例
#include <iostream>
#include <fstream>
#include <vector>
class PCMReader {
public:
static bool readPCMInfo(const std::string& filename, int sample_rate, int channels) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
std::cerr << "无法打开文件: " << filename << std::endl;
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
// 计算采样点数
int bytes_per_sample = 2; // 16位 = 2字节
int64_t total_samples = file_size / (channels * bytes_per_sample);
double duration = static_cast<double>(total_samples) / sample_rate;
std::cout << "PCM文件信息:" << std::endl;
std::cout << " 文件名: " << filename << std::endl;
std::cout << " 文件大小: " << file_size << " 字节" << std::endl;
std::cout << " 采样率: " << sample_rate << " Hz" << std::endl;
std::cout << " 声道数: " << channels << std::endl;
std::cout << " 采样点数: " << total_samples << std::endl;
std::cout << " 时长: " << duration << " 秒" << std::endl;
// 读取前几个采样点
std::vector<int16_t> samples(std::min(10, static_cast<int>(total_samples)));
file.read(reinterpret_cast<char*>(samples.data()), samples.size() * sizeof(int16_t));
std::cout << " 前10个采样点: ";
for (size_t i = 0; i < samples.size(); ++i) {
std::cout << samples[i] << " ";
}
std::cout << std::endl;
return true;
}
};
// 使用示例
int main() {
PCMReader::readPCMInfo("output.pcm", 44100, 2);
return 0;
}
高级功能扩展
1. 支持不同采样率输出
// 添加采样率转换功能
bool MP3ToPCMConverter::setupResamplerWithRate(int target_sample_rate) {
if (!codec_ctx) return false;
swr_ctx = swr_alloc();
if (!swr_ctx) {
std::cerr << "无法分配重采样上下文" << std::endl;
return false;
}
// 设置输入参数
av_opt_set_int(swr_ctx, "in_channel_layout", codec_ctx->ch_layout.u.mask, 0);
av_opt_set_int(swr_ctx, "in_sample_rate", codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", codec_ctx->sample_fmt, 0);
// 设置输出参数(支持采样率转换)
av_opt_set_int(swr_ctx, "out_channel_layout", codec_ctx->ch_layout.u.mask, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", target_sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
int ret = swr_init(swr_ctx);
if (ret < 0) {
std::cerr << "初始化重采样上下文失败" << std::endl;
return false;
}
return true;
}
2. 支持声道数转换
// 添加声道转换功能
bool MP3ToPCMConverter::setupResamplerWithChannels(int target_channels) {
if (!codec_ctx) return false;
swr_ctx = swr_alloc();
if (!swr_ctx) {
std::cerr << "无法分配重采样上下文" << std::endl;
return false;
}
// 设置输入参数
av_opt_set_int(swr_ctx, "in_channel_layout", codec_ctx->ch_layout.u.mask, 0);
av_opt_set_int(swr_ctx, "in_sample_rate", codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", codec_ctx->sample_fmt, 0);
// 设置输出参数(支持声道转换)
uint64_t out_channel_layout = (target_channels == 1) ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
av_opt_set_int(swr_ctx, "out_channel_layout", out_channel_layout, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
int ret = swr_init(swr_ctx);
if (ret < 0) {
std::cerr << "初始化重采样上下文失败" << std::endl;
return false;
}
return true;
}
错误处理和调试
1. 详细的错误处理
// 错误处理工具函数
std::string getFFmpegError(int error_code) {
char err_buf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(error_code, err_buf, sizeof(err_buf));
return std::string(err_buf);
}
// 在关键位置添加错误检查
int ret = avformat_open_input(&format_ctx, filename.c_str(), nullptr, nullptr);
if (ret < 0) {
std::cerr << "无法打开文件: " << filename
<< ", 错误码: " << ret
<< ", 错误信息: " << getFFmpegError(ret) << std::endl;
return false;
}
2. 调试信息输出
// 添加调试输出
#ifdef _DEBUG
#define DEBUG_LOG(x) std::cout << "[DEBUG] " << x << std::endl
#else
#define DEBUG_LOG(x)
#endif
// 在关键位置添加调试信息
DEBUG_LOG("打开输入文件: " << filename);
DEBUG_LOG("找到音频流索引: " << audio_stream_index);
DEBUG_LOG("解码器已打开,采样率: " << codec_ctx->sample_rate);
性能优化建议
1. 内存管理优化
// 使用缓冲区重用减少内存分配
class AudioBufferManager {
private:
std::vector<uint8_t*> buffers;
std::vector<size_t> buffer_sizes;
public:
uint8_t* getBuffer(size_t required_size) {
// 查找合适大小的缓冲区或创建新缓冲区
for (size_t i = 0; i < buffer_sizes.size(); ++i) {
if (buffer_sizes[i] >= required_size) {
return buffers[i];
}
}
// 创建新缓冲区
uint8_t* new_buffer = static_cast<uint8_t*>(av_malloc(required_size));
buffers.push_back(new_buffer);
buffer_sizes.push_back(required_size);
return new_buffer;
}
~AudioBufferManager() {
for (uint8_t* buffer : buffers) {
av_freep(&buffer);
}
}
};
2. 多线程处理
// 简单的多线程处理框架
#include <thread>
#include <queue>
#include <mutex>
class ThreadedAudioProcessor {
private:
std::queue<AVFrame*> frame_queue;
std::mutex queue_mutex;
bool processing_finished;
public:
void processFramesAsync() {
std::thread processing_thread([this]() {
while (!processing_finished || !frame_queue.empty()) {
AVFrame* frame = nullptr;
{
std::lock_guard<std::mutex> lock(queue_mutex);
if (!frame_queue.empty()) {
frame = frame_queue.front();
frame_queue.pop();
}
}
if (frame) {
// 处理音频帧
processAudioFrame(frame);
av_frame_unref(frame);
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
processing_thread.join();
}
};
测试用例
// test_converter.cpp
#include "mp3_to_pcm_converter.h"
#include <cassert>
#include <filesystem>
void testFileConversion() {
MP3ToPCMConverter converter;
// 测试正常文件转换
bool result = converter.convertMP3ToPCM("test_input.mp3", "test_output.pcm");
assert(result == true);
// 检查输出文件是否存在
assert(std::filesystem::exists("test_output.pcm") == true);
// 检查输出文件大小是否合理
auto file_size = std::filesystem::file_size("test_output.pcm");
assert(file_size > 0);
std::cout << "文件转换测试通过!" << std::endl;
}
void testInvalidFile() {
MP3ToPCMConverter converter;
// 测试无效文件
bool result = converter.convertMP3ToPCM("nonexistent.mp3", "output.pcm");
assert(result == false);
std::cout << "无效文件测试通过!" << std::endl;
}
int main() {
testFileConversion();
testInvalidFile();
std::cout << "所有测试通过!" << std::endl;
return 0;
}
完成这些配置后,你就可以成功将32位采样的MP3音频转换为16位的PCM音频了。这个转换器具有良好的错误处理、内存管理和扩展性。