从‘啊’到‘哦’:用Python+Codec2拆解语音编码,可视化理解基频、共振峰与LPC
从‘啊’到‘哦’:用Python+Codec2拆解语音编码,可视化理解基频、共振峰与LPC
语音编码技术听起来高深莫测,但它的核心原理其实就藏在我们每天发出的简单元音里。想象一下,当你发出"啊"和"哦"这两个音时,你的声带和口腔正在执行一场精密的物理交响乐——而这正是语音编码算法试图用数学语言描述的对象。本文将带你用Python和Codec2库,通过可视化手段拆解这段"交响乐"的乐谱,无需深奥的数学公式,只需几行代码和直观的图表,就能理解语音如何被压缩到每秒几百比特的秘密。
1. 准备工作:搭建语音分析实验室
在开始实验前,我们需要准备以下工具:
pip install librosa matplotlib numpy sounddevice scipy
这些Python库将构成我们的"数字听诊器":
- Librosa :专业的音频分析工具包
- Matplotlib :绘制各种语音分析图表
- NumPy :处理音频信号的数值计算
- Sounddevice :实时录音和播放
- SciPy :信号处理算法支持
提示:建议使用Jupyter Notebook进行实验,可以即时查看可视化结果并交互式调整参数
录制第一个样本很简单:
import sounddevice as sd
import numpy as np
fs = 8000 # 采样率8kHz,与Codec2标准一致
duration = 2 # 录音时长2秒
print("开始录制元音'a'...")
myrecording = sd.rec(int(duration * fs), samplerate=fs, channels=1)
sd.wait() # 等待录音完成
np.save("a_original.npy", myrecording) # 保存原始音频
2. 可视化语音的"指纹图谱"
2.1 波形图:声音的时域肖像
波形图是最直观的声音表现形式,它展示了声压随时间的变化。用以下代码绘制"啊"的波形:
import matplotlib.pyplot as plt
import librosa.display
plt.figure(figsize=(12, 4))
librosa.display.waveshow(myrecording, sr=fs)
plt.title("'啊'的波形图")
plt.xlabel("时间(s)")
plt.ylabel("振幅")
plt.show()
你会看到类似这样的特征:
- 周期性振动 :浊音的典型特征
- 振幅包络 :反映发音的强弱变化
- 过零率 :波形穿过零点的频率,清浊音的重要区分指标
2.2 频谱图:声音的频域密码
将声音转换到频域,秘密就更加清晰了:
D = np.abs(librosa.stft(myrecording.flatten(), n_fft=1024))
plt.figure(figsize=(12, 6))
librosa.display.specshow(librosa.amplitude_to_db(D, ref=np.max),
y_axis='log', x_axis='time', sr=fs)
plt.colorbar(format='%+2.0f dB')
plt.title("'啊'的频谱图")
plt.show()
这张图会揭示三个关键要素:
- 基频(F0) :最低的明显峰,决定音高
- 谐波 :基频整数倍的峰,形成音色基础
- 共振峰 :几个突出的频带,决定元音特性
注意:不同元音的共振峰分布不同,这正是区分"啊"和"哦"的关键
2.3 语谱图:声音的时空演变
语谱图是频谱的时间序列,用热力图形式展示:
plt.figure(figsize=(12, 6))
S = librosa.feature.melspectrogram(y=myrecording.flatten(), sr=fs)
S_dB = librosa.power_to_db(S, ref=np.max)
librosa.display.specshow(S_dB, x_axis='time', y_axis='mel', sr=fs)
plt.colorbar(format='%+2.0f dB')
plt.title("'啊'的语谱图")
plt.show()
观察重点:
- 横杠模式 :稳定的共振峰表现为水平条纹
- 能量集中区 :前三个共振峰(F1-F3)的位置
- 时间变化 :发音起始和结束时的瞬态特征
3. 解剖元音:基频、谐波与共振峰
3.1 测量基频:声音的"心跳"
基频是声带振动的频率,用自相关法可以准确测量:
def get_pitch(signal, sr):
signal = signal - np.mean(signal)
signal = signal / np.max(np.abs(signal))
# 自相关计算
corr = np.correlate(signal, signal, mode='full')
corr = corr[len(corr)//2:]
# 寻找峰值
d = np.diff(corr)
start = np.nonzero(d > 0)[0][0]
peak = np.argmax(corr[start:]) + start
return sr / peak
pitch = get_pitch(myrecording.flatten(), fs)
print(f"测量到的基频:{pitch:.1f} Hz")
成年男性的典型基频范围是85-180Hz,女性是165-255Hz。这个基础频率决定了我们感知到的音高。
3.2 谐波结构:声音的"泛音列"
谐波是基频的整数倍频率成分,它们决定了声音的丰富程度。提取前10个谐波:
n_harmonics = 10
harmonics = []
for i in range(1, n_harmonics+1):
harmonics.append(pitch * i)
print("谐波频率(Hz):", [f"{h:.1f}" for h in harmonics])
3.3 共振峰定位:元音的"身份证"
共振峰是声道形状的声学表现,定位前三个共振峰:
def get_formants(signal, sr):
# 预加重
signal = np.append(signal[0], signal[1:] - 0.97 * signal[:-1])
# 计算LPC系数
n_coeff = 12 # 通常取采样率(kHz)+4
a = librosa.lpc(signal, order=n_coeff)
# 求根并转换为频率
roots = np.roots(a)
roots = roots[np.imag(roots) >= 0] # 保留上半平面根
angles = np.arctan2(np.imag(roots), np.real(roots))
freqs = angles * (sr / (2 * np.pi))
# 按频率排序并返回前几个
freqs = np.sort(freqs)
return freqs[:3] # 返回前三个共振峰
formants = get_formants(myrecording.flatten(), fs)
print(f"共振峰F1-F3(Hz): {formants[0]:.1f}, {formants[1]:.1f}, {formants[2]:.1f}")
典型元音的共振峰范围:
- "啊"(/a/):F1≈700Hz, F2≈1100Hz
- "哦"(/o/):F1≈500Hz, F2≈900Hz
4. LPC分析:数学建模声道形状
线性预测编码(LPC)的核心思想是:当前采样点可以用前几个采样点的线性组合来预测。这实际上是在模拟声道的滤波特性。
4.1 计算LPC系数
order = 12 # 预测器阶数
a = librosa.lpc(myrecording.flatten(), order=order)
print("LPC系数:", a)
这些系数描述了一个虚拟的"数字声道",其频率响应会逼近原始语音的频谱包络。
4.2 可视化LPC频谱包络
# 计算原始频谱
fft_size = 1024
X = np.fft.fft(myrecording.flatten(), fft_size)
X = np.abs(X[:fft_size//2])
freqs = np.linspace(0, fs/2, fft_size//2)
# 计算LPC频谱
w, h = scipy.signal.freqz(1, a, worN=fft_size//2)
h = np.abs(h)
# 绘制对比
plt.figure(figsize=(12, 6))
plt.plot(freqs, 20 * np.log10(X / np.max(X)), label="原始频谱")
plt.plot(freqs, 20 * np.log10(h / np.max(h)), 'r', label="LPC包络")
plt.xlabel("频率(Hz)")
plt.ylabel("幅度(dB)")
plt.legend()
plt.title("原始频谱与LPC包络对比")
plt.show()
你会看到LPC曲线如何"拥抱"住频谱的峰值,这正是共振峰的位置。
4.3 合成重建语音
用LPC系数可以重建近似语音:
def synthesize_lpc(excitation, a):
# excitation是激励信号,a是LPC系数
return scipy.signal.lfilter([1], a, excitation)
# 创建脉冲串激励(模拟浊音)
pulse_train = np.zeros(8000)
pulse_train[::int(fs/pitch)] = 1 # 按基频间隔放置脉冲
# 合成语音
synthesized = synthesize_lpc(pulse_train, a)
# 保存并播放
sd.play(synthesized, fs)
虽然听起来机械感明显,但已经能辨认出元音特征,这就是低码率编码的基本原理。
5. Codec2实战:从原理到应用
5.1 安装与基本使用
Codec2有多种Python绑定,推荐使用pycodec2:
pip install pycodec2
编码解码一段语音:
import pycodec2
# 初始化编解码器(2400bps模式)
c2 = pycodec2.Codec2(2400)
# 编码
encoded = c2.encode(myrecording.flatten())
# 解码
decoded = c2.decode(encoded)
# 保存解码结果
sf.write('a_decoded.wav', decoded, fs)
5.2 编解码前后对比分析
让我们对比原始与解码后的语音特征:
# 波形对比
plt.figure(figsize=(12, 6))
plt.subplot(2,1,1)
librosa.display.waveshow(myrecording, sr=fs)
plt.title("原始语音波形")
plt.subplot(2,1,2)
librosa.display.waveshow(decoded, sr=fs)
plt.title("解码后语音波形")
plt.tight_layout()
plt.show()
# 频谱对比
plt.figure(figsize=(12, 6))
plt.subplot(2,1,1)
D_orig = np.abs(librosa.stft(myrecording.flatten()))
librosa.display.specshow(librosa.amplitude_to_db(D_orig, ref=np.max),
y_axis='linear', x_axis='time', sr=fs)
plt.title("原始语音频谱")
plt.subplot(2,1,2)
D_dec = np.abs(librosa.stft(decoded))
librosa.display.specshow(librosa.amplitude_to_db(D_dec, ref=np.max),
y_axis='linear', x_axis='time', sr=fs)
plt.title("解码后语音频谱")
plt.tight_layout()
plt.show()
虽然波形细节有所丢失,但关键的共振峰结构和基频信息都得到了保留,这正是低码率编码的智慧所在——只保留人耳最敏感的特征。
5.3 码率计算与优化
Codec2在2400bps模式下,每秒语音仅需300字节:
bits_per_second = 2400
bytes_per_second = bits_per_second / 8
print(f"每秒语音数据量:{bytes_per_second}字节")
相比之下,未压缩的8kHz 16-bit单声道音频需要16kB/s,压缩比达到53:1。这种极致的压缩效率来自于:
- 参数化编码 :只传输基频、LPC系数等关键参数
- 感知优化 :专注于保留人耳敏感的特征
- 动态比特分配 :对不同参数采用不同精度的量化
6. 进阶实验:探索语音编码的边界
6.1 不同元音的编码对比
录制"哦"、"咦"、"呜"等不同元音,比较它们的编码效果:
vowels = ['a', 'o', 'i', 'u']
formant_data = []
for vowel in vowels:
print(f"请录制元音'{vowel}'...")
recording = sd.rec(int(duration * fs), samplerate=fs, channels=1)
sd.wait()
# 分析共振峰
f1, f2, f3 = get_formants(recording.flatten(), fs)
formant_data.append([vowel, f1, f2, f3])
# 编码解码
encoded = c2.encode(recording.flatten())
decoded = c2.decode(encoded)
# 保存对比
sf.write(f'{vowel}_original.wav', recording, fs)
sf.write(f'{vowel}_decoded.wav', decoded, fs)
# 展示共振峰数据
formant_table = pd.DataFrame(formant_data, columns=['元音', 'F1', 'F2', 'F3'])
print(formant_table)
你会发现不同元音在F1-F2平面上形成清晰的聚类,这正是语音编码能区分它们的基础。
6.2 极端码率下的语音质量
Codec2支持从700bps到3200bps多种模式,对比不同码率的效果:
rates = [700, 1200, 2400, 3200]
recordings = {}
for rate in rates:
c2 = pycodec2.Codec2(rate)
encoded = c2.encode(myrecording.flatten())
decoded = c2.decode(encoded)
recordings[rate] = decoded
随着码率降低,你会注意到:
- 高频细节首先丢失
- 噪声成分增加
- 但元音特征仍保持可懂度
6.3 实际应用中的优化技巧
在实际语音通信系统中,还会采用以下技术提升质量:
- 舒适噪声生成(CNG) :在静默段插入人工噪声避免突兀感
- 丢包隐藏(PLC) :当数据包丢失时智能插值恢复
- 动态码率调整 :根据网络状况自动切换编码模式
实现一个简单的PLC算法:
def packet_loss_concealment(previous_frame, current_lost=True):
if current_lost:
# 简单重复上一帧
return previous_frame
else:
return None # 正常处理
# 模拟丢包场景
packets = [encoded[i:i+8] for i in range(0, len(encoded), 8)]
lost_indices = [3, 7] # 假设第4和第8个包丢失
decoded_with_plc = []
prev_frame = None
for i, pkt in enumerate(packets):
if i in lost_indices:
frame = packet_loss_concealment(prev_frame)
else:
frame = c2.decode(pkt)
prev_frame = frame
if frame is not None:
decoded_with_plc.extend(frame)
更多推荐
所有评论(0)