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

基于SDL2与C实现无点击声的正弦函数频率切换

解决SDL2正弦波频率切换时的点击声问题

问题背景

使用SDL2开发音频程序,通过按键切换正弦波播放频率时出现明显点击声。尝试过两种方案但无效:

  • 为新频率波形添加相位偏移,使sin(2π*time*freq)与sin(2π*time*new_freq + phase_shift)值相等
  • 对两种频率进行淡入淡出切换

以下是带相位偏移尝试的原代码:

#include <stdio.h>
#include <math.h>
#include <SDL2/SDL.h>

#define PI 3.14159265

typedef void (*periodic_function)(double time, Sint16 *sample, int max);
typedef struct callback_struct {
    int *sample_nr;
    float *frequency;
    float *new_frequency;
    periodic_function periodic_function;
} callback_struct;

void sinewave(double time, Sint16 *sample, int max) {
    double val = sin(time)*max;
    *sample = val;
}

void audio_callback(void *userdata, Uint8 *stream_, int len) {
    Sint16 *stream = (Sint16*)stream_;
    callback_struct *user_data = userdata;

    float sample_len = len / sizeof(Sint16);
    int *sample_nr = (*user_data).sample_nr;

    float *frequency = (*user_data).frequency;
    float *new_frequency = (*user_data).new_frequency;

    float deg;
    Sint16 sample;

    for (int i = 0; i < sample_len; i++, (*sample_nr)++) {
        float time = (*sample_nr) / 48000.0;

        if (*new_frequency != *frequency) {
            // Frequency has changed, add phase.
            double phase_shift = 2 * PI * time * (*frequency - *new_frequency);
            deg = 2 * PI * time * *new_frequency + phase_shift;
            *frequency = *new_frequency;
        } else {
            // Frequency has not changed.
            deg = 2 * PI * time * *frequency;
        }

        (*user_data).periodic_function(deg, &sample, 28000);
        *stream++ = sample;
    }
}

int main(int argc, char* argv[])
{
    SDL_Window *window;
    SDL_Renderer *renderer;

    if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO) < 0)
    {
        printf("Error initializing sdl\n");
        return -1;
    }

    SDL_CreateWindowAndRenderer(680,440, 0, &window, &renderer);
    if(!window)
    {
        printf("Failed to create window\n");
        return -1;
    }

    // Set up user_data struct
    struct callback_struct user_data;
    int sample_nr = 0;
    float frequency = 240.0;
    float new_frequency = 240.0;
    user_data.sample_nr = &sample_nr;
    user_data.frequency = &frequency;
    user_data.new_frequency = &new_frequency;
    user_data.periodic_function = sinewave;

    SDL_AudioSpec want,have;
    SDL_AudioDeviceID dev;

    SDL_memset(&want, 0, sizeof(want));
    want.freq = 48000;
    want.format = AUDIO_S16SYS;
    want.channels = 1;
    want.samples = 1024;
    want.callback = audio_callback;
    want.userdata = &user_data;
    if((dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_FORMAT_CHANGE)) < 0) {
        printf("Error open audio device\n");
        return -1;
    }

    // Play audio
    SDL_PauseAudioDevice(dev, 0);

    // Main loop
    int run_program = 1;
    while(run_program) {
        SDL_Event e;
        while(SDL_PollEvent(&e) > 0)
        {
            switch(e.type) {
                case SDL_QUIT:
                    run_program = 0;
                    break;
                case SDL_KEYDOWN:
                    char key = (char) *SDL_GetKeyName(e.key.keysym.sym);
                    if(key == 'P') {
                        new_frequency = frequency + 1;
                    } else if(key == 'O') {
                        new_frequency = frequency - 1;
                    }
                    continue;
                default:
                    break;
            }       
        }   
    }

    // Stop playing audio
    SDL_PauseAudioDevice(dev, 1);
    
    // Clean up and quit
    SDL_CloseAudioDevice(dev);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

问题根源

  1. 相位计算逻辑错误:原代码仅在检测到频率变化时,对第一个样本计算相位偏移并立刻修改frequency值,但后续样本直接使用新频率基于全局sample_nr计算相位,导致波形连续性断裂——新频率的相位递增速率和旧频率不同,单次偏移无法保证所有后续样本的波形平滑衔接。
  2. 淡入淡出无效可能是实现问题:如果淡入淡出是直接硬切换幅值,而非在多个样本内线性过渡,或者未结合相位连续处理,依然会出现波形突变。

解决方案

核心思路是维护连续的相位状态,而非依赖全局采样数计算时间。当频率变化时,基于当前相位值调整后续相位的递增步长,确保波形的当前采样值和斜率连续,彻底消除突变。

修改后的代码

#include <stdio.h>
#include <math.h>
#include <SDL2/SDL.h>

#define PI 3.14159265
#define SAMPLE_RATE 48000

typedef void (*periodic_function)(double phase, Sint16 *sample, int max);
typedef struct callback_struct {
    double *current_phase;  // 维护连续的相位状态
    float *target_frequency;
    float current_frequency;
    periodic_function periodic_function;
} callback_struct;

void sinewave(double phase, Sint16 *sample, int max) {
    double val = sin(phase) * max;
    *sample = (Sint16)val;
}

void audio_callback(void *userdata, Uint8 *stream_, int len) {
    Sint16 *stream = (Sint16*)stream_;
    callback_struct *user_data = (callback_struct*)userdata;

    int sample_count = len / sizeof(Sint16);
    double *phase = user_data->current_phase;
    float target_freq = *(user_data->target_frequency);
    float current_freq = user_data->current_frequency;

    // 计算每个样本的相位步长
    double phase_step = 2 * PI * current_freq / SAMPLE_RATE;
    Sint16 sample;

    for (int i = 0; i < sample_count; i++) {
        // 生成当前样本
        user_data->periodic_function(*phase, &sample, 28000);
        *stream++ = sample;

        // 更新相位
        *phase += phase_step;
        // 保持相位在0~2π范围内,避免溢出
        while (*phase > 2 * PI) *phase -= 2 * PI;
        while (*phase < 0) *phase += 2 * PI;

        // 如果目标频率变化,平滑切换相位步长
        if (target_freq != current_freq) {
            user_data->current_frequency = target_freq;
            phase_step = 2 * PI * target_freq / SAMPLE_RATE;
        }
    }
}

int main(int argc, char* argv[])
{
    SDL_Window *window;
    SDL_Renderer *renderer;

    if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO) < 0)
    {
        printf("Error initializing sdl\n");
        return -1;
    }

    SDL_CreateWindowAndRenderer(680,440, 0, &window, &renderer);
    if(!window)
    {
        printf("Failed to create window\n");
        return -1;
    }

    // 设置用户数据,维护连续相位
    struct callback_struct user_data;
    double current_phase = 0.0;
    float target_frequency = 240.0;
    user_data.current_phase = &current_phase;
    user_data.target_frequency = &target_frequency;
    user_data.current_frequency = 240.0;
    user_data.periodic_function = sinewave;

    SDL_AudioSpec want,have;
    SDL_AudioDeviceID dev;

    SDL_memset(&want, 0, sizeof(want));
    want.freq = SAMPLE_RATE;
    want.format = AUDIO_S16SYS;
    want.channels = 1;
    want.samples = 1024;
    want.callback = audio_callback;
    want.userdata = &user_data;
    if((dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_FORMAT_CHANGE)) < 0) {
        printf("Error open audio device\n");
        return -1;
    }

    // 开始播放
    SDL_PauseAudioDevice(dev, 0);

    // 主循环
    int run_program = 1;
    while(run_program) {
        SDL_Event e;
        while(SDL_PollEvent(&e) > 0)
        {
            switch(e.type) {
                case SDL_QUIT:
                    run_program = 0;
                    break;
                case SDL_KEYDOWN:
                    char key = (char)*SDL_GetKeyName(e.key.keysym.sym);
                    if(key == 'P') {
                        target_frequency += 1.0;
                    } else if(key == 'O') {
                        target_frequency -= 1.0;
                    }
                    continue;
                default:
                    break;
            }       
        }   
    }

    // 停止播放
    SDL_PauseAudioDevice(dev, 1);
    
    // 清理资源
    SDL_CloseAudioDevice(dev);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

关键修改点

  1. 新增current_phase变量:代替原有的sample_nr,持续维护当前波形的相位,确保每次采样都基于上一次的相位递增,保证波形连续性。
  2. 相位步长动态调整:当目标频率变化时,直接更新相位递增步长为新频率对应的步长,由于相位本身连续,新频率的波形会从当前相位点自然延续,不会产生突变。
  3. 相位范围约束:将相位保持在0~2π范围内,避免浮点数溢出导致的精度问题。

如果需要更平滑的过渡(比如避免频率突变带来的瞬时频谱变化),可以在多个样本内逐步调整相位步长:

  • 设置过渡样本数(如100个样本)
  • 每次采样时将当前步长向目标步长逼近一小部分
  • 直到步长完全等于目标频率的步长

这种方式可以进一步消除频率切换时的细微听觉变化,但对于小幅度频率调整(如±1Hz),直接切换步长已经足够消除点击声。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 03:54:56