You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用minimp3实时转MP3到PCM时帧开头出现噪声/间隙的解决方法

修复minimp3解码UE5音频时帧块开头的噪声/间隙问题

问题背景

我用minimp3配合WebSocket实现了向Unreal Engine 5客户端传输MP3数据的音频流水线:接收数据存入TArray缓冲区,解码为PCM后添加到音频源,循环处理所有MP3帧。已确认MP3传输正常,但转换后的PCM在每个MP3帧块开头出现噪声/间隙,导致播放失真。用100Hz正弦波MP3测试,Audacity波形显示噪声/间隙出现在新块开头,每次约3帧长度,推测和MP3字节池系统有关,且全程使用同一个mp3d实例。

相关代码

#include "minimp3.h"
#include "WebSocketModule.h"
#include "AudioMixerBlueprintLibrary.h"

class UAudioStreamer : public UObject
{
    GENERATED_BODY()

private:
    mp3d_t mp3d;
    TArray<uint8> MP3Buffer;
    FAudioDevice* AudioDevice;
    FSoundSource* AudioSource;
    TArray<int16> PCMData;

public:
    void Initialize()
    {
        mp3d_init(&mp3d);
        AudioDevice = GEngine->GetMainAudioDevice();
        AudioSource = AudioDevice->CreateSoundSource();
    }

    void OnWebSocketReceived(const TArray<uint8>& Data)
    {
        MP3Buffer.Append(Data);
        ProcessMP3Buffer();
    }

    void ProcessMP3Buffer()
    {
        mp3dec_frame_info_t info;
        int16_t pcm[MINIMP3_MAX_SAMPLES_PER_FRAME];

        while (MP3Buffer.Num() >= MINIMP3_MAX_FRAME_SIZE)
        {
            int samples = mp3dec_decode_frame(&mp3d, MP3Buffer.GetData(), MP3Buffer.Num(), pcm, &info);

            if (samples <= 0)
            {
                // Remove invalid data
                MP3Buffer.RemoveAt(0, 1);
                continue;
            }

            if (info.frame_bytes > 0)
            {
                MP3Buffer.RemoveAt(0, info.frame_bytes);
            }

            if (samples > 0)
            {
                PCMData.Append(pcm, samples);
                if (PCMData.Num() >= 4096)
                {
                    PlayPCMData();
                }
            }
        }
    }

    void PlayPCMData()
    {
        if (AudioSource && PCMData.Num() > 0)
        {
            FAudioBuffer AudioBuffer;
            AudioBuffer.AudioData = PCMData.GetData();
            AudioBuffer.NumSamples = PCMData.Num();
            AudioBuffer.NumChannels = 2; // Assuming stereo
            AudioBuffer.SampleRate = 44100;

            AudioSource->QueueAudio(AudioDevice, AudioBuffer);
            PCMData.Empty();
        }
    }
};

解码后波形

100Hz正弦波MP3解码后波形

修复方案

1. 修正无效数据的处理逻辑

当前解码失败时仅移除1字节的逻辑会破坏帧同步,导致后续解码的帧开头出现错误。改为主动寻找MP3帧同步字(0xFFE0开头的16位值),从同步位置开始解码:

if (samples <= 0)
{
    // 查找有效的MP3帧同步头
    int syncPos = -1;
    const int BufferLen = MP3Buffer.Num();
    for (int i = 0; i < BufferLen - 1; ++i)
    {
        uint16_t SyncWord = (static_cast<uint16_t>(MP3Buffer[i]) << 8) | MP3Buffer[i+1];
        // 匹配MP3帧同步标志(0xFFE0或0xFFF0)
        if ((SyncWord & 0xFFE0) == 0xFFE0)
        {
            syncPos = i;
            break;
        }
    }

    if (syncPos != -1)
    {
        MP3Buffer.RemoveAt(0, syncPos);
    }
    else
    {
        // 未找到有效同步头,清空缓冲区避免无效循环
        MP3Buffer.Empty();
    }
    continue;
}

2. 动态匹配音频格式参数

不要硬编码声道数和采样率,用minimp3返回的帧信息动态设置,避免格式不匹配导致的播放失真:

void PlayPCMData()
{
    if (AudioSource && PCMData.Num() > 0)
    {
        FAudioBuffer AudioBuffer;
        AudioBuffer.AudioData = PCMData.GetData();
        AudioBuffer.NumSamples = PCMData.Num();
        // 从解码信息中获取真实声道数和采样率
        AudioBuffer.NumChannels = mp3d.info.channels;
        AudioBuffer.SampleRate = mp3d.info.hz;

        AudioSource->QueueAudio(AudioDevice, AudioBuffer);
        PCMData.Empty();
    }
}

3. 优化缓冲区处理逻辑

  • 避免在帧中间截断PCM数据:确保每次添加到PCMData的是完整的解码帧,不要因为达到4096样本就强制清空,可调整阈值为完整帧的倍数,比如基于当前采样率和声道数计算合适的缓冲大小。
  • 设置最小解码阈值:不要一达到MINIMP3_MAX_FRAME_SIZE就解码,可积累到MINIMP3_MAX_FRAME_SIZE * 2再开始处理,减少分块截断导致的同步问题。

4. 验证传输分块的完整性

检查WebSocket发送端是否按完整MP3帧分块发送,如果发送端在帧中间截断数据,接收端缓冲区会频繁出现不完整帧,引发同步错误。尽量让发送端以完整MP3帧为单位发送数据。

内容的提问来源于stack exchange,提问作者HarrisR

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 10:57:17