返回介绍

声音的输入输出

发布于 2025-02-25 22:46:22 字数 8167 浏览 0 评论 0 收藏 0

在本章我们将学习如何读写 WAV 文件,如何利用声卡实时地进行声音的输入输出。标准的 Python 已经支持 WAV 文件的读写,而实时的声音输入输出需要安装 pyAudio( http://people.csail.mit.edu/hubert/pyaudio )。最后我们还将看看如何使用 pyMedia( http://pymedia.org ) 进行 Mp3 的解码和播放。

掌握了上面的基础知识之后,就可以做许多有趣的声效处理的算法实验了。声效处理方面的内容将在以后的章节详细介绍。

读写 Wave 文件

WAV 是 Microsoft 开发的一种声音文件格式,虽然它支持多种压缩格式,不过它通常被用来保存未压缩的声音数据(PCM 脉冲编码调制)。WAV 有三个重要的参数:声道数、取样频率和量化位数。

  • 声道数:可以是单声道或者是双声道
  • 采样频率:一秒内对声音信号的采集次数,常用的有 8kHz, 16kHz, 32kHz, 48kHz, 11.025kHz, 22.05kHz, 44.1kHz
  • 量化位数:用多少 bit 表达一次采样所采集的数据,通常有 8bit、16bit、24bit 和 32bit 等几种

例如 CD 中所储存的声音信号是双声道、44.1kHz、16bit。

如果你需要自己录制和编辑声音文件,推荐使用 Audacity( http://audacity.sourceforge.net ),它是一款开源的、跨平台、多声道的录音编辑软件。在我的工作中经常使用 Audacity 进行声音信号的录制,然后再输出成 WAV 文件供 Python 程序处理。

读 Wave 文件

下面让我们来看看如何在 Python 中读写声音文件:

# -*- coding: utf-8 -*-
import wave
import pylab as pl
import numpy as np

# 打开 WAV 文档
f = wave.open(r"c:\WINDOWS\Media\ding.wav", "rb")

# 读取格式信息
# (nchannels, sampwidth, framerate, nframes, comptype, compname)
params = f.getparams()
nchannels, sampwidth, framerate, nframes = params[:4]

# 读取波形数据
str_data = f.readframes(nframes)
f.close()

#将波形数据转换为数组
wave_data = np.fromstring(str_data, dtype=np.short)
wave_data.shape = -1, 2
wave_data = wave_data.T
time = np.arange(0, nframes) * (1.0 / framerate)

# 绘制波形
pl.subplot(211) 
pl.plot(time, wave_data[0])
pl.subplot(212) 
pl.plot(time, wave_data[1], c="g")
pl.xlabel("time (seconds)")
pl.show()

WindowsXP 的经典"叮"声的波形

首先载入 Python 的标准处理 WAV 文件的模块,然后调用 wave.open 打开 wav 文件,注意需要使用"rb"(二进制模式) 打开文件:

import wave
f = wave.open(r"c:\WINDOWS\Media\ding.wav", "rb")

open 返回一个的是一个 Wave_read 类的实例,通过调用它的方法读取 WAV 文件的格式和数据:

  • getparams:一次性返回所有的 WAV 文件的格式信息,它返回的是一个组元(tuple):声道数,量化位数(byte 单位), 采样频率,采样点数,压缩类型,压缩类型的描述。wave 模块只支持非压缩的数据,因此可以忽略最后两个信息:
    params = f.getparams()
    nchannels, sampwidth, framerate, nframes = params[:4]
    
  • getnchannels, getsampwidth, getframerate, getnframes 等方法可以单独返回 WAV 文件的特定的信息。
  • readframes:读取声音数据,传递一个参数指定需要读取的长度(以取样点为单位),readframes 返回的是二进制数据(一大堆 bytes),在 Python 中用字符串表示二进制数据:
    str_data = f.readframes(nframes)
    

接下来需要根据声道数和量化单位,将读取的二进制数据转换为一个可以计算的数组:

wave_data = np.fromstring(str_data, dtype=np.short)

通过 fromstring 函数将字符串转换为数组,通过其参数 dtype 指定转换后的数据格式,由于我们的声音格式是以两个字节表示一个取样值,因此采用 short 数据类型转换。现在我们得到的 wave_data 是一个一维的 short 类型的数组,但是因为我们的声音文件是双声道的,因此它由左右两个声道的取样交替构成:LRLRLRLR....LR(L 表示左声道的取样值,R 表示右声道取样值)。修改 wave_data 的 sharp 之后:

wave_data.shape = -1, 2

将其转置得到:

wave_data = wave_data.T

整个转换过程如下图所示:

最后通过取样点数和取样频率计算出每个取样的时间:

time = np.arange(0, nframes) * (1.0 / framerate)

写 Wave 文件

写 WAV 文件的方法和读类似:

# -*- coding: utf-8 -*-
import wave
import numpy as np
import scipy.signal as signal

framerate = 44100
time = 10

# 产生 10 秒 44.1kHz 的 100Hz - 1kHz 的频率扫描波
t = np.arange(0, time, 1.0/framerate)
wave_data = signal.chirp(t, 100, time, 1000, method='linear') * 10000
wave_data = wave_data.astype(np.short)

# 打开 WAV 文档
f = wave.open(r"sweep.wav", "wb")

# 配置声道数、量化位数和取样频率
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(framerate)
# 将 wav_data 转换为二进制数据写入文件
f.writeframes(wave_data.tostring())
f.close()

10-12 行通过调用 scipy.signal 库中的 chrip 函数,产生长度为 10 秒、取样频率为 44.1kHz、100Hz 到 1kHz 的频率扫描波。由于 chrip 函数返回的数组为 float64 型,需要调用数组的 astype 方法将其转换为 short 型。

18-20 行分别设置输出 WAV 文件的声道数、量化位数和取样频率,当然也可以调用文件对象的 setparams 方法一次性配置所有的参数。最后 21 行调用文件的 writeframes 方法,将数组的内部的二进制数据写入文件。writeframes 方法会自动的更新 WAV 文件头中的长度信息(nframes),保证其和真正的数据数量一致。

用 pyAudio 播放和录音

通过上一节介绍的读写声音文件的方法,我们可以离线处理已经录制好的声音。不过更酷的是我们可以通过 pyAudio 库从声卡读取声音数据,处理之后再写回声卡,这样就可以在电脑上实时地输入、处理和输出声音数据。想象一下,我们可以做一个小程序,读取麦克风的数据;加上回声并和 WAV 文件中的数据进行混合;最后从声卡输出。这不就是一个 Karaoke 的原型么。

pyAudio 是开源声音库 PortAudio( http://www.portaudio.com ) 的 Python 绑定,目前它只支持阻塞式的输入输出模式。所谓阻塞式就是需要用户的程序主动地去读写输入输出流。虽然阻塞式在功能上有所局限,但是由于编程比较简单,非常适合一些处理声音的脚本程序开发。

播放

下面先来看看如何用 pyAudio 播放声音。

# -*- coding: utf-8 -*-
import pyaudio
import wave

chunk = 1024

wf = wave.open(r"c:\WINDOWS\Media\ding.wav", 'rb')

p = pyaudio.PyAudio()

# 打开声音输出流
stream = p.open(format = p.get_format_from_width(wf.getsampwidth()),
        channels = wf.getnchannels(),
        rate = wf.getframerate(),
        output = True)

# 写声音输出流进行播放
while True:
  data = wf.readframes(chunk)
  if data == "": break
  stream.write(data)

stream.close()
p.terminate()

这段程序首先根据 WAV 文件的量化格式、声道数和取样频率,分别配置 open 函数的各个参数,然后循环从 WAV 文件读取数据,写入用 open 函数打开的声音输出流。我们看到 17-20 行的 while 循环没有任何等待的代码。因为 pyAudio 使用阻塞模式,因此当底层的输出数据缓存没有空间保存数据时,stream.write 会阻塞用户程序,直到 stream.write 能将数据写入输出缓存。

PyAudio 类的 open 函数有许多参数:

  • rate - 取样频率
  • channels - 声道数
  • format - 取样值的量化格式 (paFloat32, paInt32, paInt24, paInt16, paInt8 ...)。在上面的例子中,使用 get_format_from_width 方法将 wf.sampwidth() 的返回值 2 转换为 paInt16
  • input - 输入流标志,如果为 True 的话则开启输入流
  • output - 输出流标志,如果为 True 的话则开启输出流
  • input_device_index - 输入流所使用的设备的编号,如果不指定的话,则使用系统的缺省设备
  • output_device_index - 输出流所使用的设备的编号,如果不指定的话,则使用系统的缺省设备
  • frames_per_buffer - 底层的缓存的块的大小,底层的缓存由 N 个同样大小的块组成
  • start - 指定是否立即开启输入输出流,缺省值为 True

录音

从声卡读取数据和写入数据一样简单,下面我们用一个简单的声音监测小程序来展示一下如何用 pyAudio 读取声音数据。

# -*- coding: utf-8 -*-
from pyaudio import PyAudio, paInt16 
import numpy as np 
from datetime import datetime 
import wave 

# 将 data 中的数据保存到名为 filename 的 WAV 文件中
def save_wave_file(filename, data): 
  wf = wave.open(filename, 'wb') 
  wf.setnchannels(1) 
  wf.setsampwidth(2) 
  wf.setframerate(SAMPLING_RATE) 
  wf.writeframes("".join(data)) 
  wf.close() 

NUM_SAMPLES = 2000    # pyAudio 内部缓存的块的大小
SAMPLING_RATE = 8000  # 取样频率
LEVEL = 1500      # 声音保存的阈值
COUNT_NUM = 20      # NUM_SAMPLES 个取样之内出现 COUNT_NUM 个大于 LEVEL 的取样则记录声音
SAVE_LENGTH = 8     # 声音记录的最小长度:SAVE_LENGTH * NUM_SAMPLES 个取样

# 开启声音输入
pa = PyAudio() 
stream = pa.open(format=paInt16, channels=1, rate=SAMPLING_RATE, input=True, 
        frames_per_buffer=NUM_SAMPLES) 

save_count = 0 
save_buffer = [] 

while True: 
  # 读入 NUM_SAMPLES 个取样
  string_audio_data = stream.read(NUM_SAMPLES) 
  # 将读入的数据转换为数组
  audio_data = np.fromstring(string_audio_data, dtype=np.short) 
  # 计算大于 LEVEL 的取样的个数
  large_sample_count = np.sum( audio_data > LEVEL ) 
  print np.max(audio_data) 
  # 如果个数大于 COUNT_NUM,则至少保存 SAVE_LENGTH 个块
  if large_sample_count > COUNT_NUM: 
    save_count = SAVE_LENGTH 
  else: 
    save_count -= 1 

  if save_count < 0: 
    save_count = 0 

  if save_count > 0: 
    # 将要保存的数据存放到 save_buffer 中
    save_buffer.append( string_audio_data ) 
  else: 
    # 将 save_buffer 中的数据写入 WAV 文件,WAV 文件的文件名是保存的时刻
    if len(save_buffer) > 0: 
      filename = datetime.now().strftime("%Y-%m-%d_%H_%M_%S") + ".wav" 
      save_wave_file(filename, save_buffer) 
      save_buffer = [] 
      print filename, "saved"

此程序一开头是一系列的全局变量,用来配置录音的一些参数:以 SAMPLING_RATE 为采样频率,每次读入一块有 NUM_SAMPLES 个采样的数据块,当读入的采样数据中有 COUNT_NUM 个值大于 LEVEL 的取样的时候,将数据保存进 WAV 文件,一旦开始保存数据,所保存的数据长度最短为 SAVE_LENGTH 个块。WAV 文件以保存时的时刻作为文件名。

从声卡读入的数据和从 WAV 文件读入的类似,都是二进制数据,由于我们用 paInt16 格式(16bit 的 short 类型) 保存采样值,因此将它自己转换为 dtype 为 np.short 的数组。

用 pyMedia 播放 Mp3

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文