Wizard Notes

音楽信号解析の技術録、音楽のレビューおよび分析、作曲活動に関する雑記です

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

この記事をシェアする

デモ・概要

前回のリアルタイムスペクトルアナライザ を応用して、リアルタイムで音高を表示するプログラムを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上の音の取得

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