Qt + FFmpeg 音频工具:重采样实现

前言

重采样是音频处理流水线中的基础能力------不同来源的音频素材采样率、位深和通道数各异,许多后续功能(降噪、拼接、格式转换等)都依赖统一的 PCM 格式才能正确工作。AudioTools 的重采样功能支持同时调整三个维度:

参数 可选值 说明
采样率 8000 / 11025 / 16000 / 22050 / 32000 / 44100 / 48000 / 96000 Hz 控制每秒采样点数
位深 8 / 16 / 32 bit 控制每个采样值的精度和动态范围
通道数 1(单声道)/ 2(立体声)/ 4(四声道) 控制独立声道数量

重采样是一个破坏性操作 ------成功后当前 PCM 会被替换为新数据,后续所有功能(波形、分析、保存、导出)都基于重采样后的结果。这一点与大多数非破坏性功能(降噪、淡入淡出等)不同。

功能流程

text 复制代码
用户解码 PCM → ResamplePage 选择输出参数(采样率/位深/通道数)
   ↓
   点击「执行重采样」
   ↓
   MediaAnalyzer::resampleCurrentPcm()
   ↓
   ┌── 同步调用 ──┐
   │ SwrContext 创建:输入格式 → 输出格式
   │ packed PCM → swr_convert → 输出 PCM
   │ 刷出内部延迟残留样本
   └──────────────┘
   ↓
   setPcmData(newPcm)  ← 覆盖当前 PCM
   ↓
   setPcmData 自动清空所有依赖旧 PCM 的分析结果
   ↓
   后续功能均基于新 PCM 工作

关键特点:

  • 覆盖式操作 :重采样成功后 setPcmData() 替换当前 PCM,所有依赖旧 PCM 的分析结果和预览缓存被自动清空。
  • 同步执行:重采样处理速度快(纯 SwrContext 转换),不引入异步调度。
  • 链式处理基础:裁剪、二次重采样、保存 WAV 等操作都以当前 PCM 为输入,重采样可以作为流水线的中间环节。

核心算法

位深与采样格式映射

SampleSpec 结构体统一描述 PCM 采样格式,避免各函数重复解析:

cpp 复制代码
struct SampleSpec {
    AVSampleFormat format = AV_SAMPLE_FMT_NONE;
    int bitsPerSample = 0;
    int bytesPerSample = 0;
    QString name;
};

sampleSpecFromBits() 将常用位深映射为 FFmpeg 采样格式:

位深 AVSampleFormat 字节数 名称
8 bit AV_SAMPLE_FMT_U8 1 u8(unsigned)
16 bit AV_SAMPLE_FMT_S16 2 s16(signed)
32 bit AV_SAMPLE_FMT_S32 4 s32(signed)

SwrContext 重采样管线

FFmpeg 的 SwrContext(libswresample)同时处理采样率转换、位深转换和通道数转换,三者可以在一次调用中完成。

通道布局:按通道数使用 FFmpeg 默认布局,不做自定义映射:

cpp 复制代码
av_channel_layout_default(&inputLayout, inputChannels);
av_channel_layout_default(&outputLayout, outputChannels);
  • 1 通道 → mono
  • 2 通道 → stereo
  • 4 通道 → 4.0

SwrContext 创建 :输入输出格式一次性指定,swr_init 内部自动选择最优重采样算法:

cpp 复制代码
swr_alloc_set_opts2(&swrContext,
    &outputLayout, outputSpec.format, outputSampleRate,   // 输出
    &inputLayout, inputSpec.format, inputSampleRate,      // 输入
    0, nullptr);
swr_init(swrContext);

输出缓冲计算

重采样后帧数会因采样率变化而缩放,且 SwrContext 内部存在延迟,必须预留足够空间:

cpp 复制代码
// 输出样本数 = (输入样本数 + swr 内部延迟) × (输出采样率 / 输入采样率),向上取整
const int64_t outputSamples64 = av_rescale_rnd(
    swr_get_delay(swrContext, inputSampleRate) + inputSamples64,
    outputSampleRate, inputSampleRate, AV_ROUND_UP);

swr_get_delay 返回 SwrContext 内部缓冲的延迟样本数(以输入采样率为单位),确保输出缓冲区不会因延迟样本溢出。

残留样本刷出

swr_convert 主调用完成后,SwrContext 内部可能仍有少量延迟样本(尤其是采样率变化时),需要循环刷出:

cpp 复制代码
// 主转换
const int convertedSamples = swr_convert(swrContext,
    outputData, outputSamples, inputData, inputSamples64);

// 刷出残留数据
while (true) {
    const int flushedSamples = swr_convert(swrContext,
        flushData, 1024, nullptr, 0);  // 输入为空,只刷输出
    if (flushedSamples <= 0) break;
    convertedPcm.append(flushed);
}

不刷出残留会导致音频尾部被截断,在高采样率转低采样率时尤其明显。

整数溢出保护

输入帧数超过 int 范围时会拒绝处理,避免 SwrContext 参数溢出:

cpp 复制代码
if (inputSamples64 > std::numeric_limits<int>::max()) {
    *errorText = "Input PCM data is too large to resample at once.";
    return false;
}

主处理函数

AudioToolProcessor::resamplePcm

位于 src/audio/audiotoolprocessor.cpp,无状态、无 UI 依赖,只负责 PCM 格式转换。

cpp 复制代码
bool AudioToolProcessor::resamplePcm(
    const QByteArray &inputPcm,
    const QVariantMap &inputInfo,
    int outputSampleRate,
    int outputBitsPerSample,
    int outputChannels,
    QByteArray *outputPcm,
    QVariantMap *outputInfo,
    QString *errorText) const

处理步骤

  1. 输入校验:检查 PCM 非空、格式字段有效、PCM 大小与格式匹配。
  2. 格式映射sampleSpecFromInfo(inputInfo) 解析输入格式,sampleSpecFromBits(bits) 构建输出格式。
  3. SwrContext 创建与初始化:指定输入/输出的通道布局、采样格式和采样率。
  4. 输出缓冲分配av_rescale_rnd 计算输出样本数并考虑内部延迟。
  5. 主转换swr_convert 一次性转换全部输入样本(packed interleaved 格式,只需一个平面指针)。
  6. 残留刷出 :循环调用 swr_convert(nullptr, 0) 直到内部缓冲清空。
  7. 截断与输出truncate(convertedSamples × channels × bytesPerSample) 精确截取有效数据,组装输出信息。

输出信息包含新 PCM 的完整格式描述:

cpp 复制代码
resultInfo.insert("format", outputSpec.name);           // "u8" / "s16" / "s32"
resultInfo.insert("sampleRate", outputSampleRate);
resultInfo.insert("channels", outputChannels);
resultInfo.insert("channelLayout", channelLayoutName);  // "mono" / "stereo" / "4.0"
resultInfo.insert("bitsPerSample", outputSpec.bitsPerSample);
resultInfo.insert("size", convertedPcm.size());
resultInfo.insert("sizeText", formatBytes(convertedPcm.size()));

导出封装

MediaAnalyzer::resampleCurrentPcm

QML 调用入口,直接同步调用 resamplePcm,成功后覆盖当前 PCM:

cpp 复制代码
bool MediaAnalyzer::resampleCurrentPcm(int sampleRate, int bitsPerSample, int channels)
{
    if (m_pcmData.isEmpty()) {
        setStatus("请先解码 PCM");
        return false;
    }

    setBusy(true);

    const QVariantMap currentPcmInfo = m_mediaInfo.value("pcm").toMap();
    const bool ok = m_toolProcessor.resamplePcm(
        m_pcmData, currentPcmInfo,
        sampleRate, bitsPerSample, channels,
        &resampledPcm, &pcmInfo, &errorText);

    if (ok) {
        // 更新 mediaInfo 中的 pcm 字段
        QVariantMap info = m_mediaInfo;
        info.insert("pcm", pcmInfo);
        setMediaInfo(info);

        // 覆盖当前 PCM ------ 这一步会触发级联状态清理
        setPcmData(resampledPcm);
        setStatus("重采样完成:" + pcmSummary());
    } else {
        setStatus("重采样失败:" + errorText);
    }

    setBusy(false);
    return ok;
}

注意 setPcmData() 不仅更新 PCM 数据,还会更新 m_mediaInfo["pcm"] 中的格式信息,确保后续功能读取到正确的采样率、位深和通道数。

QML 页面调用

ResamplePage 直接传递三个下拉框的当前值:

javascript 复制代码
ActionButton {
    text: "执行重采样"
    enabled: !mediaAnalyzer.busy && mediaAnalyzer.pcmSize > 0
    onClicked: mediaAnalyzer.resampleCurrentPcm(
        parseInt(sampleRateBox.currentText),
        parseInt(bitsBox.currentText),
        parseInt(channelsBox.currentText))
}

下拉框的默认值通过 appRoot.currentPcmValue() 读取当前 PCM 的实际参数,用户不改参数时执行等同于"原样重采样"(不改变数据)。

状态清理

重采样的状态管理是项目中最严格的------因为它是破坏性 PCM 操作,成功后当前 PCM 被替换,所有依赖旧 PCM 的结果都变为过期数据。

setPcmData() 内部统一清空以下状态:

被清空的状态 说明
volumeInfo 音量分析(Peak/RMS/dBFS)
audioFeature* 音频特征图所有数据
silenceInfo 静音段检测结果
trimInfo 裁剪结果
smoothTrimInfo 平滑截取结果
splitInfo 声道拆分结果
convertInfo 格式转换结果
extractAudioInfo 视频提取音频结果
concatInfo 拼接结果
lufsInfo LUFS 响度分析结果
speedInfo 变速结果
denoiseInfo + denoisedPcmData 降噪结果和预览
fadeInfo + fadedPcmData 淡入淡出结果和预览
vocalExtractInfo 人声提取结果
phaseInvertInfo + PCM 相位反转预览
monoMixInfo + PCM 单声道混合预览
pitchInfo + PCM 音调调整预览
normalizeInfo + PCM 响度归一化预览
filterInfo + PCM 高低通滤波预览
dcOffsetInfo + PCM DC 偏移校正预览
reverbInfo 混响效果结果

这一设计保证了 UI 不会出现"旧结果 + 新 PCM"的不一致状态------用户在重采样后看到的任何信息都基于最新的 PCM 数据。

小结

重采样功能是音频处理流水线的格式桥梁:

  • 算法层AudioToolProcessor::resamplePcm 利用 FFmpeg SwrContext 一次性完成采样率、位深和通道数三个维度的转换,并正确处理内部延迟和残留刷出。
  • 状态层MediaAnalyzer::resampleCurrentPcm 同步调用后通过 setPcmData() 覆盖当前 PCM,触发级联状态清理,确保所有下游功能不会读取过期数据。
  • UI 层ResamplePage 提供三组下拉框选择输出参数,默认值跟随当前 PCM 的实际参数,操作简洁。
  • 破坏性语义:重采样是少数会覆盖当前 PCM 的功能之一(另一个是增益/归一化),页面顶部注释明确提醒用户"后续功能均基于重采样结果"。
相关推荐
小短腿的代码世界20 小时前
Qt Bluetooth源码深度解析:从HCI协议到跨平台API的完整架构
开发语言·qt·架构
小短腿的代码世界21 小时前
Qt WebEngine多进程架构与IPC通信:从Chromium多进程到Qt信号槽融合
qt·架构·系统架构
尘中远1 天前
【Qwt 7.0 系列】快速入门与核心新特性概览 —— 更现代的 Qt 数据可视化库
qt·数据可视化·绘图·qwt·科学绘图
初阳7851 天前
【Qt】系统相关(3)——多线程
qt
初阳7851 天前
【Qt】系统相关(2)——文件
开发语言·qt
薄荷椰果抹茶1 天前
计算机导论_第6章_笔记
笔记·ffmpeg
尘中远2 天前
【Qwt 7.0 系列】坐标轴与刻度系统 —— 刻度引擎、网格、图例与刻度朝内
qt·数据可视化·qcustomplot·qwt·工业软件·科学绘图
sycmancia2 天前
Qt——多线程间的互斥
开发语言·qt
FlightYe2 天前
FFmpeg移动端硬解机制
linux·网络·ffmpeg·音视频·实时音视频·视频编解码