如何在Pyaudio回调模式中处理in_data?

11 投票
2 回答
21180 浏览
提问于 2025-04-17 20:22

我正在用Python做一个信号处理的项目。到目前为止,我在非阻塞模式下取得了一些小成功,但输出却出现了相当大的延迟和剪切现象。

我想用Pyaudio和Scipy.Signal实现一个简单的实时音频滤波器。但是在Pyaudio示例中提供的回调函数里,当我想读取in_data时,却无法处理它。我尝试了各种转换方法,但都没有成功。

这是我想实现的代码(从麦克风读取数据,过滤,然后尽快输出):

import pyaudio
import time
import numpy as np
import scipy.signal as signal
WIDTH = 2
CHANNELS = 2
RATE = 44100

p = pyaudio.PyAudio()
b,a=signal.iirdesign(0.03,0.07,5,40)
fulldata = np.array([])

def callback(in_data, frame_count, time_info, status):
    data=signal.lfilter(b,a,in_data)
    return (data, pyaudio.paContinue)

stream = p.open(format=pyaudio.paFloat32,
                channels=CHANNELS,
                rate=RATE,
                output=True,
                input=True,
                stream_callback=callback)

stream.start_stream()

while stream.is_active():
    time.sleep(5)
    stream.stop_stream()
stream.close()

p.terminate()

这样做的正确方法是什么呢?

2 个回答

1

我之前在使用PyAudio的回调模式时遇到了类似的问题,但我的需求是:

  • 需要处理立体声输出(两个声道)。
  • 需要实时处理。
  • 需要用一个可以在处理过程中随时改变的任意脉冲响应来处理输入信号。

经过几次尝试,我成功了,以下是我代码的一些片段(基于在这里找到的PyAudio示例):

import pyaudio
import scipy.signal as ss
import numpy as np
import librosa   



track1_data, track1_rate = librosa.load('path/to/wav/track1', sr=44.1e3, dtype=np.float64)
track2_data, track2_rate = librosa.load('path/to/wav/track2', sr=44.1e3, dtype=np.float64)
track3_data, track3_rate = librosa.load('path/to/wav/track3', sr=44.1e3, dtype=np.float64)

# instantiate PyAudio (1)
p = pyaudio.PyAudio()
count = 0
IR_left = first_IR_left # Replace for actual IR
IR_right = first_IR_right # Replace for actual IR

# define callback (2)
def callback(in_data, frame_count, time_info, status):
    global count

    track1_frame = track1_data[frame_count*count : frame_count*(count+1)]
    track2_frame = track2_data[frame_count*count : frame_count*(count+1)]
    track3_frame = track3_data[frame_count*count : frame_count*(count+1)]

    track1_left = ss.fftconvolve(track1_frame, IR_left)
    track1_right = ss.fftconvolve(track1_frame, IR_right)
    track2_left = ss.fftconvolve(track2_frame, IR_left)
    track2_right = ss.fftconvolve(track2_frame, IR_right)
    track3_left = ss.fftconvolve(track3_frame, IR_left)
    track3_right = ss.fftconvolve(track3_frame, IR_right)

    track_left = 1/3 * track1_left + 1/3 * track2_left + 1/3 * track3_left
    track_right = 1/3 * track1_right + 1/3 * track2_right + 1/3 * track3_right

    ret_data = np.empty((track_left.size + track_right.size), dtype=track1_left.dtype)
    ret_data[1::2] = br_left
    ret_data[0::2] = br_right
    ret_data = ret_data.astype(np.float32).tostring()
    count += 1
    return (ret_data, pyaudio.paContinue)

# open stream using callback (3)
stream = p.open(format=pyaudio.paFloat32,
                channels=2,
                rate=int(track1_rate),
                output=True,
                stream_callback=callback,
                frames_per_buffer=2**16)

# start the stream (4)
stream.start_stream()

# wait for stream to finish (5)
while_count = 0
while stream.is_active():
    while_count += 1
    if while_count % 3 == 0:
        IR_left = first_IR_left # Replace for actual IR
        IR_right = first_IR_right # Replace for actual IR
    elif while_count % 3 == 1:
        IR_left = second_IR_left # Replace for actual IR
        IR_right = second_IR_right # Replace for actual IR
    elif while_count % 3 == 2:
        IR_left = third_IR_left # Replace for actual IR
        IR_right = third_IR_right # Replace for actual IR

    time.sleep(10)

# stop stream (6)
stream.stop_stream()
stream.close()

# close PyAudio (7)
p.terminate()

关于上面代码的一些重要思考:

  • 使用librosa而不是wave让我可以用numpy数组进行处理,这比从wave.readframes得到的数据块要好得多。
  • 你在p.open(format=中设置的数据类型必须与ret_data字节的格式匹配。而且PyAudio最多支持float32格式。
  • ret_data中的偶数索引字节会发送到右耳机,奇数索引字节则发送到左耳机。

为了澄清,这段代码将三条音轨的混合信号以立体声的形式发送到输出音频,并且每10秒会改变脉冲响应,从而改变应用的滤波器。我用这个来测试我正在开发的3D音频应用,因此脉冲响应是与头相关的脉冲响应(HRIRs),每10秒改变一次声音的位置。


编辑:
这段代码有个问题:输出中有一种噪音,其频率与帧的大小有关(帧越小,频率越高)。我通过手动进行帧的重叠和相加来修复这个问题。基本上,ss.oaconvolve返回的数组大小是track_frame.size + IR.size - 1,所以我把这个数组分成前track_frame.size个元素(然后用于ret_data),最后IR.size - 1个元素我保存起来以备后用。那些保存的元素会在下一个帧的前IR.size - 1个元素中相加。第一个帧则加上零。

10

我在这段时间找到了我问题的答案,回调函数是这样的:

def callback(in_data, frame_count, time_info, flag):
    global b,a,fulldata #global variables for filter coefficients and array
    audio_data = np.fromstring(in_data, dtype=np.float32)
    #do whatever with data, in my case I want to hear my data filtered in realtime
    audio_data = signal.filtfilt(b,a,audio_data,padlen=200).astype(np.float32).tostring()
    fulldata = np.append(fulldata,audio_data) #saves filtered data in an array
    return (audio_data, pyaudio.paContinue)

撰写回答