Pygame按频率发声:音符持音不流畅、多音叠加异常问题求助
问题与解决方案
问题概述
我用Pygame写了一个简易钢琴程序,通过A/S/D/F按键播放对应音符,但遇到三个问题:
- 音符无法流畅持续发声,有明显卡顿
- 同时按下两个按键时会产生怪异的杂音
- 必须激活Pygame的小窗口才能触发按键绑定,操作不便
原代码:
import pygame import numpy as np import math pygame.init() # Set the sample rate and bit size for the audio pygame.mixer.init(frequency=44100, size=-16, channels=10, buffer=2**12) # Create a small window in the background (1x1 size) pygame.display.set_mode((1, 1), pygame.NOFRAME) # Define the piano sounds frequencies = [ {"Note": "C2", "Frequency": 65.41}, {"Note": "C", "Frequency": 130.81}, {"Note": "D", "Frequency": 146.83}, {"Note": "E", "Frequency": 164.81}, # Add more notes as needed ] # Create a dictionary to map key codes to notes and channels key_mapping = { pygame.K_a: {"Note": "C2", "Channel": None}, pygame.K_s: {"Note": "C", "Channel": None}, pygame.K_d: {"Note": "D", "Channel": None}, pygame.K_f: {"Note": "E", "Channel": None}, # Add more key mappings as needed } # Create sound objects for each note sounds = {} for note_info in frequencies: note = note_info["Note"] frequency = note_info["Frequency"] wave_array = np.array([int(32767.0 * math.sin(2.0 * math.pi * frequency * t / 44100.0)) for t in range(44100)], dtype=np.int16) sound = pygame.mixer.Sound(wave_array) sounds[note] = sound # Main loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: key = event.key if key in key_mapping: note_info = key_mapping[key] note = note_info["Note"] if note_info["Channel"] is None: prit(f"Played {note}") channel = pygame.mixer.find_channel() if channel: channel.play(sounds[note], loops=-1) note_info["Channel"] = channel elif event.type == pygame.KEYUP: key = event.key if key in key_mapping: note_info = key_mapping[key] note = note_info["Note"] if note_info["Channel"]: print(f"Stopped {note}") note_info["Channel"].stop() note_info["Channel"] = None # Clean up pygame.quit()
解决方案
1. 解决音符卡顿问题
原代码的正弦波仅生成1秒样本,循环播放时首尾相位不连续,导致卡顿。修改方案:
- 生成更长的波形(比如2秒)
- 给波形添加淡入淡出处理,让首尾音量平滑过渡,消除衔接杂音
- 修复代码中的笔误:
prit改为print
2. 解决多音叠加杂音问题
纯正弦波叠加会产生拍频效应(音量周期性波动的杂音),真实钢琴音色是基波+泛音的复合波形。修改方案:
- 给每个音符添加多阶泛音,模拟真实音色
- 归一化波形音量,避免音频过载失真
3. 解决需激活窗口才能按键的问题
Pygame默认仅在窗口激活时接收键盘事件,用pynput库实现全局键盘监听,无需激活窗口。需要先安装:pip install pynput
修改后的完整代码
import pygame import numpy as np import math from pynput.keyboard import Key, Listener pygame.init() # 缩小缓冲区减少音频延迟,保留足够通道数 pygame.mixer.init(frequency=44100, size=-16, channels=10, buffer=2**10) # 创建无框窗口并最小化,避免干扰操作 screen = pygame.display.set_mode((1, 1), pygame.NOFRAME) pygame.display.iconify() # 定义音符,新增泛音参数模拟真实音色 frequencies = [ {"Note": "C2", "Frequency": 65.41, "Harmonics": [1, 0.5, 0.3, 0.2]}, {"Note": "C", "Frequency": 130.81, "Harmonics": [1, 0.6, 0.4, 0.25]}, {"Note": "D", "Frequency": 146.83, "Harmonics": [1, 0.55, 0.35, 0.2]}, {"Note": "E", "Frequency": 164.81, "Harmonics": [1, 0.6, 0.3, 0.15]}, ] # 按键映射,记录每个按键对应的音符和播放通道 key_mapping = { 'a': {"Note": "C2", "Channel": None}, 's': {"Note": "C", "Channel": None}, 'd': {"Note": "D", "Channel": None}, 'f': {"Note": "E", "Channel": None}, } # 生成带泛音和淡入淡出的音效 sounds = {} sample_rate = 44100 duration = 2 # 生成2秒波形,保证循环衔接自然 fade_samples = 1000 # 淡入淡出的样本长度,消除卡顿 for note_info in frequencies: note = note_info["Note"] base_freq = note_info["Frequency"] harmonics = note_info["Harmonics"] # 生成基波+多阶泛音的复合波形 t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) wave = np.zeros_like(t) for harmonic_idx, amp in enumerate(harmonics): wave += amp * np.sin(2 * math.pi * base_freq * (harmonic_idx + 1) * t) # 归一化音量,防止叠加时失真 wave = wave / np.max(np.abs(wave)) * 0.7 * 32767 # 淡入淡出处理,平滑波形首尾 wave[:fade_samples] *= np.linspace(0, 1, fade_samples) wave[-fade_samples:] *= np.linspace(1, 0, fade_samples) wave_array = wave.astype(np.int16) sound = pygame.mixer.Sound(wave_array) sounds[note] = sound # 全局按键按下事件处理 def on_press(key): try: char = key.char.lower() if char in key_mapping: note_info = key_mapping[char] if note_info["Channel"] is None: note = note_info["Note"] print(f"Played {note}") channel = pygame.mixer.find_channel() if channel: channel.play(sounds[note], loops=-1) note_info["Channel"] = channel except AttributeError: # 处理非字符按键(如Shift、Ctrl),忽略即可 pass # 全局按键释放事件处理 def on_release(key): try: char = key.char.lower() if char in key_mapping: note_info = key_mapping[char] if note_info["Channel"]: note = note_info["Note"] print(f"Stopped {note}") note_info["Channel"].stop() note_info["Channel"] = None # 按ESC键退出程序 if key == Key.esc: pygame.quit() return False except AttributeError: pass # 启动全局键盘监听 listener = Listener(on_press=on_press, on_release=on_release) listener.start() # 保持程序运行,直到监听结束 while listener.is_alive(): pygame.time.Clock().tick(10) listener.join()
内容的提问来源于stack exchange,提问作者CodingStark09
相关产品推荐
相关产品推荐

