|
|
同样演奏一个 200 Hz 的音符,笛子和钢琴听起来完全不同。
声音的“时间结构”
实际上,音色不仅仅由频谱决定,还与声音随时间怎么变化有关。
例如钢琴:
音量
│
│ /\
│ / \
│ / \______
│ /
│___/ \____
└────────────────────────→ 时间
Attack Decay Release
钢琴按下琴键之后:
瞬间出现
↓
快速达到峰值
↓
逐渐衰减
↓
最后消失
这就是典型的 ADSR 包络中的攻击、衰减和释放特征。
笛子则可能更像:
音量
│
│ ┌──────────────
│ /
│ /
│_____/
└────────────────────────→ 时间
演奏者可以持续吹气,因此声音可以保持相对稳定。
5. 从声学角度看,可以把乐器声音拆成三个部分
一个实际乐器声音大致可以理解为:
乐器声音
│
├── 基频
│
├── 谐波 / 泛音
│
└── 噪声 + 时间包络
所以:
纯正弦波
↓
只有基频
↓
听起来非常纯
而:
笛声
↓
基频 + 谐波 + 气息噪声 + 包络
以及:
钢琴
↓
基频 + 大量泛音 + 非谐性 + 击弦瞬态 + 衰减包络
left = np.sin(2 * np.pi * 200 * t)
right = np.sin(2 * np.pi * 210 * t)
这是:
左耳 → 纯 200 Hz
右耳 → 纯 210 Hz
如果把它换成笛声或钢琴声,就完全不是这么简单了。
例如:
左耳笛声:200 Hz
右耳笛声:210 Hz
笛声本身可能包含:
左耳:
200
400
600
800
...
右耳:
210
420
630
840
...
于是你实际上给听觉系统输入了一整组频率,而不仅仅是 200 和 210 Hz。
这可能产生多个频率之间的相互作用。
真实乐器声音是“很多频率按照特定比例和时间结构叠加出来的复杂波形”
import numpy as np
import soundfile as sf
import matplotlib.pyplot as plt
# ============================================================
# 1. 参数设置
# ============================================================
# 你的钢琴 WAV 文件
audio_file = "piano.wav"
# 从第几秒开始分析
start_time = 1.0
# 分析多长时间
#
# 如果 WAV 中包含一个单独的钢琴音符,
# 建议先从 0.1 ~ 0.5 秒开始观察。
analysis_duration = 0.2
# FFT 图横轴最多显示到多少 Hz
max_display_freq = 5000
# ============================================================
# 2. 读取 WAV
# ============================================================
audio, sample_rate = sf.read(audio_file)
print("采样率:", sample_rate)
print("原始数据形状:", audio.shape)
# ============================================================
# 3. 如果是立体声,转换为单声道
# ============================================================
# 立体声通常是:
#
# (采样点数量, 2)
#
# 第 0 列:左声道
# 第 1 列:右声道
#
# 这里把左右声道平均。
if audio.ndim == 2:
audio = np.mean(audio, axis=1)
# ============================================================
# 4. 截取需要分析的声音片段
# ============================================================
start_sample = int(start_time * sample_rate)
end_sample = int(
(start_time + analysis_duration) * sample_rate
)
if start_sample >= len(audio):
raise ValueError("start_time 已经超过 WAV 文件长度")
end_sample = min(end_sample, len(audio))
segment = audio[start_sample:end_sample]
print("分析采样点:", len(segment))
print(
"实际分析时间:",
len(segment) / sample_rate,
"秒"
)
# ============================================================
# 5. 绘制时域波形
# ============================================================
time_axis = np.arange(len(segment)) / sample_rate
plt.figure(figsize=(12, 5))
plt.plot(
time_axis,
segment
)
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.title("Piano waveform")
plt.grid(
True,
alpha=0.3
)
plt.tight_layout()
plt.show()
# ============================================================
# 6. 使用 Hann 窗
# ============================================================
# 如果直接把一小段声音截出来做 FFT,
# 相当于在截取位置突然把声音切断。
#
# 这种突然截断会造成频谱泄漏。
#
# Hann 窗可以降低这种影响。
window = np.hanning(len(segment))
windowed_signal = segment * window
# ============================================================
# 7. FFT
# ============================================================
# rfft 专门针对实数信号。
#
# 对于采样率 Fs,
# FFT 的有效频率范围是:
#
# 0 Hz ~ Fs/2
#
# Fs/2 就是 Nyquist 频率。
fft_result = np.fft.rfft(
windowed_signal
)
# ============================================================
# 8. 计算每个 FFT 点对应的频率
# ============================================================
freqs = np.fft.rfftfreq(
len(windowed_signal),
d=1 / sample_rate
)
# ============================================================
# 9. 计算幅度
# ============================================================
magnitude = np.abs(
fft_result
)
# 归一化
magnitude = (
magnitude /
(magnitude.max() + 1e-12)
)
# ============================================================
# 10. 转换成 dB
# ============================================================
# dB 表示:
#
# 20 * log10(幅度)
#
# 这样弱小的泛音也更容易看到。
magnitude_db = 20 * np.log10(
magnitude + 1e-12
)
# ============================================================
# 11. 只显示 0 ~ 5000 Hz
# ============================================================
mask = freqs <= max_display_freq
display_freqs = freqs[mask]
display_magnitude_db = magnitude_db[mask]
# ============================================================
# 12. 绘制 FFT 频谱
# ============================================================
plt.figure(figsize=(12, 6))
plt.plot(
display_freqs,
display_magnitude_db
)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Magnitude (dB)")
plt.title(
"Piano FFT Spectrum"
)
plt.xlim(
0,
max_display_freq
)
plt.ylim(
-100,
5
)
plt.grid(
True,
alpha=0.3
)
plt.tight_layout()
plt.show() |
|