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);
验证步骤
- 单独验证编码:将编码后的Speex文件用官方工具
speexdec解码,若输出PCM正常,说明问题出在你的解码逻辑;若官方工具解码也异常,说明编码环节出错。 - 单独验证解码:用官方工具
speexenc编码一段测试PCM,再用你的解码逻辑处理,观察输出是否正常。 - 确认原始PCM格式:确保输入PCM是16kHz、16bit、单声道(代码中帧大小320对应单声道20ms数据:16000*0.02=320),多声道需拆分后处理。
内容的提问来源于stack exchange,提问作者김회윤
相关产品推荐
相关产品推荐

