SDL_OpenAudioDevice实时播放音频杂音问题及循环缓冲区解决方法
解决SDL音频播放爆音、杂音问题:用循环缓冲区解耦帧更新与音频回调
最近我在把一款模拟器移植到SDL平台时,遇到了头疼的音频问题:虽然能正常播放,但音质很差,满是爆音和金属杂音,音频格式是16位有符号的。
问题根源
一开始我的做法是,在每帧调用的方法里直接把下一帧的音频采样传给SDL回调实时播放——这就犯了一个关键错误:帧更新的节奏和音频设备的采样需求节奏不匹配,导致音频流断断续续,自然就出现了杂音。
解决方案:循环缓冲区(环形缓冲区)
我最终用循环缓冲区解决了这个问题,核心思路是把帧更新传递的音频数据先写入缓冲区,SDL音频回调需要数据时再从缓冲区读取,用读写指针来维护数据的流转,彻底解耦两个环节的节奏。
具体来说:
- 当底层代码每帧传递下一帧音频采样时,把数据写入缓冲区的写指针位置,写完后更新写指针
- 当SDL音频回调触发(音频设备需要数据)时,从缓冲区的读指针位置读取数据播放,读完后更新读指针
- 读写指针每次移动的步长都是每帧的采样数,缓冲区大小设为多帧数据的总量,保证有足够的缓冲空间
完整代码实现
循环缓冲区结构定义
typedef struct circularBufferStruct { short *buffer; int cells; short *readPoint; short *writePoint; } circularBuffer;
音频初始化方法
int initialize_audio(int stereo) { if (stereo) channel = 2; else channel = 1; // Check if sound is disabled if (sampleRate != 0) { // Initialize SDL Audio if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { SDL_Log("SDL fails to initialize audio subsystem!\n%s", SDL_GetError()); return 1; } // Number of samples per frame samplesPerFrame = (double)sampleRate / (double)framesPerSecond * channel; audioSamplesSize = samplesPerFrame * bytesPerSample; // Bytes audioBufferSize = audioSamplesSize * 10; // Bytes, 10帧的缓冲大小 // Set and clear circular buffer audioBuffer.buffer = malloc(audioBufferSize); // Bytes, must be a multiple of audioSamplesSize memset(audioBuffer.buffer, 0, audioBufferSize); audioBuffer.cells = (audioBufferSize) / sizeof(short); // Cells, not Bytes! audioBuffer.readPoint = audioBuffer.buffer; audioBuffer.writePoint = audioBuffer.readPoint + (short)samplesPerFrame; } else samplesPerFrame = 0; // First frame return samplesPerFrame; }
SDL音频回调函数
void audioCallback(void *userdata, uint8_t *stream, int len) { SDL_memset(stream, 0, len); if (audioSamplesSize == 0) return; if (len > audioSamplesSize) { len = audioSamplesSize; } SDL_MixAudioFormat(stream, (const Uint8 *)audioBuffer.readPoint, AUDIO_S16SYS, len, SDL_MIX_MAXVOLUME); audioBuffer.readPoint += (short)samplesPerFrame; if (audioBuffer.readPoint >= audioBuffer.buffer + audioBuffer.cells) audioBuffer.readPoint = audioBuffer.readPoint - audioBuffer.cells; }
每帧更新音频的方法
int update_audio(short *buffer) { // Check if sound is disabled if (sampleRate != 0) { memcpy(audioBuffer.writePoint, buffer, audioSamplesSize); // Bytes audioBuffer.writePoint += (short)samplesPerFrame; // Cells if (audioBuffer.writePoint >= audioBuffer.buffer + audioBuffer.cells) audioBuffer.writePoint = audioBuffer.writePoint - audioBuffer.cells; if (firstTime) { // Set required audio specs want.freq = sampleRate; want.format = AUDIO_S16SYS; want.channels = channel; want.samples = samplesPerFrame / channel; // total samples divided by channel count want.padding = 0; want.callback = audioCallback; want.userdata = NULL; device = SDL_OpenAudioDevice(SDL_GetAudioDeviceName(0, 0), 0, &want, &have, 0); SDL_PauseAudioDevice(device, 0); firstTime = 0; } } else samplesPerFrame = 0; // Next frame return samplesPerFrame; }
注意事项
- 缓冲区大小建议设为多帧数据(比如我这里用了10帧),避免出现缓冲区空的情况
- 如果计算出来的
samplesPerFrame不是整数,需要做一些微调处理,保证指针移动的准确性 - 初始化时要确保缓冲区大小是单帧音频数据大小的整数倍,避免读写指针越界时出现问题
希望这个方案能帮到同样在SDL音频上踩坑的开发者——我当初在网上找了好久相关资料都没找到合适的,折腾了好一阵才搞定。
内容的提问来源于stack exchange,提问作者Francesco




