基于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; }
问题根源
- 相位计算逻辑错误:原代码仅在检测到频率变化时,对第一个样本计算相位偏移并立刻修改
frequency值,但后续样本直接使用新频率基于全局sample_nr计算相位,导致波形连续性断裂——新频率的相位递增速率和旧频率不同,单次偏移无法保证所有后续样本的波形平滑衔接。 - 淡入淡出无效可能是实现问题:如果淡入淡出是直接硬切换幅值,而非在多个样本内线性过渡,或者未结合相位连续处理,依然会出现波形突变。
解决方案
核心思路是维护连续的相位状态,而非依赖全局采样数计算时间。当频率变化时,基于当前相位值调整后续相位的递增步长,确保波形的当前采样值和斜率连续,彻底消除突变。
修改后的代码
#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 = ¤t_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; }
关键修改点
- 新增
current_phase变量:代替原有的sample_nr,持续维护当前波形的相位,确保每次采样都基于上一次的相位递增,保证波形连续性。 - 相位步长动态调整:当目标频率变化时,直接更新相位递增步长为新频率对应的步长,由于相位本身连续,新频率的波形会从当前相位点自然延续,不会产生突变。
- 相位范围约束:将相位保持在
0~2π范围内,避免浮点数溢出导致的精度问题。
如果需要更平滑的过渡(比如避免频率突变带来的瞬时频谱变化),可以在多个样本内逐步调整相位步长:
- 设置过渡样本数(如100个样本)
- 每次采样时将当前步长向目标步长逼近一小部分
- 直到步长完全等于目标频率的步长
这种方式可以进一步消除频率切换时的细微听觉变化,但对于小幅度频率调整(如±1Hz),直接切换步长已经足够消除点击声。
内容的提问来源于stack exchange,提问作者marius627
相关产品推荐
相关产品推荐

