嘿!您似乎在 United States,您想使用我们的 English 网站吗?
Switch to English site
Skip to main content

使用 Raspberry Pi 和 Pmods 进行音频处理

在这个项目中,我们将使用 Raspberry Pi 和 Digilent Pmods 对输入音频信号应用不同的音频效果。 用户界面控制效果的程度和类型。

零件清单

数量 产品 库存编号
1 Raspberry Pi 4 B 8GB 182-2098
1 DesignSpark Pmod HAT with 3 Digilent Pmod Sockets for Raspberry Pi 144-8419
1 Digilent Analog-to-Digital Converter Expansion Module 410-064 134-6443
1 Digilent Digital to Analog Converter Expansion Module 410-241 134-6456
1 Digilent LED Expansion Module 410-163 134-6450
1 Digilent Expansion Module 410-135 136-8061
1 Digilent Rotary Encoder Expansion Module 410-117 410-117
1 RS PRO 3.5 mm PCB Mount Stereo Jack Socket, 5Pole 913-1021
1 Digilent Analog Discovery 2 PC Based Oscilloscope, 30MHz, 2 Channels 134-6480
1 Digilent, 240-000 184-0451

介绍

主要任务是将音频效果应用于输入信号。 为了使事情尽可能简单,为这个项目选择的两个音频效果作为概念证明是回声效果和弯音。 这些影响的性质将在本指南后面讨论。

Block diagram showing circuit operation

Raspberry Pi 具有几乎实时更改输入信号的处理能力。 默认情况下,它缺少获取和回放这些信号所需的外围设备。 然而,在 Pmod HAT Adapter 的帮助下,Digilent Pmods 这个种类繁多的即插即用外围模块可以很容易地连接到单板计算机。 我们捕捉声音,改变它,然后回放。 首先,音频信号被送入模数转换器 (ADC)。 大多数 ADC 无法直接处理原始音频输入信号,因此可能需要调节信号。 处理数字信号后,必须借助数模转换器 (DAC) 将其转换回模拟域。 在将生成的信号发送到放大器或有源扬声器之前,可能再次需要进行调节。

为了控制音频效果,我们创建了用户界面。 对于此应用程序,将使用三个控制元件:用于设置应用效果程度的旋转编码器、用于更改效果类型的开关和用于重置设备状态的按钮。 由于用户需要有关控件状态的反馈,因此将使用由 8 个 LED 组成的 LED 条形图作为显示。 开机指示灯 LED 也很有用。 这可以显示外部电路是否通电。

设置Raspberry Pi

我们将在这个项目中使用 Python 3,它预装在 Raspberry Pi OS(3.7 版)上,但默认的 Python 是 Python 2。为了方便起见,应该将 Python 3 设置为默认解释器。 在 Raspberry Pi 上打开一个终端,然后键入:

sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1

接下来,应使用以下命令安装或升级必要的 Python 包到最新版本:

pip install numpy matplotlib gpiozero RPi.GPIO spidev --upgrade

由于我们将使用在 SPI 接口上控制的外设,因此应启用该接口。 使用以下命令打开配置设置:

sudo raspi-config

选择“Interface Options”,然后“SPI”通过选择“Yes”启用接口。

输入信号

Pmod AD1

Image of a Pmod AD1 board

Raspberry Pi 无法直接采样模拟信号,因此我们使用 Digilent Pmod AD1,一个 12 位和 1MS/s ADC。 Pmod AD1 由 Analog Devices AD7476A供电。 它通过 SPI 接口与 Raspberry Pi 通信。

转换遵循这个公式:n=212*Vin/Vref,其中n是输出数,Vin是输入电压,Vref是参考电压,它等于电源电压(3.3V)。 但是,ADC 无法处理任何低于 0V 或高于参考电压的电压。 尽管大多数设备上的音频信号幅度非常低(大约 1V),但它有 0V 偏移。 电压范围在 -1V 和 1V 之间。 为了解决这个问题,必须建立一个调节电路。

输入信号调理

由于音频信号的幅度远低于参考电压 (2*A<Vref),因此只需向信号添加一个正偏移,即可将其移至 0V 以上。 为此,将使用一个求和放大器,如下图所示。

Input Signal Conditioning Circuit

在此配置中,所需的偏移电压由电阻器 R4 和 R5 设置:Voffset=VSS*R4/(R4+R5),其中 VSS 是负电源电压。 输出电压由下式得到:Vout=-(Vin*R2/R1+Voffset*R2/R3)=-(Vin*R2/R1+VSS*R4/(R4+R5)),此时 电阻器 R4 和 R5 设置偏移电压,电阻器 R1 和 R2 设置放大率。 即使信号被反转,这也不会对电路产生任何影响。

电源

虽然 Raspberry Pi 在引脚 2 和 4 上有 5V 电源,但调节电路需要负电源。 为了获得负电源电压,我们可以使用LTM8067 隔离式 DC-DC 转换器。 首先,我们将输入连接到 5V 电源和地。 然后,我们将转换器的正输出引脚接地。 由于输入和输出是隔离的,正极接地不会使模块短路。 与 Raspberry Pi 的接地相比,负极引脚的电压电位将低于 0V。 不要尝试使用非隔离转换器! 使用电压表测量负输出电压。 用螺丝刀转动电位器,直到得到-5V。

输出信号

Pmod DA3

Image of a Pmod DA3 board

树莓派只有一个模拟输出,3.5mm 音频插孔,用于系统音频。 使用 Digilent Pmod DA3 为处理后的音频信号提供单独的输出。 Pmod DA3 是一款由 Analog Devices AD5541A供电的 16 位 DAC。 Pmod DA3 可以通过 SPI 接口与树莓派进行通信。

转换遵循这个公式:Vout=n*Vref/216,Vout是输出电压,n是输入数,Vref是参考电压,等于2.5V(内部参考)。 由于 DAC 只能处理 16 位无符号数,因此在输出中无法获得低于 0V 或高于 Vref 的电压。 然而,放大器或有源扬声器“等待”具有 0V 偏移且通常最大 1V 幅度的输入信号,因此需要调节输出信号。

输出信号调理

0-2.5V 范围允许输出信号幅度为 1V,如果它具有至少 1V 的偏移。 可以通过一个去耦电容和一个电压跟随器来消除偏移。 输出中可能还需要一个低通滤波器。 电压跟随器的负电源取自前面提到的DC-DC转换器,它是一个开关稳压器(反激式转换器),因此会产生高频开关噪声。 由于 Raspberry Pi 的速度限制,采样率也受到限制。 在降低采样率的情况下,输出可能会出现尖锐的边缘,因此也应滤除输出频率的谐波。

人类语音中的元音可以达到 2KHz 的频率,而辅音可以达到 6KHz 的频率。 如果使用简单的低通滤波器,将截止频率设计在 3KHz 和 4KHz 之间似乎是合理的,因为大多数声音低于 3500Hz ().

Output Signal Conditioning Circuit

如果使用标准电阻和电容值,滤波器的截止频率变为 fc=1/(2*π*R8*C2)=3.4KHz。

用户界面

Pmod ENC

Image of a Pmod ENC board

使用 Pmod ENC,我们可以使用一个开关来打开音频处理,一个旋转编码器来设置效果的程度,以及一个重置按钮。

Pmod 8LD

Image of a Pmod 8LD board

Pmod 8LD 包含 8 个由低功率逻辑电平控制的高亮度 LED。 这可以给用户反馈。

电源指示灯

虽然 Raspberry Pi 有一个通电 LED,但第二个指示灯可用于指示调节电路是否通电。 要构建电源指示器,只需将 LED 与限流电阻串联到 5V 电源。

Power Indicator Circuit

限流电阻的值可以通过以下公式计算:R9=(VCC-VLED)/ILED, 其中VLED 是 LED 的正向电压(红色 LED 通常约为 1.8V),ILED 是通过的所需电流 发光二极管。 必须选择电阻器以将此电流设置为低于最大值。 LED 的亮度与通过它的电流成正比。 如果需要调光指示器,则必须选择更高阻值的电阻。

将 Pmod 与 Raspberry Pi 连接

Pmod HAT Adapter

Image of Pmod HAT Adapter

我们可以通过 Pmod HAT Adapter 将 Digilent Pmods 连接到 Raspberry Pi。 Pmod HAT 适配器将 40 针 Raspberry Pi GPIO 连接器拆分为三个 2x6 Digilent Pmod 连接器(JA、JB 和 JC),它们中的每一个也可以用作两个单独的 1x6 Pmod 连接器(例如 JA 可以分离到 JAA 和 JAB)。 所有 Pmod 端口都包含一个接地和一个 3.3V 引脚,用于为连接的 Pmod 供电。 虽然所有端口都可以用作 GPIO(通用输入/输出),但某些端口具有附加功能:JAA 和 JBA 可用于连接具有 SPI 接口的 Pmod,I2C 接口可用于 JBB 端口和 JCA 上的 UART。 适配器可以直接从 Raspberry Pi 供电,也可以通过 DC 筒形插孔从外部 5V 电源供电(不要同时使用两者!)。

建议使用以下连接:

Pmod HAT Adapter Port Connected Pmod Protocol Used
JAA Pmod AD1 SPI
JAB Pmod ENC GPIO
JBA Pmod DA3 SPI
JC Pmod 8LD GPIO

要将 Pmod AD1 和 Pmod ENC 都连接到 Pmod HAT 适配器的 JA 端口,可以使用 Pmod TPH2 12 点测试接头。

Image of Pmod TPH2 board

完整的电路

调节电路后,将负电源和电源指示灯组装在面包板上,将 5V 轨连接到 40 针 Raspberry Pi GPIO 连接器上的引脚 2,将 GND 轨连接到引脚 39。这样,面包板上的电路将 被供电。 将第一个调理电路的输出连接到 Pmod AD1 的 A1 通道,将第二个调理电路的输入连接到 Pmod DA3 的 SMA 连接器(也可以将 MTE 电缆代替公头 SMA 连接器插入插头)。

The complete circuit

软件

如前所述,控制音频处理器的软件将用 Python3 编写。 该项目由六个模块组成,将以自上而下的方式呈现。

main.py

主模块包含项目最重要的设置并初始化其他模块。 每个重要的数量都应该出现在一个易于访问的地方,比如主模块的开头,以便于调整。

# global variables
spi_clock_speed = int(4e06)   # spi clock frequency in Hz
sample_time = 5e-05  # seconds between samples
buffer_size = 5000  # data points in the buffer
DEBUG = "None"  # "ADC", "DAC", "PROC", "ALL" or "None"
adc_res = 4095  # resolution of the ADC
dac_res = 65535  # resolution of the DAC

树莓派有 4 个重要任务要做:接收音频输入、处理音频信号、发送出去以及与用户通信。 如果这些任务一个接一个地完成,有两大缺陷

1. 输入语音和输出语音之间有很大的延迟(信号被记录、处理和回放的时间)

2. 输出语音中断。

为避免这些情况,必须并行完成任务。

用户界面可以通过 gpiozero Python 模块实现,该模块使用异步事件(如微控制器上的中断)与用户进行通信。 主模块只是为这些事件分配动作。

# set user interface actions
# increment/decrement a value, when the rotary encoder is rotated
UI.enc.when_rotated = UI.set_value
# reset the value, when the button is pressed
UI.btn.when_pressed = UI.reset_value
# set a flag according to the state of the switch
UI.swt.when_pressed = UI.change_mode
UI.swt.when_released = UI.change_mode

Raspberry Pi 4 Model B 有一个四核 Cortex-A72 处理器,这使我们能够通过多处理 Python 模块在不同的处理器内核上运行任务。 首先,主进程只会初始化其他子进程。 一个子进程记录输入数据,另一个子进程处理数据,最后一个子进程将其回放。

为了避免输出中断,使用了三个共享缓冲区:记录器进程一个接一个地填充三个缓冲区。 如果第一个缓冲区被播放器进程清空,则整个进程重新开始。 数据处理等待记录器并修改缓冲区中的内容。

Diagram showing the three shared buffers

共享标志用于指示每个缓冲区的状态。

# create shared lists
manager = multiprocessing.Manager()
# 3 buffers to use them in rotation
buffer = manager.list([[], [], []])
# flags to signal aquisition state
get_flag = manager.list([False, False, False])
# flags to signal processing state
set_flag = manager.list([False, False, False])
# flags to signal write-out state
ready_flag = manager.list([True, True, True])

包装器启动子进程,然后等待它们完成(程序按 Ctrl+C 退出)。

# main part
if __name__ == "__main__":
    UI.reset_value()   # reset counter

    # initialize processes
    acquisition = multiprocessing.Process(target=DI.acquire_data)
    processing = multiprocessing.Process(target=DP.process_data)
    playing = multiprocessing.Process(target=DO.output_data)

    # start threads
    acquisition.start()
    processing.start()
    playing.start()

    # wait for exit condition
    acquisition.join()
    processing.join()
    playing.join()

    UI.reset_value()   # reset counters

    # terminate processes
    acquisition.terminate()
    processing.terminate()
    playing.terminate()

user_interface.py

用户界面模块包含所有用户交互功能。 这些功能

1. 根据旋转编码器的状态设置一个变量

2. 根据此变量点亮 LED

3. 更改不同开关位置上的标志状态(必须向上或向下拉动开关,否则无法检测到边缘)

4. 按下重置按钮时重置所有值和标志。

def set_value():
    # map the counter between 0 and 1 using the rotary encoder
    global param
    param[0] = enc.steps / (2 * enc.max_steps) + 0.5
    set_leds()  # set LED states
    return
def set_leds():
    global param
    # set the leds on/off according to the counter
    if param[1]:
        led.value = param[0]
    else:
        led.value = -param[0]
    return
def change_mode():
    # switch the flag
    global param
    param[1] = bool(swt.value)
    # force software pull-up/-down
    if param[1]:
        GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    else:
        GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    set_leds()  # set LED states
    return
def reset_value():
    # reset the counter
    global param
    param[0] = 0
    enc.steps = -enc.max_steps  # reset rotary encoder state
    param[1] = bool(swt.value)  # reset switch state
    set_leds()  # reset LED states
    return

该模块利用 gpiozero Python 包的成员来更轻松地处理输入/输出设备。

# initialize devices
# Rotary Encoder
enc = RotaryEncoder(19, 21)
btn = Button(20)
swt = Button(18)

# pull down the switch
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

# LEDs
led = LEDBarGraph(16, 14, 15, 17, 4, 12, 5, 6)

接收到的值和标志存储在共享列表中,以便其他进程可以使用它们。

# shared user-interface parameters
manager = multiprocessing.Manager()
param = manager.list([0, False])

data_input.py

数据输入模块负责使用 spidev Python 包初始化与 Pmod AD1 的 SPI 通信。 该模块用接收到的 12 位数据字填充缓冲区,在每次采集后等待预定义的时间(样本之间的等待是必要的,以确保两个样本之间的时间始终相同 - 否则可能会发生音调偏移),以及 当缓冲区被填满时设置标志,以向其他进程发送其状态信号。

# initialize ADC
adc = spidev.SpiDev()
adc.open(SPI_port, CS_pin)
adc.max_speed_hz = main.spi_clock_speed
for _ in range(main.buffer_size):
  # measure start time
  start_time = time.perf_counter()

  # read data bytes
  adc_raw = adc.readbytes(2)
  # recreate the number from the bytes
  adc_number = adc_raw[1] | (adc_raw[0] << 8)
  # insert the number in the buffer
  buff.append(adc_number)

  # check the duration of the operation
  duration = time.perf_counter() - start_time
  # wait if necessary
  if main.sample_time > duration:
    time.sleep(main.sample_time - duration)
# assign buffer and set flags
if main.ready_flag[0]:
  main.buffer[0] = buff
  main.get_flag[0] = True
  main.ready_flag[0] = False
  continue_flag = True
elif main.ready_flag[1]:
  main.buffer[1] = buff
  main.get_flag[1] = True
  main.ready_flag[1] = False
  continue_flag = True
elif main.ready_flag[2]:
  main.buffer[2] = buff
  main.get_flag[2] = True
  main.ready_flag[2] = False
  continue_flag = True

data_output.py

数据输出模块与数据输入模块非常相似。 它使用 spidev Python 包通过 SPI 控制 DAC。 但是,输出模块会在处理缓冲区之前检查描述三个缓冲区状态的全局标志。 缓冲器中的样本发送到 DAC 后,等待时间可能不等于 ADC 的等待时间。 这是因为每个缓冲区的第一个元素包含有关所需的音高偏移的信息(以应用该效果)。

# output buffer
if case != None and len(buff) != 0:
  # calculate the duration of a sample
  # (this is needed because of the pitchbend effect)
  sample_duration = main.sample_time - buff[0]
  # discard the first sample
  # (this contains information about the pitch)
  buff.pop(0)

  # output every sample
  for point in buff:
    # measure start time
    start_time = time.perf_counter()

    # get high byte
    highbyte = point >> 8
    # get low byte
    lowbyte = point & 0xFF
    # send both bytes
    dac.writebytes([highbyte, lowbyte])

    # check the duration of the operation
    duration = time.perf_counter() - start_time
    # wait if necessary
    if sample_duration > duration:
      time.sleep(sample_duration - duration)

data_processing.py

数据处理模块在处理缓冲区之前检查全局标志。 进程必须同步。 该模块在 -1 和 1(归一化值)之间映射输入缓冲器,根据控制开关和旋转编码器的状态对归一化缓冲器应用一种效果,根据 DAC 的分辨率对归一化缓冲器进行插值, 并在第一个位置插入所需的时移。 音频效果“回声”和“弯音”在单独的模块中创建。

# normalize values
buff = [interp(element, [0, main.adc_res], [-1, 1]) for element in buff]
# apply audio effect
bend = 0    # store the timeshift if needed
if UI.param[1]:
  bend = AE.pitchbend(UI.param[0], main.sample_time)
else:
  buff = AE.echo(buff, UI.param[0], main.sample_time)
# scale buffer
buff = [round(interp(element, [-1, 1], [0, main.dac_res])) for element in buff]

# insert timeshift
buff.insert(0, bend)

audio_effects.py

该模块包含一些设置音频效果属性的常量:

1. echo_mag 设置回声效果的响度

2. echo_del 设置回声的最大延迟(以毫秒为单位)(如果使用更大的延迟,则缓冲区大小也必须增加,这会导致更大的延迟,而使用较小的延迟,我们可能会得到混响效果而不是回声)

3. pitch_bend 设置相对于采样频率的最大音高偏移量(如果音频每 50 微秒采样一次,0.25 最大偏移会导致输出样本之间延迟 37.5 微秒,因此输出信号的频率会高 1.33 倍)。

echo_mag = 0.8  # echo magnitude between 0 and 1
echo_del = 100  # maximum delay for echo (in ms)
pitch_bend = 0.25   # maximum delay for pitchbend
                    # in % compared to the sample time

第一个效果,pitch_bend,通过将原始采样时间乘以旋转编码器位置计数器和最大音高偏移量来计算样本之间的延迟差。 该值稍后将插入缓冲区的开头。

def pitchbend(counter, sample_time):
    # calculate sample delay/advance for pitch bending
    bend = sample_time * counter * pitch_bend
    return bend

 

回声效果采用原始缓冲区并从中创建延迟版本,方法是计算每个延迟时间的样本计数,然后将那么多 0 插入缓冲区的开头。 延迟缓冲区根据 echo_mag 常数进行衰减,然后将其添加到原始缓冲区中。

def echo(buffer, counter, sample_time):
    # count delay for samples
    counter = round(echo_del * counter / (sample_time * 1000))
    # create dummy buffer
    delay = [0 for _ in range(counter)]
    # shift samples to get the echo
    delayed_buff = delay + buffer
    # add the echo to the original buffer
    result = [buffer[index] + echo_mag * delayed_buff[index]
              for index in range(len(buffer))]
    return result

调试

调试硬件

可以使用 Analog Discovery 2 以及 WaveForms 软件来调试硬件。 将AD2的模拟输入通道1负线(橙白线)连接到树莓派的地线,然后使用正线(橙线)测量电路不同点的电压并显示模拟信号。 用示波器仪器在波形中显示结果。 使用固定频率和幅度的输入信号,以了解预期的输出。

一些建议可视化的电压和模拟信号是电源的负轨(应该在-5V左右),输入调理电路 中分压器的输出(应该在-1.5V左右) ),输入调理电路 的输出(图像中的输入是一个 1KHz 的正弦信号,响度为 50%),

Waveforms screen showing a signwave

DAC的输出(质量不好是因为采样率低),

Wavforms - output of the DAC

输出调节电路的输出,

Wavforms - the output of the output conditioning circuit,

和整个设备的输出,经过低通滤波器。

Waveforms - output of the whole device, after the low-pass filter

如果一个或多个信号不在预期范围内,则应使用电位器修改 DC-DC 转换器的转换比。 要改变信号的幅度,应修改相应的电阻器。

在 Pmod HAT 适配器和 DAC 或 ADC 之间使用 Pmod TPH2,在 SPI 信号上设置测试点。 将 AD2 的数字 I/O 引脚连接到测试点,然后使用 WaveForms 中的逻辑分析仪仪器来可视化输入/输出数据。

Waveforms - showing the select, clock and data lines

调试软件

虽然输入和输出信号可以很容易地用示波器或逻辑分析仪可视化,但也有内部“信号”,即不同缓冲器的阶段,它们只是虚拟存在的。 为了可视化这些数据点,可以使用 matplotlib.pyplot Python 模块。 为了缩写模块的名称并显示其功能,可以将其作为“调试”导入项目中。

# display the buffer if needed
if main.DEBUG == "ADC" or main.DEBUG == "ALL":
  debug.plot(buff)
  debug.show()

调音

应用程序的性能取决于一些关键参数。 整个项目中最重要的两个值是采样时间和缓冲区大小。 减少采样时间会提高输出质量和带宽(在低通滤波器之前),但填充每个缓冲区所需的时间也会增加。 如果缓冲区填充太慢,则会出现输出中断。 如果缓冲区大小减小,则可以纠正此问题,但缓冲区大小减小后,将无法应用回声效果,并且还会出现弯音时间问题。 在非常短的采样时间下,输出音频中的音高偏移可能会随机出现。 解决方案是在良好的音频质量和不间断的操作之间找到平衡。

结果

一些具有 50 微秒采样时间和 5000 个样本的缓冲区的结果:

Input audio - female voice

Output audio - female voice

Female voice, 50% echo

Female voice, 100% echo

Female voice, 50% pitch shift

Female voice, 100% pitch shift

Input audio - male voice

Output audio - male voice

Male voice, 50% echo

Male voice, 100% echo

Male voice, 50% pitch shift

Male voice, 100% pitch shift

 

                                                                                                  

awong 还没写个人简介...