You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动