Pythonで短時間フーリエ変換(scipy signal ShortTimeFFT)

scipy signal ShortTimeFFT クラスを使用した短時間フーリエ変換について紹介します。

Pythonでスペクトログラムの表示(scipy signal ShortTimeFFT)で同様のクラスを紹介しましたが、短時間フーリエ変換についても書き留めたいと思います。

前回の記事と重複する箇所がいくつかありますが、ご容赦ください。

ShortTimeFFT について

scipy signal ShortTimeFFT は SciPy v1.12.0 で提供されたクラスです。

scipy signal では短時間フーリエ変換のための関数として stft や spectrogram を提供していましたが、Scipy v1.12.0 以降はレガシーとなり、今後はアップデートされないみたいです[1]。

ShortTimeFFT のメソッドに stft という短時間フーリエ変換するものがあるため[2]、それを使用していきます。

パラメータと返り値

ShortTimeFFT クラス

scipy signal ShortTimeFFT クラスのパラメータは以下です。

scipy.signal.ShortTimeFFT(win, hop, fs, fft_mode='onesided', mfft=None, dual_win=None, scale_to=None, phase_shift=0)
表:scipy.signal.ShorTimeFFTのパラメータ
パラメータ名 データ型 概要
win np.ndarray 分析窓関数
hop int ホップサイズ
fs float サンプリングレート
fft_mode str スペクトログラムの出力方法(‘twosided’, ‘centered’, ‘onesided’, ‘onesided2X’)
mfft int FFT点数
dual_win np.ndarray 合成窓関数
scale_to str 窓関数の補正(‘magnitude’, ‘psd’)
phase_shift int 線形位相シフトの量

stft メソッド

短時間フーリエ変換を求めるメソッド ShortTimeFFT stft のパラメータは以下です。

ShortTimeFFT.stft(x, p0=None, p1=None, k_offset=0, padding='zeros', axis=-1)
表:ShorTimeFFT.stft のパラメータ
パラメータ名 データ型 概要
x np.ndarray 入力データ
p0 int 開始フレーム
p1 int 終了フレーム
k_offset int 開始サンプル
padding str パディング方法('zeros', 'edge', 'even', 'odd')
axis int 入力データの時間軸

istft メソッド

逆短時間フーリエ変換を求めるメソッド ShortTimeFFT istft のパラメータは以下です。

ShortTimeFFT.istft(x, k0=0, k1=None, f_axis=-2, t_axis=-1)
表:ShorTimeFFT.istftのパラメータ
パラメータ名 データ型 概要
x np.ndarray 入力データ
k0 int 開始サンプル
k1 int 終了サンプル
f_axis int 入力データの周波数軸
t_axis int 入力データの時間軸

主な使用例

以下の操作手順を行う ShortTimeFFT の使用例を説明します。

  1. 短時間フーリエ変換
  2. 周波数成分の操作
  3. 逆短時間フーリエ変換
  4. スペクトログラム出力

ソースコード

上記の操作手順を行うソースコードは以下です。モノラルとステレオのどちらにも対応しています。ステレオの場合、スペクトログラムは L チャンネルだけグラフ出力します。

import numpy as np
import soundfile as sf
import scipy.signal as sg
import matplotlib.pyplot as plt

# WAV を読み込む
x, fs = sf.read('input.wav', always_2d=True)
x = x.T                # 転置
length_x = x.shape[1]  # xのデータ長
N_fft = 512            # FFT点数

# 窓関数作成
win = sg.windows.hann(N_fft)  # ハン窓作成

# 短時間フーリエ変換
SFT = sg.ShortTimeFFT(win, hop=N_fft//2, fs=fs)
X_stft = SFT.stft(x)
Y_stft = np.zeros(X_stft.shape, dtype=np.complex128)
Y_stft = X_stft

# 周波数成分の操作(高域をカット)
Y_stft[:,N_fft//4:N_fft//2+1,:] = 0

# 逆短時間フーリエ変換
y = SFT.istft(Y_stft, k1=length_x)

# 対数変換
X_pow = SFT.spectrogram(x)
Y_pow = SFT.spectrogram(y)
X_dB = 10 * np.log10(np.fmax(X_pow, 1e-8))
Y_dB = 10 * np.log10(np.fmax(Y_pow, 1e-8))

# 時間軸と周波数軸の作成
N_t = np.arange(X_stft.shape[2])
t = SFT.delta_t * N_t
f = SFT.f

# グラフ作成
fig = plt.figure(figsize=(12,8))
ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)
vmax=np.max(X_dB[0,:,:])
vmin=np.min(Y_dB[0,:,:])
ax1.pcolormesh(t, f, X_dB[0,:,:], cmap="jet", \
                vmax=vmax, vmin=vmin)
ax2.pcolormesh(t, f, Y_dB[0,:,:], cmap="jet", \
                vmax=vmax, vmin=vmin)
ax1.set_ylabel('Frequency [Hz]')
ax2.set_ylabel('Frequency [Hz]')
plt.xlabel('Time [sec]')
plt.savefig("stft.png")

# 出力信号をWAVに書き込む
y = y.T  # 転置
sf.write("output.wav", y, fs, subtype='PCM_16')

ソースコード説明

短時間フーリエ変換(16~17行目)
基本的には ShortTimeFFT に窓関数(win)、ホップサイズ(hop)、サンプリングレート(fs)を与えてインスタンス SFT を作成して、SFT.stft に入力データを与えるだけで短時間フーリエ変換ができます。

周波数成分の操作(21~22行目)
簡単な周波数成分の操作としてナイキスト周波数の半分より上をカットしています。他の操作をしたい場合は、この箇所の記述を変更してください。

逆短時間フーリエ変換(24~25行目)
SFT.istft に短時間フーリエ変換のデータを入れるだけで逆短時間フーリエ変換ができます。ただし、入力データ x と出力データ y の長さが異なってしまうので、k1=(xの長さ)とすることで、データ長をそろえています。

スペクトログラム出力(27~51行目)
SFT.spectrogram を使用してスペクトログラムを出力します。

WAV出力(53~55行目)
WAVファイルを出力します。sf.read と sf.write は (時間、チャンネル) という配列に対応していますが、ShortTimeFFT.stft が対応する配列は (チャンネル、時間) のため、8行目と54行目で転置しています。

出力結果

作成されたスペクトログラムを以下に示します。

図:出力されるスペクトログラム
図:出力されるスペクトログラム

ShortTimeFFT のパラメータ詳細

fft_mode

fft_mode には "twosided", "centered", "onesided", "onesided2X" があり、出力されるFFTの結果が変わります。以下の fs はサンプリング周波数です。

twosided
0 [Hz] ~ fs [Hz]の短時間フーリエ変換データを出力するモード。

centered
-fs/2 [Hz] ~ fs/2 [Hz]の短時間フーリエ変換データを出力するモード。

onesided
0 [Hz] ~ fs/2 [Hz]の短時間フーリエ変換データを出力するモード。

onesided2X
0 [Hz] ~ fs/2 [Hz]の短時間フーリエ変換データを出力するモード。また、0 以外の周波数成分の大きさがscale_to = "magnitude"のときは2倍、scale_to = "psd"のときは \(\sqrt{2}\) 倍 になる。

fft_modeを各モードに設定したときのスペクトログラムを以下に示します。

図:fft_modeの違い
図:fft_modeの違い

scale_to

scale_to には "magnitude", "psd" があります。

magnitude
以下の補正した窓関数 \(w'\) を使用して短時間フーリエ変換を計算する。\(N\) は窓関数の大きさです。

$$
w'[n] = \frac{w[n]}{\sum_{n=0}^{N-1}w[n]}
$$

psd
以下の補正した窓関数 \(w'\) を使用して短時間フーリエ変換を計算する。\(f_{s}\) はサンプリング周波数です。

$$
w'[n] = \frac{w[n]}{\sqrt{f_{s}\sum_{n=0}^{N-1}|w[n]|^2}}
$$

上記の補正した窓関数を使用することで、短時間フーリエ変換の絶対値の2乗を計算するだけで、パワースペクトル密度(PSD)が求められる。

phase_shift

以下のように短時間フーリエ変換後のデータ \(X[i,k]\) に対して位相シフトを行います。\(N_{shift}\) は phase_shift で指定した値です。

$$
X'[i,k] = X[i,k] \exp{\left(j 2\pi \frac{k}{N} N_{shift}\right)}
$$

おわりに

scipy signal ShortTimeFFT による短時間フーリエ変換についてまとめました。

Python で短時間フーリエ変換を使うときに参考にしてください。

■参考文献
[1] The SciPy community. “scipy.signal.stft”. SciPy v1.15.0 Manual. 2008-2024.
https://docs.scipy.org/doc/scipy-1.15.0/reference/generated/scipy.signal.stft.html, (参照2025-01-11)
[2] The SciPy community. “scipy.signal.ShortTimeFFT”. SciPy v1.15.0 Manual. 2008-2024.
https://docs.scipy.org/doc/scipy-1.15.0/reference/generated/scipy.signal.ShortTimeFFT.html, (参照2025-01-11)