短時間フーリエ変換(STFT)の理論とPython実装

短時間フーリエ変換(STFT)の数学的基礎から、スペクトログラムの生成・解釈、時間-周波数分解能のトレードオフ、Pythonによる実装まで体系的に解説します。

はじめに

FFT(高速フーリエ変換)は信号全体の周波数成分を一度に解析できる強力な手法ですが、時間情報が失われるという根本的な制約があります。例えば、楽音が時間とともに変化するメロディや、機械の振動パターンの変化を解析したい場合、「どの時刻にどの周波数が存在するか」という情報が必要です。

**短時間フーリエ変換(STFT: Short-Time Fourier Transform)**は、この問題に対するアプローチとして1940年代から使われてきた手法です。信号を短い時間窓に分割して各窓にFFTを適用することで、**時間-周波数の2次元表現(スペクトログラム)**を得ます。

本記事では、STFTの数学的定義と時間-周波数分解能のトレードオフを詳しく解説し、Pythonによる実装とスペクトログラムの読み方を示します。

STFTの数学的定義

連続時間STFT

連続信号 \(x(t)\) の短時間フーリエ変換は以下のように定義されます。

\[\text{STFT}\{x\}(\tau, f) = X(\tau, f) = \int_{-\infty}^{\infty} x(t) \cdot w(t - \tau) \cdot e^{-j2\pi ft} \, dt \tag{1}\]

ここで:

  • \(w(t)\) :窓関数(有限長の局所化関数)
  • \(\tau\) :窓の中心時刻(時間シフトパラメータ)
  • \(f\) :周波数

\(w(t - \tau)\) は窓を時刻 \(\tau\) に中心を持つように平行移動したもので、この窓で信号を局所的に切り出してフーリエ変換を計算します。結果 \(X(\tau, f)\) は複素数であり、絶対値 \(|X(\tau, f)|\) が時刻 \(\tau\) における周波数 \(f\) 成分の振幅を、偏角 \(\angle X(\tau, f)\) が位相を表します。

離散時間STFT

実用的には離散時間信号を扱います。サンプリング周波数 \(f_s\) でサンプリングされた信号 \(x[n]\) (\(n = 0, 1, \ldots, N-1\) )の離散STFTは以下のように定義されます。

\[X[m, k] = \sum_{n=-\infty}^{\infty} x[n] \cdot w[n - mH] \cdot e^{-j2\pi kn/L} \tag{2}\]

ここで:

  • \(m\) :フレームインデックス(時間軸)
  • \(k\) :周波数ビンインデックス(\(k = 0, 1, \ldots, L-1\) )
  • \(H\) :ホップ長(フレームシフト)—連続するフレームの開始点の間隔(サンプル数)
  • \(L\) :窓長(FFTの点数)
  • \(w[n]\) :窓関数(長さ \(L\) で定義)

各フレームは時刻 \(mH\) を中心に長さ \(L\) の窓で切り出され、その窓内の信号にFFTが適用されます。

オーバーラップとホップ長

連続するフレーム間の重なり(オーバーラップ)は次の関係で定義されます。

\[\text{Overlap} = 1 - \frac{H}{L} \tag{3}\]

例えば、\(L = 1024\) 、\(H = 512\) の場合、オーバーラップは50%です。オーバーラップが大きいほど時間分解能が高くなりますが、計算コストも増加します。実用上は50%〜75%のオーバーラップが一般的に使われます。

時間-周波数分解能のトレードオフ

STFTの最も重要な制約は時間-周波数分解能のトレードオフです。これは量子力学のハイゼンベルクの不確定性原理と数学的に同等な関係です。

ガボール限界(Gabor Limit)

時間分解能 \(\Delta t\) と周波数分解能 \(\Delta f\) の積には下限があります。

\[\Delta t \cdot \Delta f \geq \frac{1}{4\pi} \tag{4}\]

これは信号処理における不確定性原理とも呼ばれます。

窓長とトレードオフ

窓長 \(L\) (秒単位では \(T_w = L / f_s\) )によって分解能が決まります。

窓長 \(T_w\)時間分解能 \(\Delta t\)周波数分解能 \(\Delta f\)
短い(小さい \(L\) )高い低い(隣接周波数の分離困難)
長い(大きい \(L\) )低い高い(周波数を細かく分離可能)

具体例として、\(f_s = 44100\) Hz の音声信号の場合:

  • \(L = 512\) (約12 ms):時間変化に敏感、ただし周波数分解能は約86 Hz
  • \(L = 4096\) (約93 ms):周波数分解能は約11 Hz、ただし時間変化の追従が遅い

周波数分解能は \(\Delta f = f_s / L\) です。

Python実装

基本的なSTFT実装

まずSTFTをゼロから実装し、その動作を理解します。

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import stft, istft
from scipy.signal.windows import hann

def compute_stft_manual(x, window, hop_length, n_fft):
    """
    STFTの手動実装。

    Parameters
    ----------
    x : np.ndarray
        入力信号(1D)
    window : np.ndarray
        窓関数(長さ n_fft)
    hop_length : int
        フレームシフト(サンプル数)
    n_fft : int
        FFTの点数(= 窓長)

    Returns
    -------
    stft_matrix : np.ndarray (complex)
        形状 (n_fft//2 + 1, n_frames) の複素スペクトル行列
    """
    n_frames = 1 + (len(x) - n_fft) // hop_length
    stft_matrix = np.zeros((n_fft // 2 + 1, n_frames), dtype=complex)

    for m in range(n_frames):
        # フレームの切り出し
        start = m * hop_length
        frame = x[start : start + n_fft]
        # 窓関数の適用
        windowed_frame = frame * window
        # FFT(正の周波数成分のみ取得)
        stft_matrix[:, m] = np.fft.rfft(windowed_frame)

    return stft_matrix


# --- テスト信号の生成 ---
fs = 4000          # サンプリング周波数 [Hz]
duration = 2.0     # 信号長 [s]
t = np.arange(0, duration, 1/fs)

# 時間変化する信号(チャープ + パルス)
# 0〜1秒: 100Hz → 500Hzに線形変化するチャープ
# 1〜2秒: 300Hzの定常正弦波
chirp = np.zeros_like(t)
segment1 = t < 1.0
chirp[segment1] = np.sin(2 * np.pi * (100 + 200 * t[segment1]) * t[segment1])
chirp[~segment1] = np.sin(2 * np.pi * 300 * t[~segment1])
# ホワイトノイズを加える
np.random.seed(42)
signal = chirp + 0.1 * np.random.randn(len(t))

# --- STFTのパラメータ ---
n_fft = 512
hop_length = n_fft // 4   # 75% オーバーラップ
window = hann(n_fft)

# --- 手動実装によるSTFT ---
stft_matrix = compute_stft_manual(signal, window, hop_length, n_fft)

# スペクトログラム(振幅)の計算
spectrogram = np.abs(stft_matrix)

# 時間・周波数軸の計算
n_frames = spectrogram.shape[1]
times = np.arange(n_frames) * hop_length / fs
freqs = np.fft.rfftfreq(n_fft, 1/fs)

# --- プロット ---
fig, axes = plt.subplots(2, 1, figsize=(12, 8))

# 元の信号
axes[0].plot(t, signal, linewidth=0.5)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Input Signal: Chirp (100→500 Hz) + 300 Hz')
axes[0].grid(True, alpha=0.3)

# スペクトログラム
img = axes[1].pcolormesh(
    times, freqs, 20 * np.log10(spectrogram + 1e-10),
    shading='gouraud', cmap='inferno', vmin=-60, vmax=0
)
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Frequency [Hz]')
axes[1].set_title(f'Spectrogram (n_fft={n_fft}, hop={hop_length})')
axes[1].set_ylim(0, 800)
plt.colorbar(img, ax=axes[1], label='Magnitude [dB]')

plt.tight_layout()
plt.show()

scipy.signal.stft を使った効率的な実装

実際のプロジェクトでは scipy.signal.stft を利用するのが推奨されます。

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import stft, istft
from scipy.signal.windows import hann

# --- テスト信号(音声系の模擬) ---
fs = 8000
t = np.arange(0, 3.0, 1/fs)

# 時間変化する信号を構成
# 0〜1秒: 200Hz
# 1〜2秒: 200Hz + 500Hz
# 2〜3秒: 800Hz
x = np.zeros_like(t)
mask1 = t < 1.0
mask2 = (t >= 1.0) & (t < 2.0)
mask3 = t >= 2.0
x[mask1] = np.sin(2 * np.pi * 200 * t[mask1])
x[mask2] = (np.sin(2 * np.pi * 200 * t[mask2])
            + 0.7 * np.sin(2 * np.pi * 500 * t[mask2]))
x[mask3] = np.sin(2 * np.pi * 800 * t[mask3])
x += 0.05 * np.random.randn(len(t))

# --- 窓長を変えてSTFT比較 ---
n_fft_list = [128, 512, 2048]
fig, axes = plt.subplots(len(n_fft_list), 1, figsize=(12, 10))

for ax, n_fft in zip(axes, n_fft_list):
    hop = n_fft // 4
    f, t_stft, Zxx = stft(x, fs=fs, window='hann',
                           nperseg=n_fft, noverlap=n_fft - hop)
    magnitude_dB = 20 * np.log10(np.abs(Zxx) + 1e-10)

    img = ax.pcolormesh(t_stft, f, magnitude_dB,
                        shading='gouraud', cmap='inferno',
                        vmin=-60, vmax=0)
    ax.set_ylabel('Frequency [Hz]')
    ax.set_ylim(0, 1200)
    df = fs / n_fft
    dt_ms = hop / fs * 1000
    ax.set_title(
        f'n_fft={n_fft}: Δf={df:.1f} Hz, Δt={dt_ms:.1f} ms '
        f'(time-freq tradeoff)'
    )
    plt.colorbar(img, ax=ax, label='dB')

axes[-1].set_xlabel('Time [s]')
plt.tight_layout()
plt.show()

このプロットから、\(n\_fft = 128\) では時間変化は鮮明ですが周波数の分離が粗く、\(n\_fft = 2048\) では周波数分解能が高い反面、各区間の境界が時間方向にぼやけることが確認できます。

逆STFTによる信号再構成

STFTはその逆変換(ISTFT)によって元の信号を再構成できます。適切な窓関数とホップ長を選べば**完全再構成(Perfect Reconstruction)**が可能です。

\[x[n] = \frac{\sum_{m} X[m, k] \cdot w[n - mH]}{\sum_{m} w^2[n - mH]} \tag{5}\]
from scipy.signal import stft, istft

# STFTの計算
n_fft = 512
hop = n_fft // 4
f, t_stft, Zxx = stft(x, fs=fs, window='hann',
                       nperseg=n_fft, noverlap=n_fft - hop)

# 逆STFTで再構成
_, x_reconstructed = istft(Zxx, fs=fs, window='hann',
                            nperseg=n_fft, noverlap=n_fft - hop)

# 再構成誤差の確認
min_len = min(len(x), len(x_reconstructed))
error = np.max(np.abs(x[:min_len] - x_reconstructed[:min_len]))
print(f"最大再構成誤差: {error:.2e}")
# → 1e-14 程度(ほぼ完全再構成)

スペクトログラムの読み方

スペクトログラムを解釈する際のポイントを整理します。

横軸・縦軸・色の意味

軸/要素意味単位
横軸時間(各フレームの中心時刻)秒 [s]
縦軸周波数Hz
色(輝度)振幅または対数スケールのパワーdB

色マップは inferno(暗→明:低→高パワー)や viridisjet などがよく使われます。

dBスケールの重要性

振幅をリニアスケールで表示すると、強い成分に支配されて弱い成分が見えなくなります。対数スケール(dB)を使うことで、大きなダイナミックレンジをカバーできます。

\[\text{Magnitude [dB]} = 20 \log_{10}(|X[m, k]| + \epsilon) \tag{6}\]

\(\epsilon\) は数値的安定性のための小さな定数(例: \(10^{-10}\) )です。

ハーモニクスの確認

音声や楽器音のスペクトログラムでは、基本周波数 \(f_0\) とその整数倍のハーモニクス(\(2f_0, 3f_0, \ldots\) )が横縞として現れます。これをピッチ推定に活用できます。

実践:音声信号への応用

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import stft
from scipy.io import wavfile

# --- 音声的な合成信号の作成 ---
fs = 16000
duration = 1.5
t = np.arange(0, duration, 1/fs)

# 母音「あ」を模擬: F0=120Hz, フォルマント F1=800Hz, F2=1200Hz
np.random.seed(0)
glottal = np.zeros_like(t)
T0 = int(fs / 120)  # 基本周期
for i in range(0, len(t), T0):
    if i + 20 < len(t):
        glottal[i:i+20] = np.exp(-np.arange(20) * 0.3)  # 声帯パルス近似

# フォルマントフィルタ(簡易)
from scipy.signal import lfilter, butter

def formant_filter(signal, center_freq, bandwidth, fs):
    """単純なバンドパスフィルタでフォルマントを模擬"""
    b, a = butter(2, [center_freq - bandwidth/2, center_freq + bandwidth/2],
                  btype='band', fs=fs)
    return lfilter(b, a, signal)

voiced = (formant_filter(glottal, 800, 200, fs)
          + 0.7 * formant_filter(glottal, 1200, 150, fs)
          + 0.4 * formant_filter(glottal, 2500, 200, fs))
voiced = voiced / np.max(np.abs(voiced) + 1e-8)

# --- STFTとスペクトログラム ---
n_fft = 512
hop = n_fft // 8  # 87.5% オーバーラップ(音声処理では一般的)

f, t_stft, Zxx = stft(voiced, fs=fs, window='hann',
                       nperseg=n_fft, noverlap=n_fft - hop)

fig, axes = plt.subplots(2, 1, figsize=(12, 8))

axes[0].plot(t, voiced, linewidth=0.5)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Synthetic Voiced Sound (vowel "a" approximation)')
axes[0].grid(True, alpha=0.3)

img = axes[1].pcolormesh(
    t_stft, f,
    20 * np.log10(np.abs(Zxx) + 1e-10),
    shading='gouraud', cmap='inferno', vmin=-60, vmax=0
)
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Frequency [Hz]')
axes[1].set_ylim(0, 4000)
axes[1].set_title('Spectrogram: Formants visible at ~800 Hz, 1200 Hz, 2500 Hz')
plt.colorbar(img, ax=axes[1], label='dB')

plt.tight_layout()
plt.show()

スペクトログラムには800 Hz・1200 Hz・2500 Hz付近に水平縞(フォルマント)が現れ、基本周波数120 Hzの整数倍ハーモニクスも確認できます。

STFTの限界とウェーブレット変換

STFTは窓長が固定であるため、時間-周波数分解能のトレードオフが「全周波数で一定」という制約があります。低周波数成分は短時間で多サイクルが収まるため固定窓で適切に解析できますが、高周波数成分には問題が生じることもあります。

ウェーブレット変換はこの問題を「周波数に応じた可変窓長」で解決します。低周波数では長い窓(高周波数分解能)、高周波数では短い窓(高時間分解能)を使うことで、より効率的な時間-周波数表現が得られます。

手法時間分解能周波数分解能窓長主な用途
FFTなし(全体)高(固定)全信号定常信号のスペクトル解析
STFT中(窓長依存)固定音声・音楽解析
ウェーブレット変換可変可変周波数依存過渡現象・スケール解析

実用的なパラメータ選択ガイド

用途推奨 \(n\_fft\)推奨オーバーラップ理由
音声認識(MFCC前処理)512〜102450〜75%フォルマント分解能と時間変化のバランス
音楽解析(ピッチ検出)2048〜409675%音程の分解能(約10 Hz)が必要
機械振動診断1024〜409650%高調波を分離できる周波数分解能
リアルタイム処理256〜51250%低遅延が優先
異常検知(衝撃検出)128〜25675%過渡現象の時間分解能を優先

まとめ

  • STFTは信号を短い時間窓に分割してFFTを適用することで、時間と周波数の両方の情報を持つ2次元スペクトル表現(スペクトログラム)を生成する
  • 時間-周波数分解能のトレードオフ(不確定性原理)は本質的な制約であり、窓長を短くすると時間分解能が向上する代わりに周波数分解能が低下する
  • 窓関数はスペクトル漏れを制御するために重要であり、音声処理では一般的にHann窓が使われる
  • **逆STFT(ISTFT)**により、適切なパラメータ選択のもとで元の信号をほぼ完全に再構成できる
  • スペクトログラムはdBスケールで表示することで広いダイナミックレンジをカバーし、ハーモニクス・フォルマント・周波数変化を視覚的に把握できる
  • 時間分解能と周波数分解能の両立が必要な場合はウェーブレット変換も検討すべきである

関連記事

参考文献

  • Allen, J. B. (1977). “Short term spectral analysis, synthesis, and modification by discrete Fourier transform.” IEEE Transactions on Acoustics, Speech, and Signal Processing, 25(3), 235-238.
  • Griffin, D., & Lim, J. (1984). “Signal estimation from modified short-time Fourier transform.” IEEE Transactions on Acoustics, Speech, and Signal Processing, 32(2), 236-243.
  • Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
  • SciPy Signal Processing — stft
  • Librosa: Audio and Music Analysis