はじめに
「自分が普段使っているPCスピーカーやヘッドホンは、実際どれくらいの周波数特性を持っているんだろう?」
「低音はどこまで出ているのか、高音はどこで途切れるのか、自分の耳と機材で確かめてみたい!」
そんな疑問から、Python(Tkinter + NumPy + sounddevice)を使って、滑らかな周波数スイープ音の生成機能と、マイク入力のリアルタイムFFTスペクトラムアナライザーを統合したデスクトップアプリ 「SoundTune」 を開発しました。
本記事では、SoundTune の技術的な実装ポイントと、開発中に起きたちょっとしたハプニングについて紹介します。
🎵 SoundTune の概要と主な機能
SoundTune は、低音から高音まで周波数が連続的に変化する音(周波数スイープ音 / Chirp音)を生成・再生しながら、マイク入力の応答をリアルタイムでFFT(高速フーリエ変換)解析してグラフィカルに描画できる Python GUI アプリケーションです。
🌟 主な機能
-
周波数スイープ音の出力 (Frequency Sweep)
- 開始周波数 (20Hz〜2,000Hz) から終了周波数 (20Hz〜10,000Hz) へ滑らかに音を変化
- 線形スイープ (Linear) と 対数スイープ (Logarithmic) に対応
- 正弦波 (Sine) / 矩形波 (Square) / 鋸波 (Sawtooth) / 三角波 (Triangle) の 4 種類の波形に対応
-
リアルタイムマイク・スペクトラムアナライザー (FFT)
- マイクからの入力をリアルタイムで 32 個の周波数帯(20Hz〜20,000Hz)に分解し、Peak Hold(ピークライン維持)付きのグラフィックイコライザーを描画
-
高い環境互換性(自動フォールバック)
-
sounddevice(PortAudio) が利用できない環境でも、システムコマンド (aplay,pw-play,ffplay) へ自動フォールバックして確実に再生
-
🛠️ 技術スタックとアーキテクチャ
| カテゴリ | 技術 / ライブラリ | 役割 |
|---|---|---|
| GUI / 描画 |
tkinter, ttk, Canvas
|
ダークモードUI & 高速キャンバス描画 |
| 数値計算 / 信号処理 |
numpy, scipy
|
Chirp波形合成・FFT解析・バンド集計 |
| オーディオ I/O | sounddevice |
低レイテンシの音声出力 & マイク入力ストリーム |
| フォールバック再生 | subprocess |
aplay / pw-play / ffplay によるシステム再生 |
ファイル構成は関心事の分離を意識してシンプルに設計しています。
SoundTune/
├── main.py # アプリケーションのエントリポイント
├── ui_app.py # TkinterベースのメインGUI & リアルタイム描画
├── audio_engine.py # 周波数スイープ計算、PCM合成、マイクFFT解析エンジン
├── requirements.txt # 依存ライブラリ
└── README.md
💡 実装の技術的ハイライト
1. 周波数スイープ(Chirp信号)の波形合成
周波数が時間とともに変化する音響信号を「Chirp(チャープ)信号」と呼びます。単に sin(2 * pi * f * t) とすると周波数変化に伴って位相が不連続になってしまいます。
そのため、時間 $t$ における瞬時周波数 $f(t)$ を積分して位相 $\phi(t)$ を求めます。
線形スイープ (Linear Sweep)
周波数が直線的に変化する場合:
$$f(t) = f_{\text{start}} + \frac{f_{\text{end}} - f_{\text{start}}}{T} \cdot t$$
これを積分した位相 $\phi(t)$ は以下のようになります:
$$\phi(t) = 2\pi \left( f_{\text{start}} \cdot t + \frac{f_{\text{end}} - f_{\text{start}}}{2T} \cdot t^2 \right)$$
Python + NumPy での実装例:
import numpy as np
def generate_linear_sweep(f_start, f_end, duration, sample_rate=44100):
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
# 瞬時周波数の積分による位相計算
phase = 2 * np.pi * (f_start * t + 0.5 * (f_end - f_start) / duration * (t ** 2))
# 正弦波の合成
waveform = np.sin(phase)
return waveform
対数スイープ(人の耳の周波数知覚に近い変化)や矩形波・三角波への波形変換も、この位相計算をベースに行っています。
2. NumPy によるリアルタイム FFT アナライザー
マイクからの入力音声(PCMデータ)を非同期ストリームで受け取り、リアルタイムでスペクトラム表示します。
-
窓関数の適用: フレーム境界の断続ノイズ(リーク誤差)を抑えるため、ハニング窓 (
np.hanning) を乗算 -
FFTの実行:
np.fft.rfftを使用して実数FFTを高速処理 - 対数周波数バンド化: 人間の聴覚特性に合わせ、20Hz〜20kHzを対数スケールで 32 個のバンドに分割・平滑化
def process_mic_fft(pcm_data, sample_rate=44100, num_bands=32):
# 窓関数の適用
window = np.hanning(len(pcm_data))
windowed = pcm_data * window
# 実数FFT
fft_spectrum = np.abs(np.fft.rfft(windowed))
freqs = np.fft.rfftfreq(len(pcm_data), 1 / sample_rate)
# 対数スケールで 32 バンドにバケット分け
log_edges = np.logspace(np.log10(20), np.log10(20000), num_bands + 1)
band_levels = []
for i in range(num_bands):
idx = np.where((freqs >= log_edges[i]) & (freqs < log_edges[i+1]))[0]
if len(idx) > 0:
val = np.mean(fft_spectrum[idx])
else:
val = 0.0
band_levels.append(val)
return band_levels
3. オーディオデバイス非依存の自動フォールバック設計
sounddevice は低レイテンシで強力ですが、Linux環境やPortAudioの設定によってはデバイスオープンに失敗することがあります。
SoundTune では、sounddevice での再生に失敗した場合、一時的な WAV ファイルを書き出してシステムコマンド(aplay, pw-play, ffplay)をバックグラウンド実行するフォールバック機構を備えています。これにより、どんな環境でもアプリが落ちずに音を鳴らせます。
🎙️💥 開発中に起きたハプニング:壊れたマイクの発見
マイクアナライザー機能を実装し、「いざ動作確認!」とスピーカーからスイープ音を出してマイクで集音テストを行いました。
しかし……アナライザーのバーがピクリとも動かない。
「FFTの計算式が間違っているのか?」「PortAudioの入力ストリームコールバックが動いていない?」「入力バッファのサイズが原因か?」と、コード見直しとデバッグに半日近く費やしました。
しかし、いくらプログラムを直しても反応がありません。最終的に気になって別アプリでマイクテストを行ってみたところ……
「マイク自体が物理的に壊れていた」 ことが判明しました(笑)。
想定外のオチに最初はガックリとしましたが、よく考えると 「オンライン会議や日常の通話で故障マイクを使って周囲に迷惑をかける前に発見できた」 という予期せぬ副産物(ライブラリ開発の恩恵?)を得ることができました。
📝 まとめ
自分でツールを作ってみると、オーディオプログラミングや信号処理の学びになるだけでなく、思わぬところで身の回りのハードウェアのトラブルに気づけるという発見がありました。
作成した SoundTune のコードは GitHub にてオープンソースで公開しています。
- 📦 GitHub リポジトリ: amekusa03/SoundTune