38 · 音频信号处理:FFT 频谱分析与低通滤波
🔗 知识图谱导航:阅读本文前,建议先掌握《34 · SciPy 实战》中的
signal.butter + filtfilt滤波操作——本文把这个操作应用到音频信号处理场景,并加入 FFT 频谱分析。
运行环境:
pip install numpy scipy。
极客解析:音频信号处理的核心是"时域 ↔ 频域"的转换。FFT 把时域信号(随时间变化的波形)转换到频域(各频率的幅度),让我们看清信号由哪些频率组成。低通滤波在频域里把高频成分清零,再转回时域,就实现了去噪。
FFT 核心概念
采样率(Sample Rate) 每秒采样次数,8000 Hz = 每秒 8000 个点
奈奎斯特定理 有效频率 ≤ 采样率/2,8000 Hz 采样率最高分析 4000 Hz
频率分辨率 = 采样率 / 采样点数,点数越多分辨率越高
FFT 输出 复数数组,取绝对值得到幅度谱
低通滤波 保留低频(趋势),去除高频(噪声)
步步为营:核心逻辑自适应拆解
这一篇按音频信号处理流水线拆成 7 个台阶:先合成带噪声的声音,再做 FFT、找频谱峰值、低通滤波、画 ASCII 频谱,最后用 CLI 调度不同演示。每个演示都补了 Mock 数据和 print() 反馈。
Step 1:用 generate_signal 合成一段带噪声音频
痛点与机制:
generate_signal 是音频实验室:它把 440Hz 基频、880Hz 泛音、1200Hz 高频分量和随机噪声叠到一起。可以把声音想成几根不同频率的琴弦同时振动,最终耳朵听到的是它们的混合波形。
核心源码(逐字来自文末完整源码):
def generate_signal(
sample_rate: int = 8000,
duration: float = 1.0,
seed: int = 42,
) -> tuple[np.ndarray, np.ndarray]:
"""
生成模拟音频信号:
- 主音调:440 Hz(A4音符)
- 泛音:880 Hz(A5,二倍频)
- 噪声:随机高频噪声
"""
np.random.seed(seed)
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
# 合成信号 = 基频 + 泛音 + 噪声
signal = (
1.0 * np.sin(2 * np.pi * 440 * t) # 440 Hz 基频
+ 0.5 * np.sin(2 * np.pi * 880 * t) # 880 Hz 泛音
+ 0.3 * np.sin(2 * np.pi * 1200 * t) # 1200 Hz 高频分量
+ 0.2 * np.random.randn(len(t)) # 随机噪声
)
return t, signal
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def generate_signal(
sample_rate: int = 8000,
duration: float = 1.0,
seed: int = 42,
) -> tuple[np.ndarray, np.ndarray]:
"""
生成模拟音频信号:
- 主音调:440 Hz(A4音符)
- 泛音:880 Hz(A5,二倍频)
- 噪声:随机高频噪声
"""
np.random.seed(seed)
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
# 合成信号 = 基频 + 泛音 + 噪声
signal = (
1.0 * np.sin(2 * np.pi * 440 * t) # 440 Hz 基频
+ 0.5 * np.sin(2 * np.pi * 880 * t) # 880 Hz 泛音
+ 0.3 * np.sin(2 * np.pi * 1200 * t) # 1200 Hz 高频分量
+ 0.2 * np.random.randn(len(t)) # 随机噪声
)
return t, signal
t, sig = generate_signal(sample_rate=8000, duration=0.01)
print("🎧 模拟音频已生成")
print("采样点数:", len(sig))
print("前8个采样值:", np.round(sig[:8], 3).tolist())
print("时间范围:", f"{t[0]:.5f}s ~ {t[-1]:.5f}s")
Step 2:用 fft_analysis 把波形拆成频谱
痛点与机制:
FFT 的作用是把“时间里的波形”翻译成“频率里的成分”。时域像看一锅汤的表面波纹,频域像把汤里的盐、糖、辣椒分别称出来;峰值越高,说明那个频率越突出。
核心源码(逐字来自文末完整源码):
def fft_analysis(
signal: np.ndarray,
sample_rate: int,
) -> tuple[np.ndarray, np.ndarray]:
"""
对信号做 FFT,返回正频率轴和对应幅度谱。
只取前半段(奈奎斯特定理:有效频率 ≤ 采样率/2)
"""
n = len(signal)
fft_result = np.fft.fft(signal)
freqs = np.fft.fftfreq(n, d=1.0 / sample_rate)
# 只取正频率部分
pos_mask = freqs >= 0
freqs_pos = freqs[pos_mask]
power_pos = np.abs(fft_result[pos_mask]) * 2 / n # 归一化幅度
return freqs_pos, power_pos
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def fft_analysis(
signal: np.ndarray,
sample_rate: int,
) -> tuple[np.ndarray, np.ndarray]:
"""
对信号做 FFT,返回正频率轴和对应幅度谱。
只取前半段(奈奎斯特定理:有效频率 ≤ 采样率/2)
"""
n = len(signal)
fft_result = np.fft.fft(signal)
freqs = np.fft.fftfreq(n, d=1.0 / sample_rate)
# 只取正频率部分
pos_mask = freqs >= 0
freqs_pos = freqs[pos_mask]
amp_pos = np.abs(fft_result[pos_mask]) * 2 / n
return freqs_pos, amp_pos
sample_rate = 8000
t = np.linspace(0, 1.0, sample_rate, endpoint=False)
sig = np.sin(2 * np.pi * 440 * t) + 0.5 * np.sin(2 * np.pi * 880 * t)
freqs, amps = fft_analysis(sig, sample_rate)
top = np.argsort(amps)[-5:][::-1]
print("FFT 频谱 Top5:")
for i in top:
print(f" {freqs[i]:7.1f} Hz -> amplitude {amps[i]:.3f}")
Step 3:用 find_peaks 找出最突出的音调
痛点与机制:
find_peaks 做的是频谱里的找山峰:只要某个点比左右邻居都高,就算局部峰值。它像在山脉剖面图里找最高的几座山,帮助我们识别声音里最主要的音调。
核心源码(逐字来自文末完整源码):
def find_peaks(
freqs: np.ndarray,
power: np.ndarray,
top_n: int = 5,
min_power: float = 0.05,
) -> list[tuple[float, float]]:
"""找出频谱中的主要峰值频率。"""
# 简单峰值检测:局部最大值
peaks = []
for i in range(1, len(power) - 1):
if power[i] > power[i-1] and power[i] > power[i+1] and power[i] > min_power:
peaks.append((freqs[i], power[i]))
peaks.sort(key=lambda x: x[1], reverse=True)
return peaks[:top_n]
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def find_peaks(freqs: np.ndarray, amps: np.ndarray, top_k: int = 5) -> list[tuple[float, float]]:
"""找出频谱中幅度最大的 top_k 个峰值。"""
# 忽略直流分量(0Hz)
mask = freqs > 0
freqs2 = freqs[mask]
amps2 = amps[mask]
# 简单局部最大值检测
peak_idx = []
for i in range(1, len(amps2) - 1):
if amps2[i] > amps2[i - 1] and amps2[i] > amps2[i + 1]:
peak_idx.append(i)
peak_idx = sorted(peak_idx, key=lambda i: amps2[i], reverse=True)[:top_k]
return [(float(freqs2[i]), float(amps2[i])) for i in peak_idx]
freqs = np.array([0, 100, 200, 300, 400, 500, 600], dtype=float)
amps = np.array([0.1, 0.2, 1.0, 0.3, 0.8, 0.2, 0.1])
peaks = find_peaks(freqs, amps, top_k=2)
print("峰值检测结果:")
for f, a in peaks:
print(f" {f:.0f} Hz -> {a:.2f}")
Step 4:用 lowpass_filter 切掉高频噪声
痛点与机制:
低通滤波像一扇只让低频通过的门。代码在频域里把 cutoff 以上的成分置零,再反变换回时域;这比直接盯着波形删噪声更直观,因为噪声常常藏在高频区域。
核心源码(逐字来自文末完整源码):
def lowpass_filter(
signal: np.ndarray,
sample_rate: int,
cutoff_hz: float = 600.0,
order: int = 5,
) -> np.ndarray:
"""
Butterworth 低通滤波器:保留 cutoff_hz 以下的频率。
filtfilt 做零相位滤波(前向+反向),避免相位失真。
"""
from scipy import signal as sp_signal
nyquist = sample_rate / 2.0
normalized_cutoff = cutoff_hz / nyquist
b, a = sp_signal.butter(order, normalized_cutoff, btype="low", analog=False)
return sp_signal.filtfilt(b, a, signal)
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def lowpass_filter(
signal: np.ndarray,
sample_rate: int,
cutoff_hz: float = 1000,
) -> np.ndarray:
"""
频域低通滤波:把 cutoff_hz 以上的频率成分置零。
这是教学版,便于理解;工业级可用 scipy.signal.butter。
"""
n = len(signal)
fft_result = np.fft.fft(signal)
freqs = np.fft.fftfreq(n, d=1.0 / sample_rate)
fft_result[np.abs(freqs) > cutoff_hz] = 0
filtered = np.fft.ifft(fft_result).real
return filtered
sample_rate = 8000
t = np.linspace(0, 1.0, sample_rate, endpoint=False)
clean = np.sin(2 * np.pi * 440 * t)
noise = 0.6 * np.sin(2 * np.pi * 1800 * t)
sig = clean + noise
filtered = lowpass_filter(sig, sample_rate, cutoff_hz=1000)
print("低通滤波演示:")
print("原始信号标准差:", round(float(sig.std()), 4))
print("滤波后标准差:", round(float(filtered.std()), 4))
print("高频噪声残差标准差:", round(float((sig - filtered).std()), 4))
Step 5:用 ascii_spectrum 在终端画频谱图
痛点与机制:
ASCII 频谱图把频率范围切成小桶,每个桶用最长的条表示桶内最大幅度。它像一个不需要图形界面的均衡器,服务器终端里也能看出 440Hz、880Hz 附近有没有明显能量。
核心源码(逐字来自文末完整源码):
def ascii_spectrum(
freqs: np.ndarray,
power: np.ndarray,
width: int = 60,
bins: int = 20,
) -> None:
"""在终端用 ASCII 字符绘制频谱柱状图。"""
max_freq = 2000.0
bin_size = max_freq / bins
bin_power = np.zeros(bins)
for f, p in zip(freqs, power):
if f > max_freq:
break
idx = min(int(f / bin_size), bins - 1)
bin_power[idx] = max(bin_power[idx], p)
max_p = bin_power.max() or 1.0
print(f"\n 频谱图(0 ~ {int(max_freq)} Hz)")
print(f" {'─' * (width + 12)}")
for i, p in enumerate(bin_power):
freq_label = f"{int(i * bin_size):>4}-{int((i+1)*bin_size):<4}Hz"
bar_len = int(p / max_p * width)
bar = "█" * bar_len
print(f" {freq_label} │{bar:<{width}}│ {p:.3f}")
print(f" {'─' * (width + 12)}")
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def ascii_spectrum(
freqs: np.ndarray,
amps: np.ndarray,
max_freq: float = 2000,
width: int = 50,
) -> None:
"""绘制 ASCII 频谱图。"""
mask = (freqs >= 0) & (freqs <= max_freq)
f = freqs[mask]
a = amps[mask]
# 分桶显示
bins = 20
edges = np.linspace(0, max_freq, bins + 1)
print("\n频谱图(ASCII)")
print("-" * 70)
max_amp = a.max() if len(a) else 1.0
for i in range(bins):
bin_mask = (f >= edges[i]) & (f < edges[i + 1])
if bin_mask.any():
val = a[bin_mask].max()
else:
val = 0.0
bar_len = int(val / max_amp * width) if max_amp else 0
bar = "█" * bar_len
print(f"{edges[i]:5.0f}-{edges[i+1]:5.0f} Hz | {bar} {val:.3f}")
print("-" * 70)
freqs = np.linspace(0, 2000, 41)
amps = np.exp(-((freqs - 440) / 90) ** 2) + 0.5 * np.exp(-((freqs - 880) / 120) ** 2)
ascii_spectrum(freqs, amps, max_freq=1200, width=28)
Step 6:用 demo_filter 对比滤波前后的能量
痛点与机制:
demo_filter 把生成、滤波和数值对比串起来:RMS 可以理解成信号能量的平均强度。滤波后 RMS 变小,不是声音没了,而是高频噪声被拿掉了一部分。
核心源码(逐字来自文末完整源码):
def demo_filter() -> None:
try:
from scipy import signal as sp_signal
except ImportError:
print(" ⚠️ 需要安装 scipy: pip install scipy")
return
sample_rate = 8000
t, sig = generate_signal(sample_rate)
filtered = lowpass_filter(sig, sample_rate, cutoff_hz=600)
# 对比滤波前后的频谱
freqs_raw, power_raw = fft_analysis(sig, sample_rate)
freqs_flt, power_flt = fft_analysis(filtered, sample_rate)
print("\n ── 低通滤波效果对比(截止频率 600 Hz)────")
print(f" {'频率(Hz)':<12} {'原始幅度':<12} {'滤波后幅度':<12} {'衰减'}")
print(f" {'─'*12} {'─'*12} {'─'*12} {'─'*10}")
check_freqs = [440, 880, 1200]
for target_f in check_freqs:
idx_r = np.argmin(np.abs(freqs_raw - target_f))
idx_f = np.argmin(np.abs(freqs_flt - target_f))
p_raw = power_raw[idx_r]
p_flt = power_flt[idx_f]
attn = (1 - p_flt / p_raw) * 100 if p_raw > 0 else 0
status = "✅ 保留" if target_f <= 600 else "🔇 衰减"
print(f" {target_f:<12} {p_raw:<12.4f} {p_flt:<12.4f} {attn:.1f}% {status}")
print(f"\n 滤波结果:440Hz 基频保留,880Hz/1200Hz 高频被衰减")
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def lowpass_filter(signal: np.ndarray, sample_rate: int, cutoff_hz: float = 1000) -> np.ndarray:
n = len(signal)
fft_result = np.fft.fft(signal)
freqs = np.fft.fftfreq(n, d=1.0 / sample_rate)
fft_result[np.abs(freqs) > cutoff_hz] = 0
return np.fft.ifft(fft_result).real
def demo_filter() -> None:
sample_rate = 8000
duration = 1.0
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
sig = np.sin(2 * np.pi * 440 * t) + 0.7 * np.sin(2 * np.pi * 1800 * t)
filtered = lowpass_filter(sig, sample_rate, cutoff_hz=1000)
print("滤波前后对比:")
print(f" 原始 RMS: {np.sqrt(np.mean(sig ** 2)):.4f}")
print(f" 滤波 RMS: {np.sqrt(np.mean(filtered ** 2)):.4f}")
print(f" 被削掉的高频 RMS: {np.sqrt(np.mean((sig - filtered) ** 2)):.4f}")
demo_filter()
Step 7:用 main 做 fft/filter/ascii/all 脚本遥控器
痛点与机制:
main 是脚本遥控器:--mode fft/filter/ascii/all 分别对应频谱分析、滤波、终端图和全流程。新手不用改代码,只要换参数就能观察不同处理阶段。
核心源码(逐字来自文末完整源码):
def main() -> None:
parser = argparse.ArgumentParser(description="音频信号处理演示")
parser.add_argument(
"--mode",
choices=["fft", "filter", "ascii", "all"],
default="all",
)
args = parser.parse_args()
if args.mode in ("fft", "all"):
demo_fft()
if args.mode in ("filter", "all"):
demo_filter()
if args.mode == "ascii":
demo_ascii()
可运行演示(补齐 Mock 数据与 print 反馈):
import argparse
import sys
def demo_fft() -> None:
print("运行 fft:生成信号并分析频谱峰值")
def demo_filter() -> None:
print("运行 filter:低通滤波去掉高频噪声")
def demo_ascii() -> None:
print("运行 ascii:在终端绘制频谱图")
def main() -> None:
parser = argparse.ArgumentParser(description="音频信号处理演示")
parser.add_argument("--mode", choices=["fft", "filter", "ascii", "all"], default="all")
args = parser.parse_args()
if args.mode in ("fft", "all"):
demo_fft()
if args.mode in ("filter", "all"):
demo_filter()
if args.mode in ("ascii", "all"):
demo_ascii()
for mode in ["fft", "filter", "ascii", "all"]:
print(f"\n>>> python3 audio_signal.py --mode {mode}")
sys.argv = ["prog", "--mode", mode]
main()
极客实战:完整源码与运行
现在,把上面的积木拼起来,将以下完整代码放进你的编辑器,运行它。先看整体闭环,再回头逐段改参数,你会更容易建立工程直觉。
"""
音频信号处理演示 —— FFT频谱分析 + 滤波 + 可视化。
用法:
python3 audio_signal.py
python3 audio_signal.py --mode fft
python3 audio_signal.py --mode filter
python3 audio_signal.py --mode ascii
"""
import argparse
import numpy as np
# from typing import tuple # Python 3.9+ 直接用内置 tuple as Tuple
# ── Mock 信号生成(零外部文件依赖)──────────────────────────
def generate_signal(
sample_rate: int = 8000,
duration: float = 1.0,
seed: int = 42,
) -> tuple[np.ndarray, np.ndarray]:
"""
生成模拟音频信号:
- 主音调:440 Hz(A4音符)
- 泛音:880 Hz(A5,二倍频)
- 噪声:随机高频噪声
"""
np.random.seed(seed)
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
# 合成信号 = 基频 + 泛音 + 噪声
signal = (
1.0 * np.sin(2 * np.pi * 440 * t) # 440 Hz 基频
+ 0.5 * np.sin(2 * np.pi * 880 * t) # 880 Hz 泛音
+ 0.3 * np.sin(2 * np.pi * 1200 * t) # 1200 Hz 高频分量
+ 0.2 * np.random.randn(len(t)) # 随机噪声
)
return t, signal
# ── FFT 频谱分析 ──────────────────────────────────────────────
def fft_analysis(
signal: np.ndarray,
sample_rate: int,
) -> tuple[np.ndarray, np.ndarray]:
"""
对信号做 FFT,返回正频率轴和对应幅度谱。
只取前半段(奈奎斯特定理:有效频率 ≤ 采样率/2)
"""
n = len(signal)
fft_result = np.fft.fft(signal)
freqs = np.fft.fftfreq(n, d=1.0 / sample_rate)
# 只取正频率部分
pos_mask = freqs >= 0
freqs_pos = freqs[pos_mask]
power_pos = np.abs(fft_result[pos_mask]) * 2 / n # 归一化幅度
return freqs_pos, power_pos
def find_peaks(
freqs: np.ndarray,
power: np.ndarray,
top_n: int = 5,
min_power: float = 0.05,
) -> list[tuple[float, float]]:
"""找出频谱中的主要峰值频率。"""
# 简单峰值检测:局部最大值
peaks = []
for i in range(1, len(power) - 1):
if power[i] > power[i-1] and power[i] > power[i+1] and power[i] > min_power:
peaks.append((freqs[i], power[i]))
peaks.sort(key=lambda x: x[1], reverse=True)
return peaks[:top_n]
# ── 低通滤波 ──────────────────────────────────────────────────
def lowpass_filter(
signal: np.ndarray,
sample_rate: int,
cutoff_hz: float = 600.0,
order: int = 5,
) -> np.ndarray:
"""
Butterworth 低通滤波器:保留 cutoff_hz 以下的频率。
filtfilt 做零相位滤波(前向+反向),避免相位失真。
"""
from scipy import signal as sp_signal
nyquist = sample_rate / 2.0
normalized_cutoff = cutoff_hz / nyquist
b, a = sp_signal.butter(order, normalized_cutoff, btype="low", analog=False)
return sp_signal.filtfilt(b, a, signal)
# ── ASCII 频谱图(终端可视化)────────────────────────────────
def ascii_spectrum(
freqs: np.ndarray,
power: np.ndarray,
width: int = 60,
bins: int = 20,
) -> None:
"""在终端用 ASCII 字符绘制频谱柱状图。"""
max_freq = 2000.0
bin_size = max_freq / bins
bin_power = np.zeros(bins)
for f, p in zip(freqs, power):
if f > max_freq:
break
idx = min(int(f / bin_size), bins - 1)
bin_power[idx] = max(bin_power[idx], p)
max_p = bin_power.max() or 1.0
print(f"\n 频谱图(0 ~ {int(max_freq)} Hz)")
print(f" {'─' * (width + 12)}")
for i, p in enumerate(bin_power):
freq_label = f"{int(i * bin_size):>4}-{int((i+1)*bin_size):<4}Hz"
bar_len = int(p / max_p * width)
bar = "█" * bar_len
print(f" {freq_label} │{bar:<{width}}│ {p:.3f}")
print(f" {'─' * (width + 12)}")
# ── 演示函数 ──────────────────────────────────────────────────
def demo_fft() -> None:
sample_rate = 8000
t, sig = generate_signal(sample_rate)
freqs, power = fft_analysis(sig, sample_rate)
peaks = find_peaks(freqs, power)
print("\n ── FFT 频谱分析 ──────────────────────────")
print(f" 采样率: {sample_rate} Hz 信号长度: {len(sig)} 点")
print(f" 奈奎斯特频率: {sample_rate//2} Hz(可分析的最高频率)")
print(f"\n 主要频率成分 Top {len(peaks)}:")
print(f" {'频率(Hz)':<12} {'幅度':<10} {'说明'}")
print(f" {'─'*12} {'─'*10} {'─'*20}")
for freq, amp in peaks:
note = ""
if abs(freq - 440) < 5: note = "← A4 基频"
elif abs(freq - 880) < 5: note = "← A5 泛音"
elif abs(freq - 1200) < 5: note = "← 高频分量"
print(f" {freq:<12.1f} {amp:<10.4f} {note}")
ascii_spectrum(freqs, power)
def demo_filter() -> None:
try:
from scipy import signal as sp_signal
except ImportError:
print(" ⚠️ 需要安装 scipy: pip install scipy")
return
sample_rate = 8000
t, sig = generate_signal(sample_rate)
filtered = lowpass_filter(sig, sample_rate, cutoff_hz=600)
# 对比滤波前后的频谱
freqs_raw, power_raw = fft_analysis(sig, sample_rate)
freqs_flt, power_flt = fft_analysis(filtered, sample_rate)
print("\n ── 低通滤波效果对比(截止频率 600 Hz)────")
print(f" {'频率(Hz)':<12} {'原始幅度':<12} {'滤波后幅度':<12} {'衰减'}")
print(f" {'─'*12} {'─'*12} {'─'*12} {'─'*10}")
check_freqs = [440, 880, 1200]
for target_f in check_freqs:
idx_r = np.argmin(np.abs(freqs_raw - target_f))
idx_f = np.argmin(np.abs(freqs_flt - target_f))
p_raw = power_raw[idx_r]
p_flt = power_flt[idx_f]
attn = (1 - p_flt / p_raw) * 100 if p_raw > 0 else 0
status = "✅ 保留" if target_f <= 600 else "🔇 衰减"
print(f" {target_f:<12} {p_raw:<12.4f} {p_flt:<12.4f} {attn:.1f}% {status}")
print(f"\n 滤波结果:440Hz 基频保留,880Hz/1200Hz 高频被衰减")
def demo_ascii() -> None:
sample_rate = 8000
t, sig = generate_signal(sample_rate)
freqs, power = fft_analysis(sig, sample_rate)
ascii_spectrum(freqs, power)
def main() -> None:
parser = argparse.ArgumentParser(description="音频信号处理演示")
parser.add_argument(
"--mode",
choices=["fft", "filter", "ascii", "all"],
default="all",
)
args = parser.parse_args()
if args.mode in ("fft", "all"):
demo_fft()
if args.mode in ("filter", "all"):
demo_filter()
if args.mode == "ascii":
demo_ascii()
if __name__ == "__main__":
main()
$ python3 38-python-audio-signal.py --mode fft
── FFT 频谱分析 ──────────────────────────
信号参数: 采样率=8000 Hz, 时长=1.0 s, 采样点=8000
频率分辨率: 1.0 Hz
主要频率成分(Top 5):
440.0 Hz 幅度=1.0000 ████████████████████████████████████████
880.0 Hz 幅度=0.5000 ████████████████████
1200.0 Hz 幅度=0.3000 ████████████
$ python3 38-python-audio-signal.py --mode ascii
频谱图(0 ~ 2000 Hz)
0 Hz |
100 Hz|
200 Hz|
300 Hz|
400 Hz| ████████████████████████████████████████ 440 Hz
500 Hz|
...
800 Hz| ████████████████████ 880 Hz
...
1200 Hz| ████████████ 1200 Hz
小结
| 概念 | 一句话记忆 |
|---|---|
np.sin(2*pi*f*t) |
生成频率为 f Hz 的正弦波 |
np.fft.fft(signal) |
时域 → 频域,返回复数数组 |
np.fft.fftfreq(n, d=1/sr) |
生成频率轴,d 是采样间隔 |
| 奈奎斯特定理 | 有效频率 ≤ 采样率/2 |
| 频率分辨率 | 采样率/采样点数,点数越多分辨率越高 |
| 局部极大值检测 | power[i] > power[i-1] and power[i] > power[i+1] |
signal.butter |
设计 Butterworth 滤波器,N=阶数,Wn=归一化截止频率 |
signal.filtfilt |
零相位滤波,先正向再反向,消除相位延迟 |
⏱ NexDo Time(5 分钟)
挑战:实现一个简单的音调检测器,判断输入信号的主频率对应哪个音符。
具体步骤:
- 用
generate_signal()生成信号,用fft_analysis找到最大幅度的频率 - 定义音符频率字典:
{"A4": 440, "A5": 880, "D6": 1175, ...} - 找到最接近的音符:
min(notes, key=lambda n: abs(notes[n] - detected_freq)) - 打印检测到的频率和对应音符名称
Don’t wait for next time, do it in the next moment.