Python 播放 PCM 音频文件

2,172 阅读6分钟

什么是 PCM

现实生活中的音频数据是连续的模拟信号,如图中红线所示。这种连续的信号可以用沟槽的形式记录在黑胶唱片上。但是计算机使用数字信号存储信息,不能存储连续的信号,因此如果想将音频数据记录在计算机中就需要将模拟信号转换成计算机能够存储的数字信号。其中最常见的转化方法就是 脉冲编码调制(Pulse Code Modulation, PCM) 。使用 PCM 可以将图中红线表示的音频转化成蓝点对应的数值,将数值编码之后即可存储在计算机上。

image.png

相关概念

想要根据一段音频模拟信号转化成 PCM 文件需要考虑以下几个参数:采样率、采样位数、声道数、比特率

采样率

将模拟信号转化成数字信号的过程中需要取点采样,比如上图中对整个模拟信号取了 26 个点,分别对应 26 个数值,之后也只会保存这 26 个值,其他没有取到的部分就丢失掉了,所以模拟信号转化成数字信号的过程中必然会出现数据的缺失。为了尽量保证数据的完整性,就要增大采样点的个数。一秒内采样的次数就叫做采样率,单位是 Hz。采样率越高,采样后数字信号的波形就越接近原始模拟信号的波形。 常见的采样率如下:

采样率用途
8k Hz电话
22.05k HzFM 电台
44.1k HzCD
48k Hz标准 DVD
96k Hz蓝光 DVD
192k Hz声卡

采样位数

采样位数就是用来表示一个采样点数值的 bit 位数。图中蓝点对应的值得范围是 0~15,因此只要使用 4 位就能满足需求了,因此采样位数就是 4。显然,采样位数越高声音就记录得越精细。 常见的采样位数如下:

采样位数用途
8电话
16CD
24DVD

声道

声道数是指录制声音时不同空间位置独立的音频信号,也可以说是录制声音时麦克风的数量。 如下是一个 5.1 声道家庭影院示意图,六个扬声器分别是 :

  • C(Central)中置
  • FL(Front Left)左前置
  • FR(Front Right)右前置
  • SL(Surround Left)左环绕
  • SR(Surround Right)右环绕
  • SW(Subwoofer)低音炮

image.png

比特率

比特率是指每秒数据的位数,单位是 bps。根据采样率、采样位数、声道数,我们可以计算出对应的数字信号的比特率是 采样率*采样位数*声道数  常见的比特率如下:

比特率用途
96k bpsFM 广播
128k, 160k, 192k, 320k bpsMP3
400k~1411 kbps无损压缩音频
1411.2 kbpsPCM 存储的数字音频 CD

PCM 格式

PCM 就是将采样编码后的二进制数据直接写入文件中,例如对于一个采样位数为 16,声道数为 2 的数据,其保存格式如下:

+-------------+-------------+-------------+-------------+-----
| 声道1(16bit) | 声道2(16bit) | 声道1(16bit) | 声道2(16bit)| ...
+-------------+-------------+-------------+-------------+-----

其中 16bit 表示一个声道获取到的一个采样点的数据,并且两个声道的数据相互间隔均匀存储。 PCM 数据只是纯粹的二进制数据,播放器拿到这个数据之后如果不知道数据对应的采样率、采样位数、声道数就无法将这个二进制数据再切分成原来的字节片段,因此需要 封装格式 对二进制数据对应的基本参数进行描述,常用的封装格式就是 WAVE。

WAVE 格式

标准的 WAVE 格式如下图:

image.png

它包含以下几个部分:

  1. RIFF 头部
    1. ChunkID:固定为 "RIFF" 字符串
    2. ChunkSize:表示剩下部分的长度,其值为 4 + (8 + SubChunk1Size)+(8 + SubChunk2Size) 
    3. Format:固定为 "WAVE" 字符串
  2. "fmt" 部分
    1. Subchunk1ID:固定为 "fmt " 字符串(注意 t 后面有一个空格)
    2. Subchunk1Size:对应后面 Subchunk 的大小,PCM 对应的值是 16
    3. AudioFormat:音频压缩格式,PCM 对应的值是 1,表示没有压缩
    4. NumChannel:声道数
    5. SampleRate:采样率
    6. ByteRate:字节率, 字节率=比特率/8 
    7. BlockAlign:文件块对齐的大小,通过 声道数*采样位数/8
    8. BitsPerSample: 采样位数
  3. 数据部分
    1. Subchunk2ID:固定为 "data" 字符串
    2. Subchunk2Size: 数据大小, 数据大小=采样数*声道数*采样位数/8 
    3. Data: 音频数据

将 PCM 封装成 WAVE 文件后就可以使用播放器播放音频文件了

播放 PCM 音频文件

使用 Python 播放 PCM 有 2 中方法:

  1. 将 PCM 封装成 WAVE 文件
  2. 直接播放 PCM 文件

封装成 WAVE 播放

Python 中内置了 wave 库用来处理 WAVE 格式的文件。由前一节的内容可知,要想将 PCM 封装成 WAVE 只需要给音频数据加上一个头。 wave 库提供了丰富的接口帮我们完成这项任务,步骤如下:

  1. 设置声道数
  2. 设置采样字节数
  3. 设置采样率
  4. 将 PCM 文件中的数据写入 WAVE 文件中的 data 部分
def pcm2wav(pcm_file, wav_file, channels=1, bits=16, sample_rate=16000):
    # 打开 PCM 文件
    pcmf = open(pcm_file, 'rb')
    pcmdata = pcmf.read()
    pcmf.close()
    
    # 打开将要写入的 WAVE 文件
    wavfile = wave.open(wav_file, 'wb')
    # 设置声道数
    wavfile.setnchannels(channels)
    # 设置采样位宽
    wavfile.setsampwidth(bits // 8)
	# 设置采样率
    wavfile.setframerate(sample_rate)
    # 写入 data 部分
    wavfile.writeframes(pcmdata)
    wavfile.close()
    
pcm2wave("f1.pcm", "f2.wav")

这样就能成功将 PCM 格式的 f1.pcm 转化成 WAVE 格式的 f2.wav,也就能用播放器播放了。 Python 中的 pyaudio 库实现了播放器的功能,使用它提供的接口可以直接播放 WAVE 的音频文件

import pyaudio  
import wave  

# 设置读取单位大小
chunk = 1024  

# 打开文件
f = wave.open(r"/usr/share/sounds/alsa/Rear_Center.wav","rb")
# 初始化播放器
p = pyaudio.PyAudio()  
# 初始化数据流,设置采样位宽、声道数、采样率
stream = p.open(format = p.get_format_from_width(f.getsampwidth()),  
                channels = f.getnchannels(),  
                rate = f.getframerate(),  
                output = True)  
# 读取数据,并写入 PyAudio 对象的数据流
data = f.readframes(chunk)  
while data:  
    stream.write(data)  
    data = f.readframes(chunk)  

# 停止播放
stream.stop_stream()  
stream.close()  
p.terminate()  

直接播放 PCM 音频

使用 PyAudio 直接播放 PCM 音频与播放 WAVE 文件的方法大同小异,只不过需要先手动设置 PyAudio 播放器的采样位数、采样率、声道数等参数,然后再将 PCM 数据写入 PyAudio 的数据流即可。

import pyaudio

# 初始化播放器
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(2), channels=1, rate=16000, output=True)

# 将 pcm 数据直接写入 PyAudio 的数据流
with open("f1.pcm", "rb") as f:
    stream.write(f.read())

stream.stop_stream()
stream.close()
p.terminate()

参考