Qt + FFmpeg 音频工具:淡入淡出功能实现

前言

"淡入淡出"功能,这是纯 PCM 操作,零新依赖,实现简单却很实用------裁剪后的音频经常需要淡入淡出避免爆音,是音频后处理的基础能力。

本文介绍实现的关键点。

效果图:


功能概述

  • 淡入(Fade In):音频开头从静音逐渐增大到正常音量
  • 淡出(Fade Out):音频结尾从正常音量逐渐减小到静音
  • 曲线类型:支持线性、等功率(指数)、S 曲线(余弦)三种渐变形状
  • 非破坏性:不修改当前 PCM 数据,处理结果另存为 WAV,并支持播放试听对比

核心算法

本质是对 interleaved PCM 采样数据做幅度包络处理

  • 淡入:前 N 帧的所有通道采样乘以一个从 0 → 1 递增的系数
  • 淡出:后 N 帧的所有通道采样乘以一个从 1 → 0 递减的系数

曲线函数

三种曲线的数学表达:

cpp 复制代码
// 根据曲线类型计算增益系数,t ∈ [0, 1]
static double fadeCurveValue(double t, const QString &curveType)
{
    if (curveType == QStringLiteral("exponential")) {
        // 等功率曲线(正弦弧):听感上比线性更平滑
        return std::sin(M_PI * 0.5 * t);
    }
    if (curveType == QStringLiteral("sc")) {
        // S 曲线(余弦弧):首尾变化慢、中间变化快,最自然
        return 0.5 * (1.0 - std::cos(M_PI * t));
    }
    // 默认线性:系数与时间成正比
    return t;
}
曲线 公式 听感
线性 f(t) = t 均匀变化,简单直接
等功率 f(t) = sin(πt/2) 更平滑,专业音频常用
S 曲线 f(t) = (1 - cos(πt)) / 2 首尾柔和,最自然

C++ 后端实现

接口声明

AudioToolProcessor 中新增方法,遵循项目统一的无状态工具函数风格:

cpp 复制代码
// 纯 PCM 操作,不引入任何新依赖,输出格式与输入完全一致
bool applyFadeInOut(const QByteArray &inputPcm,
                    const QVariantMap &inputInfo,
                    int fadeInMs,       // 淡入时长(ms),0 表示跳过
                    int fadeOutMs,      // 淡出时长(ms),0 表示跳过
                    const QString &curveType,
                    QByteArray *outputPcm,
                    QVariantMap *outputInfo,
                    QVariantMap *fadeInfo,
                    QString *errorText) const;

关键实现逻辑

cpp 复制代码
bool AudioToolProcessor::applyFadeInOut(...) const
{
    // 1. 解析 PCM 格式(采样率、通道数、位深)
    const int sampleRate = inputInfo.value("sampleRate").toInt();
    const int channels   = inputInfo.value("channels").toInt();
    const SampleSpec sampleSpec = sampleSpecFromInfo(inputInfo);

    // 2. 毫秒 → frame 数(以 frame 为单位保证多通道对齐)
    const qint64 fadeInFrames  = fadeInMs  * sampleRate / 1000;
    const qint64 fadeOutFrames = fadeOutMs * sampleRate / 1000;

    // 3. 防重叠:淡入 + 淡出 > 总帧数时,淡入优先,缩短淡出
    if (safeFadeInFrames + safeFadeOutFrames > frameCount)
        effectiveFadeOutFrames = frameCount - safeFadeInFrames;

    // 4. 淡入:前 N 帧,每帧所有通道 × 递增系数
    for (qint64 f = 0; f < safeFadeInFrames; ++f) {
        const double t = (double)f / (safeFadeInFrames - 1);
        const double coeff = fadeCurveValue(t, curve);
        for (int ch = 0; ch < channels; ++ch) {
            // 读取采样 → 乘以系数 → 钳位 → 写回
            double value = readSample(...);
            writeSample(..., qBound(min, value * coeff, max));
        }
    }

    // 5. 淡出:后 N 帧,每帧所有通道 × 递减系数(逻辑对称)
    ...
}

要点:

  • frame 为单位遍历(而非 sample),确保多通道对齐不被破坏
  • 支持 8-bit unsigned / 16-bit signed / 32-bit signed 三种位深
  • 淡入淡出区间重叠时淡入优先,避免短音频异常

门面层串联

MediaAnalyzer 负责 QML 状态管理,exportFadeWav 串联处理和保存:

cpp 复制代码
bool MediaAnalyzer::exportFadeWav(int fadeInMs, int fadeOutMs,
                                   const QString &curveType, const QString &filePath)
{
    // 调用 AudioToolProcessor 执行淡入淡出 PCM 处理
    const bool processOk = m_toolProcessor.applyFadeInOut(
        m_pcmData, currentPcmInfo, fadeInMs, fadeOutMs, curveType,
        &processedPcm, &pcmInfo, &fadeInfo, &errorText);

    if (processOk) {
        // 处理成功后封装保存为 WAV
        ok = m_toolProcessor.savePcmAsWav(outputPath, processedPcm, pcmInfo, &errorText);
    }

    if (ok) {
        setFadeInfo(fadeInfo);
        // 关键:将处理后的 PCM 存入预览属性,供 QML 波形播放控件试听
        setFadedPcm(processedPcm, pcmInfo);
    }
}

为了让 QML 侧能播放处理后的音频,新增了 fadedPcmData / fadedPcmSize / fadedPcmInfo 三个属性,与已有的 denoisedPcmData 预览模式一致。


QML 前端

参数控制

曲线类型三选一按钮 + 淡入/淡出时长滑块(0~5000ms):

qml 复制代码
Repeater {
    model: [
        { key: "linear",      label: "线性" },
        { key: "exponential", label: "等功率" },
        { key: "sc",          label: "S 曲线" }
    ]
    delegate: Rectangle {
        color: page.curveType === modelData.key ? accentColor : "#071526"
        Text { text: modelData.label }
        MouseArea { onClicked: page.curveType = modelData.key }
    }
}

波形对比 + 播放试听

使用两个 WaveformPlayer 组件并排展示,自带播放/暂停/进度条/音量控制:

qml 复制代码
GridLayout {
    columns: width >= 820 ? 2 : 1

    // 左侧:原始 PCM 波形 + 播放原声
    WaveformPlayer {
        title: "处理前(原始 PCM)"
        pcmData: mediaAnalyzer.pcmData
        pcmSize: mediaAnalyzer.pcmSize
        previewKey: "fade_before"
    }

    // 右侧:处理后 PCM 波形 + 播放淡入淡出效果
    WaveformPlayer {
        title: "处理后(淡入淡出)"
        pcmData: mediaAnalyzer.fadedPcmData
        pcmSize: mediaAnalyzer.fadedPcmSize
        previewKey: "fade_after"
    }
}

WaveformPlayer 内部调用 preparePcmPreviewWav 将 PCM 写入临时 WAV 文件,再由 MediaPlayer 播放,实现即点即听。


架构总结

复制代码
QML (FadePage.qml)
  │  用户设置 fadeInMs / fadeOutMs / curveType
  ▼
MediaAnalyzer::exportFadeWav()
  │  调度 + 状态管理 + 保存 WAV
  ▼
AudioToolProcessor::applyFadeInOut()
  │  纯 PCM 采样点遍历 × 渐变系数
  ▼
输出:fadedPcmData (预览) + _fade.wav (导出)
层级 文件 职责
工具层 AudioToolProcessor 纯 PCM 数据处理,无状态
门面层 MediaAnalyzer QML 状态管理、异步调度、文件 I/O
界面层 FadePage.qml 参数输入、波形展示、播放试听

一句话总结

淡入淡出是最简单的音频 DSP 功能之一------遍历 PCM 采样乘以渐变系数即可。关键在于以 frame 为单位保证通道对齐、支持多种曲线形状、以及通过预览 PCM 属性实现处理后即时试听对比。