Wizard Notes

Python, JavaScript を使った音楽信号分析の技術録、作曲活動に関する雑記

Python+PyAudioで作るリアルタイム音高アナライザ

デモ・概要

https://github.com/Kurene/pyaudio_spectrum_analyzer/blob/master/libmir/rasp_audio_stream.py

PyAudioとmatplotlib で、リアルタイムで音高を表示するプログラムを作ってみました。

このプログラムは、前回のリアルタイムスペクトルアナライザ を応用となっています。

www.wizard-notes.com

実装

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を使ってストリームとして音信号を取得しています。

実際の実装は以下をご覧ください。

github.com

PyAudioの使い方は、以下の記事を参考にしてください。

www.wizard-notes.com

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出力信号の振幅成分に各音高に対応した三角窓をかけることで実現しています。

www.wizard-notes.com

実装としては、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

まとめ

PythonPyAudioを使って、リアルタイムで音高を表示するスペクトルアナライザを実装しました。 前回の実装をオブジェクト指向リファクタリングし、音高表示部をシンプルに実装しました。

リアルタイムでのプロット用モジュールとしては PyQtGraph などがよく進められているのですが、プロット用モジュールとして使いなれているmatplotlib でも簡単&リアルタイム表示ができることを確かめることができたのが収穫でした。PyQtと組み合わせて何かアプリでも作れたらいいなと思っています。

付録:PythonスクリプトGitHub

github.com