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

Python实现键盘按键触发多正弦波同时播放的问题求助

多音调同时播放的Python按键发声程序解决方案

问题背景

编程基础薄弱,为大学研讨课开发Python按键触发发声程序,使用PyCharm实现,但当前按下"A"或"S"键只能依次播放对应音调,必须等待当前播放结束才能触发下一个,无法实现双音调同时叠加播放,需要修改代码实现多音同步输出。

原代码

import pyaudio
import numpy as np
import math
import struct
import keyboard

FS = 44100  #  frames per second, samples per second or sample rate

def play_sound(type, frequency, volume, duration):
   generate_sound(type, frequency, volume, duration)

def generate_sound(type, frequency, volume, duration):
    outbuf = np.random.normal(loc=0, scale=1, size=int(float(duration / 1000.0)*FS))

    if type == "sine":
        dur = int(FS * float(duration / 1000.0))
        theta = 0.0
        incr_theta = frequency * 2 * math.pi / FS # frequency increment normalized for sample rate
        for i in range(dur):
            outbuf[i] = volume * math.sin(theta)
            theta += incr_theta

    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paFloat32, channels=1, rate=FS, output=True)
    data = b''.join(struct.pack('f', samp) for samp in outbuf) # must pack the binary data
    stream.write(data)
    stream.stop_stream()
    stream.close()
    p.terminate()

#thread here maybe?

while True:
    if keyboard.is_pressed("a"):
        play_sound("sine",200, 0.5, 3000)
    if keyboard.is_pressed("s"):
        play_sound("sine",300,0.5,3000)
    elif keyboard.is_pressed("esc"):
        break

解决方案

原代码的核心问题是每次播放都创建独立的PyAudio实例和流,且stream.write()是阻塞操作,导致无法同时输出多音调。要实现多音叠加,需要改为单流持续输出+实时波形叠加的模式,具体修改如下:

修改后完整代码

import pyaudio
import math
import struct
import keyboard
import threading
import time

FS = 44100  # 采样率
VOLUME = 0.3  # 全局音量,避免叠加后爆音
CHUNK_SIZE = 1024  # 每次输出的音频块大小

# 保存当前激活的音符(频率)
active_notes = set()
# 每个音符的相位跟踪,避免切换时波形断层
note_phases = {}

def generate_sine_sample(frequency, phase):
    """生成单个采样点的正弦波值"""
    sample = VOLUME * math.sin(phase)
    # 更新相位
    new_phase = phase + (frequency * 2 * math.pi / FS)
    # 相位取模,防止数值过大
    return sample, new_phase % (2 * math.pi)

def audio_loop():
    """持续输出叠加后的音频流"""
    p = pyaudio.PyAudio()
    # 打开持续输出的流
    stream = p.open(
        format=pyaudio.paFloat32,
        channels=1,
        rate=FS,
        output=True,
        frames_per_buffer=CHUNK_SIZE
    )

    while True:
        chunk = []
        for _ in range(CHUNK_SIZE):
            sample = 0.0
            # 叠加所有激活音符的采样
            for freq in list(active_notes):
                # 初始化相位(如果是新音符)
                if freq not in note_phases:
                    note_phases[freq] = 0.0
                s, new_phase = generate_sine_sample(freq, note_phases[freq])
                sample += s
                note_phases[freq] = new_phase
            # 叠加后做限幅,防止音量过大失真
            sample = max(min(sample, 1.0), -1.0)
            chunk.append(sample)
        
        # 打包成二进制数据写入流
        data = b''.join(struct.pack('f', samp) for samp in chunk)
        stream.write(data)
        
        # 短暂休眠,降低CPU占用
        time.sleep(0.001)

def main():
    # 启动音频输出线程
    audio_thread = threading.Thread(target=audio_loop, daemon=True)
    audio_thread.start()

    # 按键状态跟踪,避免重复触发
    key_states = {"a": False, "s": False}

    print("按下A/S键播放音调,ESC键退出")
    while True:
        # 检测A键:200Hz
        if keyboard.is_pressed("a"):
            if not key_states["a"]:
                active_notes.add(200)
                key_states["a"] = True
        else:
            if key_states["a"]:
                active_notes.discard(200)
                note_phases.pop(200, None)
                key_states["a"] = False
        
        # 检测S键:300Hz
        if keyboard.is_pressed("s"):
            if not key_states["s"]:
                active_notes.add(300)
                key_states["s"] = True
        else:
            if key_states["s"]:
                active_notes.discard(300)
                note_phases.pop(300, None)
                key_states["s"] = False
        
        # 退出程序
        if keyboard.is_pressed("esc"):
            break

if __name__ == "__main__":
    main()

关键修改说明

  • 线程分离:用threading把音频输出循环放到后台线程,主线程专注于按键检测,避免阻塞。
  • 实时波形叠加:维护active_notes集合保存当前按下的音符,每次生成音频块时叠加所有激活音符的采样值。
  • 相位跟踪:用note_phases字典记录每个音符的当前相位,避免按键松开再按下时波形出现断层。
  • 限幅处理:叠加后的采样值做max(min(sample, 1.0), -1.0)处理,防止多个音调叠加导致音量过大失真。
  • 按键状态跟踪:用key_states避免按键按住时重复添加音符到集合中。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 03:17:07