
本文旨在解决使用 PyAudio 库播放声音时,如何在不预先设定播放时长的情况下,实现声音的持续播放,并通过外部事件(例如按键释放)来停止声音。我们将分析问题代码,找出循环播放的错误原因,并提供修改方案,最终实现灵活的声音控制。
在使用 PyAudio 库进行音频编程时,一个常见的需求是在不预先确定播放时长的情况下,持续播放声音,并根据外部事件(例如,MIDI 控制器的按键释放)来停止播放。原始代码尝试通过一个 while 循环来实现这个功能,但存在一些逻辑上的问题,导致声音无法持续播放,或者无法正确停止。
分析与改进
原始代码的核心问题在于 while play == True 循环内部的逻辑。让我们再次审视一下:
while play == True:
start_time = time.time()
stream.write(vysledek)
time.sleep(1)
play = False
stream.stop_stream()
play = True这段代码的意图是循环播放声音数据 vysledek。然而,在每次循环迭代中,它会执行以下操作:
- 写入声音数据 stream.write(vysledek)。
- 暂停 1 秒 time.sleep(1)。
- 将 play 设置为 False,从而结束循环。
- 停止音频流 stream.stop_stream()。
- 在循环外部将 play = True,为下次MIDI触发做准备。
因此,循环只执行一次,导致声音只播放一秒钟。此外,stream.stop_stream() 被放置在循环内部,导致每次播放结束后立即停止音频流,这与持续播放的需求相悖。
解决方案
要实现持续播放,我们需要修改代码,移除不必要的 time.sleep(1) 和 play = False,并将 stream.stop_stream() 移到循环外部。更重要的是,我们需要根据MIDI消息来控制play变量,从而控制声音的播放和停止。
以下是修改后的代码示例:
import time
from rtmidi.midiutil import open_midiinput
import numpy as np
import pyaudio
p = pyaudio.PyAudio()
play = False # 初始化为False,只有按下按键才播放
volume = 0.5
fs = 44100
fA = 440.0
fB = 493.88
fC = 523.25
fD = 587.33
frekvence = 440
frekvence_seznam = {
(144, 32): fA,
(144, 33): fB,
(144, 34): fC,
(144, 35): fD,
}
port = 0
midiin, port_name = open_midiinput(port)
stream = None # 初始化stream为None
try:
while True:
msg = midiin.get_message()
if msg:
message = msg
klic = message[0]
lepsi_klic = tuple(klic[:2])
print(message[0])
if lepsi_klic in frekvence_seznam:
print("je to tam")
frekvence = frekvence_seznam[lepsi_klic]
period = 2 * np.pi
x = period * np.arange(fs * 1) * frekvence / fs # duration改为1,或者更小的值,减少计算量
sinus = np.sin(x)
square = np.sign(sinus)
triangle = 2/np.pi * np.arcsin(np.sin(x))
saw = abs((x % period) - 1)
curvy_triangle = (abs((x % period) - 1)) ** 2
samples = (triangle).astype(np.float32)
vysledek = volume * samples
# 启动播放
if not stream: # 如果stream未初始化,则初始化
stream = p.open(format=pyaudio.paFloat32,
channels=1,
rate=fs,
output=True)
play = True # 开始播放
elif lepsi_klic == (128, 32) or lepsi_klic == (128, 33) or lepsi_klic == (128, 34) or lepsi_klic == (128, 35): # 假设128代表按键释放
print("停止播放")
play = False # 停止播放
elif lepsi_klic == (144, 81):
break # 退出主循环
if play and stream: # 只有在play为True且stream已初始化时才播放
stream.write(vysledek)
except KeyboardInterrupt:
print("Interrupted by user")
finally:
if stream:
stream.stop_stream()
stream.close()
p.terminate()
midiin.close_port()
del midiin修改说明:
- 初始化 play 为 False: 确保程序启动时声音不会自动播放。
- 移除 time.sleep(1) 和 play = False: 避免循环提前结束。
- 将 stream.stop_stream() 和 stream.close() 移到循环外部: 在程序退出前才关闭音频流。
- 根据 MIDI 消息控制 play 变量: 当检测到按键按下时,将 play 设置为 True,当检测到按键释放时,将 play 设置为 False。这里假设MIDI消息144代表按键按下,128代表按键释放,你需要根据你的MIDI设备实际情况进行调整。
- 在主循环中持续写入数据: 当 play 为 True 时,持续将声音数据写入音频流。
- duration改为1: 在生成声音数据时,duration改为1,或者更小的值,减少计算量,提高响应速度。
- 增加stream初始化判断: 在启动播放前,判断stream是否已经初始化,如果没有初始化,则初始化stream。
- 异常处理: 增加try...except...finally块,确保程序在异常情况下也能正确关闭音频流和MIDI端口。
注意事项
- MIDI 消息: 确保你了解你的 MIDI 设备发送的按键按下和释放消息的格式。不同的设备可能使用不同的消息类型和值。
- 性能: 持续写入音频数据可能会消耗大量的 CPU 资源。如果性能成为问题,可以考虑使用缓冲技术来减少写入频率。
- 异常处理: 在实际应用中,应该添加更完善的错误处理机制,以应对各种可能出现的异常情况。
总结
通过修改循环逻辑,并根据 MIDI 消息动态控制播放状态,我们成功实现了在不预先设定播放时长的情况下,持续播放声音,并通过外部事件来停止播放的功能。这个方法可以应用于各种需要实时控制音频播放的场景,例如音乐游戏、交互式音频装置等。









