Soundly 音频DSP流程核心逻辑说明

本文档着重介绍 Soundly 项目内 DSP 流程的代码架构逻辑实现细节,周边配套逻辑不做说明

一、为什么要有 DSP 工具?

录音场景下无法确保周边环境绝对安静,导致生成的录音文件会有杂声、噪音等无关主题内容的声音,影响用户收听最终产物的体验。常见问题真实场景有博客录音、视频会议、VLog等,这些场景一般情况下都会伴随周边杂声等。

DSP 工具可以针对有杂声、噪音的音频文件,进行音频解析、噪声识别、降噪、声音优化、再生成优化后的音频产物这几个流程,完成整个优化过程。

二、框架、流程、步骤

flowchart TD A[开始] --> B[选择音频文件 WAV / MP4 / MP3] B --> C[解析音频文件生成原始PCM数据] C --> D[分析音频特征] D --> E[确定DSP策略] E --> F[根据DSP策略配置音频处理流水线参数] F --> G[根据流水线参数重新生成新的PCM数据] G --> H[根据新的PCM数据生成WAV音频文件] H --> I[音频处理结束]

三、核心步骤、算法逻辑

1、选择音频文件

此部分不涉及核心逻辑,按常规获取本地文件方法处理即可

2、解析音频文件

此部分会涉及到 MediaExtractor 对媒体文件的信息解释,该组件是 Android 系统自带的多媒体 这个步骤会把媒体文件的相关信息解析出来,如 mime、码率、声道数量 等 处理完音频文件后,会把音频数据转化为 PCM 原始数据,方便后续进一步处理

Kotlin 复制代码
fun decode(uri: Uri): PcmData {
    val extractor = MediaExtractor()
    extractor.setDataSource(context, uri, null)

    val trackIndex = selectAudioTrack(extractor)
    require(trackIndex >= 0) {"No audio track found"}

    extractor.selectTrack(trackIndex)
    val format = extractor.getTrackFormat(trackIndex)

    val mime = format.getString(MediaFormat.KEY_MIME)!!
    val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
    val channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)

    Timber.i("mime=$mime, sampleRate=$sampleRate, channelCount=$channelCount")

    val codec = MediaCodec.createDecoderByType(mime)
    codec.configure(format, null, null, 0)
    codec.start()

    val pcmBuffer = ArrayList<Float>(1024 * 1024)

    val bufferInfo = MediaCodec.BufferInfo()
    var isEos = false

    while (true) {
        // ---------  输入  ----------
        if (!isEos) {
            val inputIndex = codec.dequeueInputBuffer(10_000)
            if (inputIndex >= 0) {
                val inputBuffer = codec.getInputBuffer(inputIndex)!!
                val size = extractor.readSampleData(inputBuffer, 0)

                if (size < 0) {
                    codec.queueInputBuffer(
                        inputIndex,
                        0,
                        0,
                        0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                    )
                    isEos = true
                } else {
                    codec.queueInputBuffer(
                        inputIndex,
                        0,
                        size,
                        extractor.sampleTime,
                        0
                    )
                    extractor.advance()
                }

            }
        }

        // --------  输出  ----------
        val outputIndex = codec.dequeueOutputBuffer(bufferInfo, 10_000)
        when {
            outputIndex >= 0 -> {
                val outputBuffer = codec.getOutputBuffer(outputIndex)!!
                val pcmChunk = decodeToFloatPcm(
                    buffer = outputBuffer,
                    size = bufferInfo.size,
                    channelCount = channelCount
                )
                pcmBuffer.addAll(pcmChunk.toList())
                codec.releaseOutputBuffer(outputIndex, false)

                if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                    break
                }
            }

            outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                // k可读取新 format (例如 sampleRate 变化)
            }
        }

    }

    codec.stop()
    codec.release()
    extractor.release()

    // 重新计算 channel
    val finalPcm: FloatArray
    val finalChannel: Int

    if (channelCount == 2) {
        finalPcm = stereoToMono(pcmBuffer.toFloatArray())
        finalChannel = 1
    } else {
        finalPcm = pcmBuffer.toFloatArray()
        finalChannel = 1
    }

    return PcmData(
        pcm = finalPcm,
        sampleRate = sampleRate,
        channels = finalChannel
    )

}

3、分析音频特征

这个步骤会根据原始 PCM 数据,按照以下条件得出音频的特征信息

  1. RMS(整体响度)
  2. Zero Crossing Rate(嘈杂 / 高频程度)
  3. Spectral Flatness(声音类型近似值)
  4. Speech Confidence(人声可信度)
kotlin 复制代码
data class AudioFeatures(
    val rms: Float, // 整体响度
    val zeroCrossingRate: Float, // 嘈杂程度
    val spectralFlatness: Float, // 0.0 ~ 0.3 有结构(人声),0.5 ~ 1.0 (白噪声/风声)
    val speechConfidence: Float, // 0 ~ 1 人声可信度
    val isValid: Boolean, // 是否可用
)
Kotlin 复制代码
override fun analyze(
    pcm: FloatArray,
    sampleRate: Int
): AudioFeatures {

    // ========= 基础合法性校验 =========
    if (pcm.isEmpty() || sampleRate <= 0) {
        return invalid()
    }

    // -------- 1. RMS(整体响度)--------
    var energySum = 0f
    for (v in pcm) {
        energySum += v * v
    }
    val rms = sqrt(energySum / pcm.size)

    // -------- 2. Zero Crossing Rate(嘈杂 / 高频程度)--------
    var zeroCrossings = 0
    for (i in 1 until pcm.size) {
        if (pcm[i - 1] * pcm[i] < 0) {
            zeroCrossings++
        }
    }
    val zeroCrossingRate =
        zeroCrossings.toFloat() / pcm.size.toFloat()

    // -------- 3. Spectral Flatness(工程近似版)--------
    // 用"瞬时变化能量 / 总能量"来近似
    var diffEnergy = 0f
    for (i in 1 until pcm.size) {
        val diff = pcm[i] - pcm[i - 1]
        diffEnergy += diff * diff
    }
    val spectralFlatness =
        (diffEnergy / (energySum + 1e-9f))
            .coerceIn(0f, 1f)

    // -------- 4. Speech Confidence(人声可信度)--------
    val speechConfidence = estimateSpeechConfidence(
        rms = rms,
        zcr = zeroCrossingRate,
        flatness = spectralFlatness
    )

    val valid = rms.isFinite() &&
            spectralFlatness.isFinite() &&
            zeroCrossingRate.isFinite() &&
            speechConfidence.isFinite()

    return AudioFeatures(
        rms = rms,
        spectralFlatness = spectralFlatness,
        zeroCrossingRate = zeroCrossingRate,
        speechConfidence = speechConfidence,
        isValid = valid
    )
}

4、确定 DSP 策略

当前有多种固定的 DSP 策略可以提前选择,分别是

  1. 原始音频(完全不处理)
  2. 默认增强(产品默认)
  3. 人声稳态增强
  4. 清晰优先
  5. 自然顺滑
  6. 播客/访谈场景
  7. 会议/电话场景
  8. 音乐/嘈杂场景
  9. AI增强

1 - 8 选项是已经成型的固定参数配置选项

第 9 项 AI增强 为根据当前音频特征云端分析后下发适配参数(待扩展内容)

5、根据 DSP 策略配置音频处理流水线参数

DSP 策略本质上是针对流水线上4个处理节点下发的配置参数,4个处理节点分别为

  1. 降噪节点
  2. 均衡器节点
  3. 压缩节点
  4. 最大允许振幅节点

当前逻辑架构是流水线形式,不一定4个节点都要进入处理,可以跳过当中任意1个或多个节点,跳过的节点会保持原音频的数据特征。

对应节点参数安全区间

rust 复制代码
 NoiseReduction.strength     0.05 ~ 0.30    过高会导致水声 / 能量掏空
 NoiseReduction.noiseFloor  0.008 ~ 0.025   低于下限关键频噪被带进人声
 
 Eq.lowGain                 0.90 ~ 1.05     低频过高→浑厚 过低→薄
 Eq.midGain                 1.00 ~ 1.15     核心人声频段,过高刺耳
 Eq.highGain                0.95 ~ 1.10     高频太高→刺耳;太低→沉闷
 
 Compressor.thresholdDb     0.60 ~ 0.75     阈值越小越容易压顶
 Compressor.ratio           1.0 ~ 1.4       太大压缩推进→炸音

 Limiter.limit              0.985 ~ 1.000   过低 → 人声闷;过高 → 不保护

6、DSP 参数确定后,进入流水线进行音频数据再生产,最终成品为处理后的 PCM 数据

  1. 降噪节点 NoiseReductionNode
Kotlin 复制代码
override fun process(input: FloatArray, sampleRate: Int): FloatArray {

    val p = preset ?: return input

    val strength = p.strength ?: 0.2f
    val noiseFloor = p.noiseFloor ?: 0.015f

    val output = FloatArray(input.size)

    for (i in input.indices) {
        val s = input[i]
        output[i] =
            if (abs(s) < noiseFloor) {
                s * (1f - strength)
            } else {
                s
            }
    }

    return output
}
  1. 均衡器节点 EqNode
Kotlin 复制代码
override fun process(input: FloatArray, sampleRate: Int): FloatArray {
    val p = preset ?: return input

    val low = p.lowGain ?: 1.0f
    val mid = p.midGain ?: 1.05f
    val high = p.highGain ?: 1.0f

    val gain = (low + mid + high) / 3f

    return FloatArray(input.size) {
        (input[it] * gain).coerceIn(-1f, 1f)
    }
}
  1. 压缩节点 CompressorNode
Kotlin 复制代码
override fun process(input: FloatArray, sampleRate: Int): FloatArray {
    val p = preset ?: return input

    val threshold = p.threshold ?: 0.7f
    val ratio = p.ratio ?: 1.8f

    val out = FloatArray(input.size)

    for (i in input.indices) {
        val s = input[i]
        val abs = kotlin.math.abs(s)

        out[i] =
            if (abs > threshold) {
                val excess = abs - threshold
                (threshold + excess / ratio) * kotlin.math.sign(s)
            } else s
    }
    return out
}
  1. 最大允许振幅节点
Kotlin 复制代码
override fun process(input: FloatArray, sampleRate: Int): FloatArray {
    val limit = preset?.limit ?: 0.95f

    return FloatArray(input.size) {
        input[it].coerceIn(-limit, limit)
    }
}

7、将处理后的 PCM 数据,压缩为 wav 文件

此处为常规文件格式转换逻辑,无特殊加工

Kotlin 复制代码
fun write(
    pcm: FloatArray,
    sampleRate: Int,
    channels:Int,
    fileName: String
): File {

    Timber.i("Writing WAV sampleRate=$sampleRate, pcmSize=${pcm.size}")

    val durationSec = pcm.size.toFloat() / sampleRate
    Timber.i("PCM duration ~ ${durationSec}s")

    val pcm16 = floatToPcm16(pcm)
    val dataSize = pcm16.size * 2
    val bitsPerSample = 16
    val blockAlgin = channels * (bitsPerSample / 8)
    val byteRate = sampleRate * blockAlgin

    val file = File(outputDir, "$fileName.wav")

    FileOutputStream(file).use { out ->

        // RIFF header
        writeString(out, "RIFF")
        writeInt(out, 36 + dataSize)
        writeString(out, "WAVE")

        // fmt chunk
        writeString(out, "fmt ")
        writeInt(out, 16)
        writeShort(out, 1)              // PCM
        writeShort(out, channels.toShort())              // Mono
        writeInt(out, sampleRate)
        writeInt(out, byteRate)
        writeShort(out, blockAlgin.toShort())
        writeShort(out, bitsPerSample.toShort())

        // data chunk
        writeString(out, "data")
        writeInt(out, dataSize)

        // PCM data
        for (s in pcm16) {
            writeShort(out, s)
        }
    }

    return file
}

经过7个步骤,即可将导入的原始音频进行优化加工,得到优化后的音频文件。可对处理后的文件进行播放、转发、再生产等

相关推荐
程序员_Rya2 天前
语聊房如何选择实时语音SDK?一文说清楚决策要点!
实时音视频·音视频开发·技术选型·音视频sdk·音视频sdk对比
ZengLiangYi5 天前
用 AudioContext.suspend()/resume() 作为流式音视频的同步门控
前端·音视频开发
leafyyuki9 天前
如何优雅地上传大文件?分片上传实战指南
前端·音视频开发
炼金术16 天前
AI 驱动的自主开发闭环:从"人工测试员"到"需求驱动"的转变
ai编程·音视频开发
冬奇Lab1 个月前
一天一个开源项目(第17篇):ViMax - 多智能体视频生成框架,导演、编剧、制片人全包
开源·音视频开发
冬奇Lab1 个月前
一天一个开源项目(第16篇):Code2Video - 用代码生成高质量教学视频的智能框架
开源·aigc·音视频开发
u1301302 个月前
深入理解 M3U8 与 HLS 协议:从原理到实战解析
前端·音视频开发·流媒体·hls·m3u8
字节架构前端2 个月前
媒体采集标准草案 与 Chromium 音频采集实现简介
前端·chrome·音视频开发
Tiny_React2 个月前
使用 Claude Code Skills 模拟的视频生成流程
人工智能·音视频开发·vibecoding