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

C# WPF中SpeexCodec编解码PCM文件后音质异常求助

Speex编解码后音质异常问题排查

我在C# WPF环境下使用SpeexCodec处理100Hz的PCM文件,参数设置为:采样率16kHz、采样位深16bit,每20ms原始数据大小640Bytes,编解码后每20ms生成70Bytes的帧(共2帧)。但将编解码后的文件转换为WAV格式播放时,音质与原文件差异极大,无法确定是编码还是解码环节出错。

以下是实现代码:

public class SpeexLib
{
    public struct SpeexBits
    {
        public IntPtr chars;      // char *chars;
        public int nbBits;        // int nbBits;
        public int charPtr;       // int charPtr;
        public int bitPtr;        // int bitPtr;
        public int owner;         // int owner;
        public int overflow;      // int overflow;
        public int buf_size;      // int buf_size;
        public int reserved1;     // int reserved1;
        public IntPtr reserved2;  // void *reserved2;
    }

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern IntPtr speex_encoder_init(IntPtr mode);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_encoder_destroy(IntPtr state);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_bits_reset(ref SpeexBits bits);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern int speex_encode_int(IntPtr state, short[] input, ref SpeexBits speexbits);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern int speex_bits_write(ref SpeexBits bits, byte[] output, int maxBytes);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern IntPtr speex_decoder_init(IntPtr mode);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_decoder_destroy(IntPtr state);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern int speex_decode(IntPtr state, ref SpeexBits bits, float[] output);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_bits_init(out SpeexBits bits);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_bits_destroy(ref SpeexBits bits);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern IntPtr speex_lib_get_mode(int mode);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_bits_read_from(ref SpeexBits bits, byte[] chars, int len);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_bits_init_buffer(ref SpeexBits bits, IntPtr buff, int buf_size);

    //추가된 부분
    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern int speex_encoder_ctl(IntPtr state, int request, ref int value);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern int speex_decoder_ctl(IntPtr state, int request, ref int value);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void speex_bits_set_bit_buffer(ref SpeexBits bits, IntPtr buff, int buf_size);

    [DllImport("libspeex.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern int speex_encode(IntPtr state, float[] input, ref SpeexBits bits);

    public void initSpeexBits()
    {
    }

    const int SPEEX_MODEID_NB = 0;
    const int SPEEX_MODEID_WB = 1;
    const int FRAME_SIZE = 320; // 20ms의 데이터 (16kHz 샘플레이트)
    const int COMPRESSED_SIZE = 70; // 인코딩사이즈
    const int SPEEX_SET_SAMPLING_RATE = 24; // 샘플레이트 설정에 사용되는 요청 ID
    const int SPEEX_SET_BITRATE = 18;
    const int SPEEX_GET_FRAME_SIZE = 3;
    const int SPEEX_SET_QUALITY = 4;


    public void EncodePCMToSpeex2()
    {
        string exePath = Assembly.GetExecutingAssembly().Location;
        string exeDirectory = Path.GetDirectoryName(exePath);
        string inputFilePath = Path.Combine(exeDirectory, "원본2.pcm");
        string outputFilePath = Path.Combine(exeDirectory, "path_to_output_file.pcm");

        short[] inBuffer = new short[FRAME_SIZE];
        float[] input = new float[FRAME_SIZE];
        byte[] cbits = new byte[COMPRESSED_SIZE];
        int nbBytes;

        // 인코더 초기화 및 설정
        IntPtr state = speex_encoder_init(speex_lib_get_mode(SPEEX_MODEID_WB));
        int tmp = 8; // 품질 설정
        speex_encoder_ctl(state, SPEEX_SET_QUALITY, ref tmp);
        int sampleRate = 16000; // 16kHz
        speex_encoder_ctl(state, SPEEX_SET_SAMPLING_RATE, ref sampleRate);


        SpeexBits bits;
        speex_bits_init(out bits);

        using (var fin = new FileStream(inputFilePath, FileMode.Open))
        using (var fout = new FileStream(outputFilePath, FileMode.Create))
        using (var bin = new BinaryReader(fin))
        using (var bout = new BinaryWriter(fout))
        {
            while (bin.BaseStream.Position < bin.BaseStream.Length)
            {
                for (int i = 0; i < FRAME_SIZE; i++)
                {
                    if (bin.BaseStream.Position < bin.BaseStream.Length)
                        inBuffer[i] = bin.ReadInt16();
                    else
                        inBuffer[i] = 0;
                    input[i] = inBuffer[i];
                }

                speex_bits_reset(ref bits);
                speex_encode(state, input, ref bits);
                nbBytes = speex_bits_write(ref bits, cbits, COMPRESSED_SIZE);

                bout.Write(nbBytes);
                bout.Write(cbits, 0, nbBytes);
            }
        }

        speex_bits_destroy(ref bits);
        speex_encoder_destroy(state);
    }

    public void DecodeSpeexToPCM()
    {
        string exePath = Assembly.GetExecutingAssembly().Location;
        string exeDirectory = Path.GetDirectoryName(exePath);
        string inputFilePath = Path.Combine(exeDirectory, "path_to_output_file.pcm");
        string outputFilePath = Path.Combine(exeDirectory, "path_to_output_file2.pcm");

        SpeexBits bits;
        speex_bits_init(out bits);

        IntPtr state = speex_decoder_init(speex_lib_get_mode(SPEEX_MODEID_WB));

        int enh = 1; // 음질 개선 설정
        speex_decoder_ctl(state, 1, ref enh);

        using (var fin = new FileStream(inputFilePath, FileMode.Open))
        using (var fout = new FileStream(outputFilePath, FileMode.Create))
        using (var bin = new BinaryReader(fin))
        using (var bout = new BinaryWriter(fout))
        {
            while (fin.Position < fin.Length)
            {
                int nbBytes = bin.ReadInt32(); // 인코딩된 바이트 수 읽기
                byte[] cbits = bin.ReadBytes(nbBytes);

                speex_bits_read_from(ref bits, cbits, nbBytes);

                float[] output = new float[FRAME_SIZE];
                speex_decode(state, ref bits, output);

                foreach (float sample in output)
                {
                    short outSample = (short)(sample);
                    bout.Write(outSample);
                }
            }
        }

        speex_bits_destroy(ref bits);
        speex_decoder_destroy(state);
    }


    public void ConvertPcmToWav(string inputFile, string outputFile, int sampleRate, int channels, int bitsPerSample)
    {
        using (var reader = new RawSourceWaveStream(File.OpenRead(inputFile), new WaveFormat(sampleRate, bitsPerSample, channels)))
        using (var writer = new WaveFileWriter(outputFile, reader.WaveFormat))
        {
            reader.CopyTo(writer);
        }
    }

    private short[] ConvertFloatArrayToShortArray(float[] floatArray)
    {
        short[] shortArray = new short[floatArray.Length];
        for (int i = 0; i < floatArray.Length; i++)
        {
            shortArray[i] = (short)(floatArray[i] * short.MaxValue);
        }
        return shortArray;
    }
}

核心问题排查与修复

1. 编码环节:输入数据未归一化

Speex的speex_encode接口要求输入的float值是归一化到[-1, 1]范围的数值,但你直接将short类型的PCM样本(范围[-32768, 32767])赋值给float数组,导致输入超出预期范围,编码时出现严重失真。
修复代码:

// 替换编码环节的input[i] = inBuffer[i]
input[i] = inBuffer[i] / (float)short.MaxValue;

2. 解码环节:输出数据未还原归一化

Speex的speex_decode输出的float值是归一化到[-1,1]的数值,你直接强制转换为short会导致大部分样本值趋近于0,声音几乎无法正常播放。应该使用你已经实现的ConvertFloatArrayToShortArray方法还原:
修复代码:

// 替换解码环节的foreach循环
short[] outSamples = ConvertFloatArrayToShortArray(output);
foreach (short sample in outSamples)
{
    bout.Write(sample);
}

3. 解码时未重置位缓冲区

每次解码一帧前,必须调用speex_bits_reset清空位缓冲区,否则上一帧的残留位数据会干扰当前帧解码,导致输出混乱。
修复代码:

while (fin.Position < fin.Length)
{
    int nbBytes = bin.ReadInt32();
    byte[] cbits = bin.ReadBytes(nbBytes);

    speex_bits_reset(ref bits); // 新增此行
    speex_bits_read_from(ref bits, cbits, nbBytes);

    float[] output = new float[FRAME_SIZE];
    speex_decode(state, ref bits, output);
    // ... 后续转换和写入
}

4. 解码器采样率未显式设置

虽然使用了宽频模式(默认16kHz),但显式设置采样率可以避免环境或版本差异导致的参数不匹配问题:
修复代码:

// 解码器初始化后添加
int sampleRate = 16000;
speex_decoder_ctl(state, SPEEX_SET_SAMPLING_RATE, ref sampleRate);

验证步骤

  1. 单独验证编码:将编码后的Speex文件用官方工具speexdec解码,若输出PCM正常,说明问题出在你的解码逻辑;若官方工具解码也异常,说明编码环节出错。
  2. 单独验证解码:用官方工具speexenc编码一段测试PCM,再用你的解码逻辑处理,观察输出是否正常。
  3. 确认原始PCM格式:确保输入PCM是16kHz、16bit、单声道(代码中帧大小320对应单声道20ms数据:16000*0.02=320),多声道需拆分后处理。

内容的提问来源于stack exchange,提问作者김회윤

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 16:45:55