デモ・概要
https://github.com/Kurene/pyaudio_spectrum_analyzer/blob/master/libmir/rasp_audio_stream.py
PyAudioとmatplotlib で、リアルタイムで音高を表示するプログラムを作ってみました。
このプログラムは、前回のリアルタイムスペクトルアナライザ を応用となっています。
実装
main.py
今回の実装は、簡単にアナライザを使えるようにオブジェクト指向で実装しています。
まず、AudioInputStream
オブジェクトによってループバック録音可能な入力ストリームを開きます。そして、ストリームオブジェクト ais.stream
をアナライザ SpectrumAnalyzer
のオブジェクトに渡すことで、リアルタイムでスペクトルを表示しています。ここで、PitchAnalyzer
クラスは SpectrumAnalyzer
クラスを継承しているため、同じように使えます。
以上の操作を書いたのが以下のスクリプトです。
オブジェクトの中身はさておき、これでデモのようなスペアナを走らせることができます。
import sys from libmir.rasp_audio_stream import AudioInputStream from libmir.rasp_analyzer import SpectrumAnalyzer from libmir.rasp_pitch_analyzer import PitchAnalyzer def test_callback_sigproc(sig, sr): print(sig.shape, sr) ais = AudioInputStream() mode = int(sys.argv[1]) if mode == 0: analyzer = SpectrumAnalyzer(ais.RATE, ais.CHUNK, ais.CHANNELS) else: analyzer = PitchAnalyzer(ais.RATE, ais.CHUNK, ais.CHANNELS) analyzer.run(ais.stream)
AudioInputStream: PC上の音の取得
PyAudioを使ってストリームとして音信号を取得しています。
実際の実装は以下をご覧ください。
PyAudioの使い方は、以下の記事を参考にしてください。
SpectrumAnalyzer: 波形表示
前回 の実装の内、波形表示部をSpectrum Analyzer
クラスとしてまとめています。
import numpy as np import matplotlib.pyplot as plt import matplotlib.animation class SpectrumAnalyzer(): def __init__(self, sr, n_chunk, n_ch): self.sr = sr self.n_chunk = n_chunk self.n_fft = n_chunk * 2 self.n_freq = self.n_fft // 2 + 1 self.n_ch = n_ch self.__set_mlp() self.set_plt() def __set_mlp(self): self.fig = plt.figure(figsize = (6, 4)) ax = self.fig.add_subplot(111) self.fig.patch.set_facecolor('gray') ax.patch.set_facecolor('black') win = plt.gcf().canvas.manager.window win.setWindowOpacity(0.85) def set_plt(self): self.x = np.linspace(0.0, 1.0, self.n_freq) x_labels = np.linspace(0.0, 1.0, self.n_freq) * (self.sr//2) self.artist = plt.plot([], [], c="c")[0] plt.xlim(0, 1.0) plt.ylim(-120, 0) plt.xticks(self.x[::self.n_freq//6], x_labels[::self.n_freq//6].astype(np.int)) plt.xlabel('Frequency') plt.ylabel('dB') plt.title('Spectrum Analyzer with pyaudio and matplotlib') plt.grid() def set_data(self, y): self.artist.set_data(self.x, y) def sig_proc(self, frame, spec): spec_mag = np.abs(spec) if frame == 0: self.msp = 1e-9 else: self.msp = np.maximum(self.msp, np.max(spec_mag)) y = 20 * np.log10(1e-9 + spec_mag / self.msp) # to dB return y def run(self, stream): sig_block = np.zeros(self.n_fft) n_block = self.n_fft n_block_1_4 = n_block // 4 n_block_2_4 = n_block // 2 n_block_3_4 = 3 * n_block // 4 fft_window = np.hanning(n_block) def update(frame): try: data = np.fromstring(stream.read(self.n_chunk), dtype=np.float32) # data: []L, R, L, R, ..., L, R] => data[n_fft, 2] (data[n_fft, 0] is Left channel) sig_tmp = np.reshape(data, (self.n_chunk, 2)).T # M/S processing if self.n_ch == 2: sig_current = (sig_tmp[0] + sig_tmp[1]) # Mid sig_block[n_block_3_4:n_block] = sig_current[0:n_block_1_4] # FFT spec = np.fft.rfft(fft_window * sig_block) # Signal proc y = self.sig_proc(frame, spec) sig_block[0:n_block_1_4] = sig_block[n_block_2_4:n_block_3_4] sig_block[n_block_1_4:n_block_3_4] = sig_current except IOError: pass self.set_data(y) return self.artist, # if blit is True, must return the list contains artist (Graph) objects anifunc = matplotlib.animation.FuncAnimation(self.fig, update, interval=0, blit=True) plt.show()
PitchAnalyzer: FFT振幅 ⇒ 音高の抽出
以下の記事で掲載した、FFT出力信号の振幅成分に各音高に対応した三角窓をかけることで実現しています。
実装としては、SpectrumAnalyzer
クラスを継承し、いくつかの関数を修正した PitchAnalyzer
クラスを定義しています。
import numpy as np import matplotlib.pyplot as plt import matplotlib.animation from libmir.rasp_analyzer import SpectrumAnalyzer from libmir.pitch import gen_pitch_windows class PitchAnalyzer(SpectrumAnalyzer): def set_plt(self): chroma_labels = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] n_chroma = 12 self.n_oct = 8 self.min_An = 4 - 2 #A4 - n self.tempo_ref = 440 freqs = np.linspace(0.0, 1.0, self.n_fft//2 + 1) * (self.sr / 2) offset = self.min_An * n_chroma - 3 indices = np.arange(0, n_chroma * self.n_oct) log_freqs = self.tempo_ref * 2 ** ( (indices - offset) / n_chroma ) self.n_pitch = len(log_freqs) self.pitch_windows = gen_pitch_windows(freqs, log_freqs) self.x = np.linspace(0.0, 1.0, self.n_pitch) x_labels = [ c+str(p+1) for p in range(0, self.n_oct) for c in chroma_labels] self.artist = plt.plot([], [], c="c")[0] plt.xlim(0, 1.0) plt.ylim(-120, 0) plt.xticks(self.x[::n_chroma], x_labels[::n_chroma]) plt.xlabel('Frequency') plt.ylabel('dB') plt.title('Pitch Analyzer with pyaudio and matplotlib') plt.grid() def sig_proc(self, frame, spec): spec_mag = np.abs(spec) log_spec_mag = np.dot(self.pitch_windows, spec_mag) if frame == 0: self.msp = 1e-9 else: self.msp = np.maximum(self.msp, np.max(log_spec_mag)) y = 20 * np.log10(1e-9 + log_spec_mag / self.msp) # to dB return y
まとめ
PythonとPyAudioを使って、リアルタイムで音高を表示するスペクトルアナライザを実装しました。 前回の実装をオブジェクト指向でリファクタリングし、音高表示部をシンプルに実装しました。
リアルタイムでのプロット用モジュールとしては PyQtGraph
などがよく進められているのですが、プロット用モジュールとして使いなれているmatplotlib
でも簡単&リアルタイム表示ができることを確かめることができたのが収穫でした。PyQtと組み合わせて何かアプリでも作れたらいいなと思っています。