FFmpeg开发 把32位采样的MP3转换为16位的PCM音频完整指南

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音频了。这个转换器具有良好的错误处理、内存管理和扩展性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值