文章

38 · 音频信号处理:FFT 频谱分析与低通滤波

#047 · 2026-04-17 · Python

🔗 知识图谱导航:阅读本文前,建议先掌握《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 分钟)

挑战:实现一个简单的音调检测器,判断输入信号的主频率对应哪个音符。

具体步骤:

  1. generate_signal() 生成信号,用 fft_analysis 找到最大幅度的频率
  2. 定义音符频率字典:{"A4": 440, "A5": 880, "D6": 1175, ...}
  3. 找到最接近的音符:min(notes, key=lambda n: abs(notes[n] - detected_freq))
  4. 打印检测到的频率和对应音符名称

Don’t wait for next time, do it in the next moment.