プログラミング習得への道のり #11 メトロノーム機能をつける ~PCキーボードで演奏したい~

プログラミング完全初心者の私が、プログラミングを習得するまでの全過程(主に悩み…)を記事にしていきます。

プログラミング学習にあたり、以下の順で進めています。

①アウトプットを決める、②基礎を学習する、③アウトプットを作り始める

アウトプットは PCキーボードで演奏できるアプリケーション に決めました。

前回までの記事で “TKinterを用いて画面(GUI部分)をカスタマイズする” にチャレンジし、BPMをスケールバーで変更できるウィジェットや、押したキーを表示するウィジェットを追加することができました。

「ステップ1.キーボード入力で音を鳴らす」に足りない要素として、メトロノーム機能があります。また、課題として、キープレスして音声が流れてからグラフがプロットされるまでにラグがあることや、キーを連続で押したときに音声が途切れてしまうことがあげられます。そこで本記事では、 TKinterを用いてメトロノーム機能をつけ、残課題を解消する にチャレンジしました。 

足りない要素

  • メトロノーム機能をつけること

残課題

  • キープレス、音声出力、グラフプロット間のリアルタイム性
  • 音声出力の連続性

PCキーボード演奏アプリの作成ステップ

  1. キーボード入力で音を鳴らす
    ☑1−1.1つの音を鳴らす
    ☑1-2.音階を鳴らす
    ☑1-3.音の長さを自由に変える
    1-4.画面をつくる
    1-5.メトロノーム機能をつくる
  2. 音を制御する
    2-1.波形を変える
    2-2.フィルタリング機能を追加する
    2-3.複数の音を同時に鳴らす
  3. 外部から音を取り込む
    3-1.単音の音源サンプルから音を取り込む(1楽器)
    3-2.音源から楽器を取り出す
  4. 機能を追加する
    4-1.録音&再生する(パターンをつくる)
    4-2.外部と連携する
  5. 曲を演奏する
  6. 曲を作る

☑︎本記事の内容

  • 必要な材料:メトロノーム機能をつける、リアルタイム性および連続性を調整する
  • レシピ①:音の連続性を調整する
  • レシピ②:スレッドを用いてメトロノーム機能をつける
  • レシピ③:スレッド、キューを用いてリアルタイム性を調整する

☑︎著者の経験

この記事を書いている私は、非情報系の大学院を卒業後、通信関係の企業に3年勤めた後、現在までコンサルティング会社に勤めています。
学生時代〜現職までプログラミングとは縁がなく、前職でルーターの設定を少しかじった程度のプログラミング完全初心者です。
こういった私が、解説していきます。

Twitterもはじめました。

必要な材料:GUI部分の作成

○ライブラリ

今回使用するライブラリは以下です。

使用したライブラリ

  • numpy:数値演算用ライブラリ → sin波を作る
  • PyAudio:オーディオ関連ライブラリ → 音を鳴らす
  • matplotlib:グラフ作成ライブラリ → 波形をプロットする
  • Tkinter:GUI作成ライブラリ → 画面をつくる

レシピ①:音の連続性を調整する

まずは、今ある知識で修正できる部分から着手しました。

現状、キーを連続して入力すると、1音1音途切れてしまいます。

コードはこちら
 

# ライブラリのインポート
import tkinter as tk
import numpy as np  # sin波
import pyaudio  # メモリ上の音楽を再生
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

#=== アプリケーションの定義
class Synth_application(tk.Frame):
    #=== 初期設定
    def __init__(self, master=None):
        #=== メインウィンドウの設定
        super().__init__(master) # 親クラス(Application)の __init__ メソッドを実行
        self.master = master
        self.master.title('Synth_Application')
        self.master.resizable(width=False,height=False) #ウィンドウ幅の固定

        #=== オプション:メインウィンドウを画面ん中央に表示させる
        winWidth = 700 # メインウィンドウの幅を定義
        winHeight = 570 # メインウィンドウの高さを定義
        screenWidth = self.master.winfo_screenwidth() # 使用しているモニターの横幅を取得
        screenHeight = self.master.winfo_screenheight() # 使用しているモニターの縦幅を取得
        startX = int((screenWidth / 2) - (winWidth / 2))
        startY = int((screenHeight / 2) - (winHeight / 2))
        self.master.geometry('{}x{}+{}+{}'.format(winWidth, winHeight, startX, startY))  # f-strings 700px × 570pxの画面が左上(縦Xpx、横Ypx)

        #=== sin波の初期値設定 → def start_up
        self.start_up() # 初期値
        
        #=== ウィジットをつくる → def create_widgets
        self.create_widgets()
        
        #=== pyaudioの開始             
        self.p = pyaudio.PyAudio() 

        
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
        global freq # 初期値のためにglobal変数が必要

        freq = 261.26
        self.DURATION = {
            'L1': (60 / 100 * 4),      # 全音符
            'L2': (60 / 100 * 4) / 2,  # 二分音符
            'L4': (60 / 100 * 4) / 4,  # 四分音符
            'L8': (60 / 100 * 4) / 8,  # 八分音符
        }
        self.duration = self.DURATION['L4']
        self.bpm = 100 # New
        
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い

        self.t = np.arange(0, self.duration * self.rate) / self.rate
        self.envRange = np.ones(int(self.duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)
        
    #=== ウィジットの定義
    def create_widgets(self):
        #=== フレームをつくる
        self.canvas_frame = tk.Frame(self.master, width=700, height=250, bg="#000080")
        self.canvas_frame.grid(column=0, row=0) # 1行1列に配置
        self.canvas_frame.grid_propagate(0) # フレーム幅の固定
        self.canvas_frame.grid_anchor(tk.CENTER) # gridを中央に表示するには、grid()で配置する要素の親要素にgrid_anchor(tkinter.CENTER)を使う

        self.option_frame = tk.Frame(self.master, width=700, height=320, bd = 5, relief = tk.GROOVE) # New
        self.option_frame.grid(column=0, row=1)
        self.option_frame.grid_propagate(0)
        self.option_frame.grid_anchor(tk.CENTER)
        
        #=== ウィジェット:LabelFrameウィジットをつくる
        self.chunk_labelFrame = tk.LabelFrame(self.canvas_frame, text="WaveForm", fg='white', bg='#444', relief=tk.FLAT)
        self.chunk_labelFrame.grid(column=0, row=0)
        self.chunk_innerBox = tk.Frame(self.chunk_labelFrame)
        self.chunk_innerBox.grid(column=0, row=0)

        #=== Figureインスタンスをつくる
        fig = Figure(figsize=(6.5,2), dpi=100, tight_layout=True, facecolor='#F0F0F0') ## 650 x 200のFigureインスタンスを作成, tight_layoutでオブジェクトの配置を自動調整

        axes_envRange = fig.add_subplot(1, 2, 1)  # 1行2列の最初(1,1)に表示
        axes_envRange.set_title('envRange')
        axes_envRange.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_envRange.set_ylabel('envRange', fontsize=8) # ラベルのオプション

        axes_wave = fig.add_subplot(1, 2, 2)  # 1行2列の2番目(1,2)に表示
        axes_wave.set_title('Sin_wave')
        axes_wave.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylabel('sin_t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylim([-(self.gain + 0.05), (self.gain + 0.05)])

        self.line_envRange, = axes_envRange.plot(self.t, self.envRange)
        self.line_wave, = axes_wave.plot(self.sin_t[:500])       
        
        #=== ウィジェット:FigureCanvasTkAggを宣言し、LabelFrameウィジェット上に配置する
        self.canvas = FigureCanvasTkAgg(fig, master=self.chunk_innerBox)
        self.canvas.get_tk_widget().pack(padx=10, pady=10)
        self.canvas.get_tk_widget().bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        
        #=== ウィジェット:キープレスウィジェット
        self.keypress_labelFrame = tk.LabelFrame(self.option_frame, text="KeyPress", fg='white', bg='#444', relief=tk.FLAT) # New
        self.keypress_labelFrame.grid(column=0, row=0, padx=20 ,sticky=tk.N)
        self.keypress_innerBox = tk.Frame(self.keypress_labelFrame, width=250, height=60)
        self.keypress_innerBox.grid(column=0, row=0)
        self.keypress_innerBox.grid_propagate(0)
        self.keypress_innerBox.grid_anchor(tk.CENTER)      
        
        self.keypress_label = tk.Label(self.keypress_innerBox, text="push key is : ", font=("メイリオ", "16"))
        self.keypress_label.grid() 

        self.buffer = tk.StringVar()
        self.buffer.set('xxx')          
        self.press_key_label = tk.Label(self.keypress_innerBox, textvariable = self.buffer, font=("メイリオ", "16"))
        self.press_key_label.grid() 
        self.press_key_label.bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        self.press_key_label.focus_set()
        
        #=== ウィジェット:BPMスケールバーウィジェット
        self.scale_labelFrame = tk.LabelFrame(self.option_frame, text="BPM", fg='white', bg='#444', relief=tk.FLAT) # New
        self.scale_labelFrame.grid(column=0, row=1, padx=20)
        self.scale_innerBox = tk.Frame(self.scale_labelFrame, width=250, height=50)
        self.scale_innerBox.grid(column=0, row=0)
        self.scale_innerBox.grid_propagate(0)
        self.scale_innerBox.grid_anchor(tk.CENTER)
        
        self.bpm_scale = tk.DoubleVar()
        self.bpm_scale.set(100)
        self.bpm_scalebar = tk.Scale(self.scale_innerBox,
                                    variable=self.bpm_scale,  #変数
                                    from_=10,           #下限値
                                    to=200,             #上限値
                                    resolution=2,       #増減ステップ
                                    orient=tk.HORIZONTAL,
                                    length=150,
                                    command=self.reset_bpm)
        self.bpm_scalebar.grid(column=0,row=0, padx=100, pady=20)
        
        #=== ウィジット:メトロノームウィジェット
        self.metronom_labelFrame = tk.LabelFrame(self.option_frame, text="Metronom", fg='white', bg='#444', relief=tk.FLAT)
        self.metronom_labelFrame.grid(column=0, row=2, padx=20)
        self.metronom_innerBox = tk.Frame(self.metronom_labelFrame, width=250, height=70)
        self.metronom_innerBox.grid(column=0, row=0)
        self.metronom_innerBox.grid_propagate(0)
        self.metronom_innerBox.grid_anchor(tk.CENTER)

        
        #=== ウィジェット:Tableウィジェット
        self.table_labelFrame = tk.LabelFrame(self.option_frame, text="Table", fg='white', bg='#444', relief=tk.FLAT) # New
        self.table_labelFrame.grid(column=1, row=0, padx=20, rowspan=3) # rowspan
        self.table_innerBox = tk.Frame(self.table_labelFrame, width=250, height=250)
        self.table_innerBox.grid(column=0, row=0)
        self.table_innerBox.grid_propagate(0)
        self.table_innerBox.grid_anchor(tk.CENTER)
        
        self.table_c4 = tk.Label(self.table_innerBox, text = 'a : ド/C4', font=("メイリオ", "16")).grid(column=0, row=0, pady=1, sticky=tk.W)
        self.table_d4 = tk.Label(self.table_innerBox, text = 's : レ/D4', font=("メイリオ", "16")).grid(column=0, row=1, pady=1, sticky=tk.W)
        self.table_e4 = tk.Label(self.table_innerBox, text = 'd : ミ/E4', font=("メイリオ", "16")).grid(column=0, row=2, pady=1, sticky=tk.W)
        self.table_f4 = tk.Label(self.table_innerBox, text = 'f : ファ/F4', font=("メイリオ", "16")).grid(column=0, row=3, pady=1, sticky=tk.W)
        self.table_g4 = tk.Label(self.table_innerBox, text = 'w : ソ/G4', font=("メイリオ", "16")).grid(column=0, row=4, pady=1, sticky=tk.W)
        self.table_a4 = tk.Label(self.table_innerBox, text = 'e : ラ/A4', font=("メイリオ", "16")).grid(column=0, row=5, pady=1, sticky=tk.W)
        self.table_b4 = tk.Label(self.table_innerBox, text = 'r : シ/B4', font=("メイリオ", "16")).grid(column=0, row=6, pady=1, sticky=tk.W)
        self.table_c5 = tk.Label(self.table_innerBox, text = 'A : ド/C5', font=("メイリオ", "16")).grid(column=0, row=7, pady=1, sticky=tk.W)

    def reset_bpm(self, bpm):
        self.bpm = self.bpm_scale.get() # New
        self.DURATION = {            
            'L1': (60 / self.bpm * 4),      # 全音符
            'L2': (60 / self.bpm * 4) / 2,  # 二分音符
            'L4': (60 / self.bpm * 4) / 4,  # 四分音符
            'L8': (60 / self.bpm * 4) / 8,  # 八分音符
        }
        
    # sin波の作成
    def create_sinwave(self, duration, freq):
        self.duration = duration
        self.freq = freq
        
        # 指定周波数のsin波を指定秒数生成
        self.t = np.arange(0, duration * self.rate) / self.rate
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)

    def press_key(self, event):
        global freq # 初期値として、create_sinwave(FREQ_SCALE['ド/C4'])のために必要

        self.key = event.keysym
        if self.key == 'j':
            self.duration = self.DURATION['L8']
        elif self.key == 'k':
            self.duration = self.DURATION['L4']
        elif self.key == 'l':
            self.duration = self.DURATION['L2']
        elif self.key == 'semicolon':
            self.duration = self.DURATION['L1']

        if self.key == 'a':
            freq = FREQ_SCALE['ド/C4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 's':
            freq = FREQ_SCALE['レ/D4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'd':
            freq = FREQ_SCALE['ミ/E4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'f':
            freq = FREQ_SCALE['ファ/F4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'w':
            freq = FREQ_SCALE['ソ/G4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'e':
            freq = FREQ_SCALE['ラ/A4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'r':
            freq = FREQ_SCALE['シ/B4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'A':
            freq = FREQ_SCALE['ド/C5']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 's':
            freq = FREQ_SCALE['レ/D4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()

        self.buffer.set(self.key) # New
        self.line_wave.set_ydata(self.sin_t[:500])      # グラフのy軸をアップデート
        self.canvas.draw()                    # グラフのアップデート

    # ストリームに渡して再生
    def play(self):
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)
        self.stream.write(self.sin_t.astype(np.float32).tobytes())
        self.stream.close()


# パラメータ
FREQ_SCALE = {  # 周波数f0のスケール
    'ド/C4': 261.626,
    'レ/D4': 293.665,
    'ミ/E4': 329.628,
    'ファ/F4': 349.228,
    'ファ#/F#4': 369.994,
    'ソ/G4': 391.995,
    'ソ#/G#4': 415.305,
    'ラ/A4': 440.000,
    'ラ#/A#4': 466.164,
    'シ/B4': 493.883,
    'ド/C5': 523.251,
    'レ/D5': 587.330,
    'ミ/E5': 659.255,
}

root = tk.Tk()
app = Synth_application(master=root)
app.mainloop()

Tkinterを使う前のpygameを使っていたときは、滑らかに音が出力されていました。

ライブラリを変えた影響?画面を作った影響?とも考えましたが、音声出力部分のpyaudioに関する記載を見直しました。

○結果

下記のコードで滑らかに音が鳴るようになりました。

コードはこちら

# ライブラリのインポート
import tkinter as tk
import numpy as np  # sin波
import pyaudio  # メモリ上の音楽を再生
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

#=== アプリケーションの定義
class Synth_application(tk.Frame):
    #=== 初期設定
    def __init__(self, master=None):
        #=== メインウィンドウの設定
        super().__init__(master) # 親クラス(Application)の __init__ メソッドを実行
        self.master = master
        self.master.title('Synth_Application')
        self.master.resizable(width=False,height=False) #ウィンドウ幅の固定

        #=== オプション:メインウィンドウを画面ん中央に表示させる
        winWidth = 700 # メインウィンドウの幅を定義
        winHeight = 570 # メインウィンドウの高さを定義
        screenWidth = self.master.winfo_screenwidth() # 使用しているモニターの横幅を取得
        screenHeight = self.master.winfo_screenheight() # 使用しているモニターの縦幅を取得
        startX = int((screenWidth / 2) - (winWidth / 2))
        startY = int((screenHeight / 2) - (winHeight / 2))
        self.master.geometry('{}x{}+{}+{}'.format(winWidth, winHeight, startX, startY))  # f-strings 700px × 570pxの画面が左上(縦Xpx、横Ypx)

        #=== sin波の初期値設定 → def start_up
        self.start_up() # 初期値
        
        #=== ウィジットをつくる → def create_widgets
        self.create_widgets()
        
        #=== pyaudioの開始             
        self.p = pyaudio.PyAudio() 
        #==== 移動 ===#
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)

        #==== 追加:終了したときの処理 → def quite_app===#
        self.master.protocol("WM_DELETE_WINDOW", self.quit_app)

        
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
        global freq # 初期値のためにglobal変数が必要

        freq = 261.26
        self.DURATION = {
            'L1': (60 / 100 * 4),      # 全音符
            'L2': (60 / 100 * 4) / 2,  # 二分音符
            'L4': (60 / 100 * 4) / 4,  # 四分音符
            'L8': (60 / 100 * 4) / 8,  # 八分音符
        }
        self.duration = self.DURATION['L4']
        self.bpm = 100 # New
        
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い

        self.start_pos = 0
        self.end_pos = self.start_pos + self.duration * self.rate
        
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(self.duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)
        
        
    #=== ウィジットの定義
    def create_widgets(self):
        #=== フレームをつくる
        self.canvas_frame = tk.Frame(self.master, width=700, height=250, bg="#000080")
        self.canvas_frame.grid(column=0, row=0) # 1行1列に配置
        self.canvas_frame.grid_propagate(0) # フレーム幅の固定
        self.canvas_frame.grid_anchor(tk.CENTER) # gridを中央に表示するには、grid()で配置する要素の親要素にgrid_anchor(tkinter.CENTER)を使う

        self.option_frame = tk.Frame(self.master, width=700, height=320, bd = 5, relief = tk.GROOVE) # New
        self.option_frame.grid(column=0, row=1)
        self.option_frame.grid_propagate(0)
        self.option_frame.grid_anchor(tk.CENTER)
        
        #=== ウィジェット:LabelFrameウィジットをつくる
        self.chunk_labelFrame = tk.LabelFrame(self.canvas_frame, text="WaveForm", fg='white', bg='#444', relief=tk.FLAT)
        self.chunk_labelFrame.grid(column=0, row=0)
        self.chunk_innerBox = tk.Frame(self.chunk_labelFrame)
        self.chunk_innerBox.grid(column=0, row=0)

        #=== Figureインスタンスをつくる
        fig = Figure(figsize=(6.5,2), dpi=100, tight_layout=True, facecolor='#F0F0F0') # 650 x 200のFigureインスタンスを作成, tight_layoutでオブジェクトの配置を自動調整

        axes_envRange = fig.add_subplot(1, 2, 1)  # 1行2列の最初(1,1)に表示
        axes_envRange.set_title('envRange')
        axes_envRange.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_envRange.set_ylabel('envRange', fontsize=8) # ラベルのオプション

        axes_wave = fig.add_subplot(1, 2, 2)  # 1行2列の2番目(1,2)に表示
        axes_wave.set_title('Sin_wave')
        axes_wave.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylabel('sin_t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylim([-(self.gain + 0.05), (self.gain + 0.05)])

        self.line_envRange, = axes_envRange.plot(self.t, self.envRange)
        self.line_wave, = axes_wave.plot(self.sin_t[:500])       
        
        #=== ウィジェット:FigureCanvasTkAggを宣言し、LabelFrameウィジェット上に配置する
        self.canvas = FigureCanvasTkAgg(fig, master=self.chunk_innerBox)
        self.canvas.get_tk_widget().pack(padx=10, pady=10)
        self.canvas.get_tk_widget().bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        
        #=== ウィジェット:キープレスウィジェット
        self.keypress_labelFrame = tk.LabelFrame(self.option_frame, text="KeyPress", fg='white', bg='#444', relief=tk.FLAT) # New
        self.keypress_labelFrame.grid(column=0, row=0, padx=20 ,sticky=tk.N)
        self.keypress_innerBox = tk.Frame(self.keypress_labelFrame, width=250, height=60)
        self.keypress_innerBox.grid(column=0, row=0)
        self.keypress_innerBox.grid_propagate(0)
        self.keypress_innerBox.grid_anchor(tk.CENTER)      
        
        self.keypress_label = tk.Label(self.keypress_innerBox, text="push key is : ", font=("メイリオ", "16"))
        self.keypress_label.grid() 

        self.buffer = tk.StringVar()
        self.buffer.set('xxx')          
        self.press_key_label = tk.Label(self.keypress_innerBox, textvariable = self.buffer, font=("メイリオ", "16"))
        self.press_key_label.grid() 
        self.press_key_label.bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        self.press_key_label.focus_set()
        
        #=== ウィジェット:BPMスケールバーウィジェット
        self.scale_labelFrame = tk.LabelFrame(self.option_frame, text="BPM", fg='white', bg='#444', relief=tk.FLAT) # New
        self.scale_labelFrame.grid(column=0, row=1, padx=20)
        self.scale_innerBox = tk.Frame(self.scale_labelFrame, width=250, height=50)
        self.scale_innerBox.grid(column=0, row=0)
        self.scale_innerBox.grid_propagate(0)
        self.scale_innerBox.grid_anchor(tk.CENTER)
        
        self.bpm_scale = tk.DoubleVar()
        self.bpm_scale.set(100)
        self.bpm_scalebar = tk.Scale(self.scale_innerBox,
                                    variable=self.bpm_scale,  #変数
                                    from_=10,           #下限値
                                    to=200,             #上限値
                                    resolution=2,       #増減ステップ
                                    orient=tk.HORIZONTAL,
                                    length=150,
                                    command=self.reset_bpm)
        self.bpm_scalebar.grid(column=0,row=0, padx=100, pady=20)
        
                
        #=== ウィジット:メトロノームウィジェット
        self.metronom_labelFrame = tk.LabelFrame(self.option_frame, text="Metronom", fg='white', bg='#444', relief=tk.FLAT)
        self.metronom_labelFrame.grid(column=0, row=2, padx=20)
        self.metronom_innerBox = tk.Frame(self.metronom_labelFrame, width=250, height=70)
        self.metronom_innerBox.grid(column=0, row=0)
        self.metronom_innerBox.grid_propagate(0)
        self.metronom_innerBox.grid_anchor(tk.CENTER)
        
        #=== ウィジェット:Tableウィジェット
        self.table_labelFrame = tk.LabelFrame(self.option_frame, text="Table", fg='white', bg='#444', relief=tk.FLAT) # New
        self.table_labelFrame.grid(column=1, row=0, padx=20, rowspan=3) # rowspan
        self.table_innerBox = tk.Frame(self.table_labelFrame, width=250, height=250)
        self.table_innerBox.grid(column=0, row=0)
        self.table_innerBox.grid_propagate(0)
        self.table_innerBox.grid_anchor(tk.CENTER)
        
        self.table_c4 = tk.Label(self.table_innerBox, text = 'a : ド/C4', font=("メイリオ", "16")).grid(column=0, row=0, pady=1, sticky=tk.W)
        self.table_d4 = tk.Label(self.table_innerBox, text = 's : レ/D4', font=("メイリオ", "16")).grid(column=0, row=1, pady=1, sticky=tk.W)
        self.table_e4 = tk.Label(self.table_innerBox, text = 'd : ミ/E4', font=("メイリオ", "16")).grid(column=0, row=2, pady=1, sticky=tk.W)
        self.table_f4 = tk.Label(self.table_innerBox, text = 'f : ファ/F4', font=("メイリオ", "16")).grid(column=0, row=3, pady=1, sticky=tk.W)
        self.table_g4 = tk.Label(self.table_innerBox, text = 'w : ソ/G4', font=("メイリオ", "16")).grid(column=0, row=4, pady=1, sticky=tk.W)
        self.table_a4 = tk.Label(self.table_innerBox, text = 'e : ラ/A4', font=("メイリオ", "16")).grid(column=0, row=5, pady=1, sticky=tk.W)
        self.table_b4 = tk.Label(self.table_innerBox, text = 'r : シ/B4', font=("メイリオ", "16")).grid(column=0, row=6, pady=1, sticky=tk.W)
        self.table_c5 = tk.Label(self.table_innerBox, text = 'A : ド/C5', font=("メイリオ", "16")).grid(column=0, row=7, pady=1, sticky=tk.W)
        self.table_d5 = tk.Label(self.table_innerBox, text = 'S : レ/D5', font=("メイリオ", "16")).grid(column=0, row=8, pady=1, sticky=tk.W)
        self.table_e5 = tk.Label(self.table_innerBox, text = 'D : ミ/E5', font=("メイリオ", "16")).grid(column=0, row=9, pady=1, sticky=tk.W)

    def reset_bpm(self, bpm):
        self.bpm = self.bpm_scale.get() # New
        self.DURATION = {            
            'L1': (60 / self.bpm * 4),      # 全音符
            'L2': (60 / self.bpm * 4) / 2,  # 二分音符
            'L4': (60 / self.bpm * 4) / 4,  # 四分音符
            'L8': (60 / self.bpm * 4) / 8,  # 八分音符
        }
        
    # sin波の作成
    def create_sinwave(self, duration, freq):
        self.duration = duration
        self.freq = freq
        
        # 指定周波数のsin波を指定秒数生成  
        self.start_pos = self.end_pos # New
        self.end_pos = self.start_pos + duration * self.rate # New
#        print("start:", self.start_pos)
#        print("end:", self.end_pos)
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate # New
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)

    def press_key(self, event):
        global freq # 初期値として、create_sinwave(FREQ_SCALE['ド/C4'])のために必要

        self.key = event.keysym
        if self.key == 'j':
            self.duration = self.DURATION['L8']
        elif self.key == 'k':
            self.duration = self.DURATION['L4']
        elif self.key == 'l':
            self.duration = self.DURATION['L2']
        elif self.key == 'semicolon':
            self.duration = self.DURATION['L1']

        if self.key == 'a':
            freq = FREQ_SCALE['ド/C4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 's':
            freq = FREQ_SCALE['レ/D4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'd':
            freq = FREQ_SCALE['ミ/E4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'f':
            freq = FREQ_SCALE['ファ/F4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'w':
            freq = FREQ_SCALE['ソ/G4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'e':
            freq = FREQ_SCALE['ラ/A4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'r':
            freq = FREQ_SCALE['シ/B4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'A':
            freq = FREQ_SCALE['ド/C5']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'S':
            freq = FREQ_SCALE['レ/D5']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'D':
            freq = FREQ_SCALE['ミ/E5']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()

        self.buffer.set(self.key) # New
        self.reset_sinwave()

    # ストリームに渡して再生
    def play(self):
        self.stream.write(self.sin_t.astype(np.float32).tobytes())

    #==== 追加 ===#
    def reset_sinwave(self):
        self.line_wave.set_ydata(self.sin_t[:500])      # グラフのy軸をアップデート
        self.canvas.draw()      

    #==== 追加 ===#
    # 終了ボタンが押された時の処理
    def quit_app(self):
        self.stream.close()
        self.master.destroy()

# パラメータ
FREQ_SCALE = {  # 周波数f0のスケール
    'ド/C4': 261.626,
    'レ/D4': 293.665,
    'ミ/E4': 329.628,
    'ファ/F4': 349.228,
    'ファ#/F#4': 369.994,
    'ソ/G4': 391.995,
    'ソ#/G#4': 415.305,
    'ラ/A4': 440.000,
    'ラ#/A#4': 466.164,
    'シ/B4': 493.883,
    'ド/C5': 523.251,
    'レ/D5': 587.330,
    'ミ/E5': 659.255,
}

root = tk.Tk()
app = Synth_application(master=root)
app.mainloop()

以下、メモです。

音声を滑らかに出力させるための改善として、以下二点実施しました。詳しくは「課題と解決方法」に記載します。

  • pyaudioの見直し
  • サイン波の開始値および終了値の見直し

○課題と解決方法

pyaudioの見直し

1音1音途切れてしまう原因は、play()メソッドにありました。

以前の記事でpyaudioで音声を出力するためには、以下のプロセスが必要と紹介しました。

①PyAudioインスタンスを作成
②ストリームを開く
③ストリームにデータを書き込み&再生する
④ストリームを閉じる&インスタンスを破棄する

ここで、変更前のコードではplay()メソッドで②〜④を定義していました。
そのため、毎回ストリームを閉じてからストリームを開いているため、1音1音途切れるとわかりました。

よって、アプリ起動中はストリームを開いたままに変更したところ改善しました。

# 変更前
    def play(self):
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)
        self.stream.write(self.sin_t.astype(np.float32).tobytes())
        self.stream.close()


# 変更後
    def __init__(self, master=None):
…       
        #=== pyaudioの開始             
        self.p = pyaudio.PyAudio() 
        #==== 移動 ===#
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)

        #==== 追加:終了したときの処理 → def quite_app===#
        self.master.protocol("WM_DELETE_WINDOW", self.quit_app)
…
    def play(self):
        self.stream.write(self.sin_t.astype(np.float32).tobytes())
…
    def quit_app(self):
        self.stream.close()
        self.master.destroy()
サイン波の開始値および終了値の見直し

pyaudioのストリームを開きっぱなしにしたころで、ラグは少なくなりましたが、よーく聴いてみるとプツプツと音が途切れています。

特にわかりやすいのが、同じキーを連続して出力したときです。PCのキーを押しっぱなしにすると、キーを連打しているのと同じ状態になりますが、このとき、プツプツ音が途切れていることが分かります。

この原因はsin波の式にありました。

# 元の式
        self.t = np.arange(0, self.duration * self.rate) / self.rate
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)

このとき、pyaudioで書き込まれるデータself.sin_tは毎回、開始値sin(0)、終了値sin(self.duration * self.rate)で生成されますが、データの最後はdurationによってまちまちとなるので、連続で音を鳴らそうとしても波が不連続に結合され、再生するとプツプツ音がするということです。

よって、 波形データの開始値と終了値を定義し、波が連続して結合されるようにする必要があります。 

  • 引数でデータの開始位置(start_pos)を渡す
  • 戻り値としてデータの終わりの位置(end_pos)を戻す

ちなみに参考にした記事はこちらです。

(参考)pythonで波形の生成と再生をリアルタイムに行いたい – Teratail

ここからは実験を交えて確かめてみたいと思います。

まず、一番単純なドの波を(duration*rate)秒生成した場合、これが今の状況です。

コードはこちら
 

# ライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt

# ドの条件
rate = 44100
duration = 0.05
freq = 261.626

# 波の式
t = np.arange(0, duration * rate) / rate
sin_t = np.sin(2 * np.pi * freq * t)

# グラフのプロット
fig = plt.figure()
fig_wave = fig.add_subplot(2, 1, 2)
fig_wave.plot(t, sin_t)

次に、2回音が鳴るようにコードを追加し、2回めは開始値と終了値を(duration*rate)秒ずらしました。
結果、滑らかに音が鳴り、作られた2つのグラフを見ても、綺麗に結合できることがわかります。

コードはこちら
 

# ライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt
import pyaudio

# ドの条件
rate = 44100
duration = 0.05 
freq = 261.626

# PyAudioを開始
p = pyaudio.PyAudio()

# PyAudioを使うには、ストリームをopenし、writeし、close
# ストリームを開く
stream = p.open(format=pyaudio.paFloat32,
                channels=1,
                rate=rate,
                frames_per_buffer=1024,
                output=True)

# 波の式①(start:0, end:duration*rate)
t = np.arange(0, duration * rate) / rate
sin_t = np.sin(2 * np.pi * freq * t)

fig = plt.figure()
fig_wave = fig.add_subplot(2, 1, 1)
fig_wave.plot(t, sin_t)

stream.write(sin_t.astype(np.float32).tobytes())

# 波の式②(start:duration*rate, end:2*duration*rate)
t = np.arange(duration * rate, 2 * duration * rate) / rate
sin_t = np.sin(2 * np.pi * freq * t)

fig = plt.figure()
fig_wave2 = fig.add_subplot(2, 1, 2) 
fig_wave2.plot(t, sin_t)

stream.write(sin_t.astype(np.float32).tobytes())

これを関数で定義したのが下記の部分です。

# 変更前
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
…
        self.t = np.arange(0, duration * self.rate) / self.rate
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)
…
    def create_sinwave(self, duration, freq):
…
        self.t = np.arange(0, duration * self.rate) / self.rate
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)


# 変更後
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
…
        self.start_pos = 0
        self.end_pos = self.start_pos + self.duration * self.rate
        
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(self.duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)


    def create_sinwave(self, duration, freq):
…  
        self.start_pos = self.end_pos
        self.end_pos = self.start_pos + duration * self.rate

        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)

レシピ②:スレッドを用いてメトロノーム機能をつける

次にメトロノーム機能について考えました。

以前の記事で紹介したとおり、テンポをを知る方法は以下の2通りが考えられました。

  • 耳で知る方法
  • 目で知る方法

メトロノームのイメージは、テンポに合わせて「カチッカチッ」と音が鳴るものです。

ただ、PC内のスピーカーが1つであることから実装が難しいと考え、目で知る方法に切り替えました。

そして、最もシンプルな方法として、タイマーでの実装を考えました。
具体的には、テンポ(BPM)に応じて、画面上の数字が「1→2→3→4→1→2…」と繰り返されるイメージです。

これを実装するうえで、重大なキーワードがあります。
それは、 ”並行処理(マルチスレッド)”です。 

○マルチスレッドの基本

プログラミングの処理は基本的にコードを上から順に実行するものです。

Tkinterではメインループというものを定義し、何かしらのイベントがあるとイベントに応じた処理が実行され、また始めに戻るというものです。

メインループの構造上、キー入力に対する処理(音声を出力してからグラフをプロットする)とタイマーのカウント処理を同時に実行することはできません

じゃあ、メトロノーム機能つくるの無理じゃん。。。

そう思いましたが、Pythonでこの同時処理の実装を可能にするのが”マルチスレッド”という概念です。

並行処理と並列処理

コンピュータの処理には、並行処理と並列処理という似た言葉があります。

  • 並行処理(マルチスレッド):複数の処理を独立に実行できる構成のこと
  • 並列処理(マルチプロセッシング):複数の処理を実際に同時に実行すること

ここでは詳細については割愛します。
詳しくは(参考)Pythonの並行処理を理解したい [マルチスレッド編] – Zennを見てみてください。

今回のように、 メインループを実行しながらイベントに応じてタイマーのカウント処理を実行する場合は、並行処理(マルチスレッド)が該当する ようです。

マルチスレッドの概念

『スレッド』というのは CPU(PCの頭脳)が実行する処理を指します。CPUの中には”コア”と呼ばれる処理作業を行う中核が存在し、PCによって1つのCPUに対し複数コアを要しています。

シングルスレッドの場合、CPUの中の1つのコアを用いて処理を実行しています。通常のメインループも1スレッドと考えられますので、複数処理ができません。

マルチスレッドの場合、 コアごとにスレッドを用意することで並行処理が実現できる ようになります。スレッド1でメインループを回し、スレッド2で別のイベント処理を実行する、といったイメージです。

こちらも詳しくは(参考)【Python/tkinter】tkinterでマルチスレッドを利用する – だえう …が分かりやすかったです。

マルチスレッドの実装方法

マルチスレッドの実装には、threadingというpython標準のライブラリが必要になります。

ライブラリをインポートしたあと、スレッドを作成します。このとき、引数 target にはそのスレッドで実行する「関数」や「メソッド」を指定します。そして、スレッドを開始することでスレッドがスタンバイ状態になります。

# ライブラリのインポート
import threading

# スレッドの作成
thread1 = threading.Thread(target=func)

# スレッドの開始
thread1.start()

実際に、スレッドで実行するタイマーを作ってみました。

イメージとしては、メインループ側で今までどおりキープレスイベント時にグラフのプロットや音声出力を行い、ボタンウィジェットを押したタイミングでスレッド側のタイマーがカウント処理を行うといったものです。

スレッドの処理を実行させるためには、メインループとスレッドの間で何かしらのやり取りが必要です。このスレッド間の通知用にstart_flagを用意し、メインループ側でイベントが発生した場合にstart_flagが変化させ、スレッド側の処理を実行するようにしました。

参考サイトを真似てみたところ、スレッドを用いてタイマーが動くことを確認しました。

コードはこちら
 

# -*- coding:utf-8 -*-
import tkinter
import threading

start_flag = False
quitting_flag = False
count = 0

# タイマー
def timer():
    global label
    global start_flag
    global quitting_flag
    global count

    while not quitting_flag:
        if start_flag:
            label.config(text=count)
            count += 1
            if count > 4:
                count = 1

            import time
            time.sleep(0.5)

# スタートボタンが押された時の処理
def start_button_click(event):
    global start_flag
    global count

    count = 0
    start_flag = True

# ストップボタンが押された時の処理
def stop_button_click(event):
    global start_flag
    start_flag = False

# 終了ボタンが押された時の処理
def quit_app():
    global quitting_flag
    global app
    global thread1

    quitting_flag = True

    # thread1終了まで待つ
    thread1.join()

    # thread1終了後にアプリ終了
    app.destroy()

# メインウィンドウを作成
app = tkinter.Tk()
app.geometry("200x100")

# ボタンの作成と配置
start_button = tkinter.Button(
    app,
    text="スタート",
)
start_button.pack()

stop_button = tkinter.Button(
    app,
    text="ストップ",
)
stop_button.pack()


# ラベルの作成と配置
label = tkinter.Label(
    app,
    width=5,
    height=1,
    text=0,
    font=("", 20)
)
label.pack()

# イベント処理の設定
start_button.bind("<ButtonPress>", start_button_click)
stop_button.bind("<ButtonPress>", stop_button_click)
app.protocol("WM_DELETE_WINDOW", quit_app)

# スレッドの生成と開始
thread1 = threading.Thread(target=timer)
thread1.start()

# メインループ
app.mainloop()

○結果

以下のコードで、メトロノーム機能をタイマーウィジェットにて実現しました。

ただし、音を鳴らさない状態でと問題なくタイマーが動いていますが、キーを押して音を鳴らすとラグがすごいです。。。

コードはこちら

# ライブラリのインポート
import tkinter as tk
import numpy as np  # sin波
import pyaudio  # メモリ上の音楽を再生
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import threading


#=== アプリケーションの定義
class Synth_application(tk.Frame):
    #=== 初期設定
    def __init__(self, master=None):
        #=== メインウィンドウの設定
        super().__init__(master) # 親クラス(Application)の __init__ メソッドを実行
        self.master = master
        self.master.title('Synth_Application')
        self.master.resizable(width=False,height=False) #ウィンドウ幅の固定

        #=== オプション:メインウィンドウを画面ん中央に表示させる
        winWidth = 700 # メインウィンドウの幅を定義
        winHeight = 570 # メインウィンドウの高さを定義
        screenWidth = self.master.winfo_screenwidth() # 使用しているモニターの横幅を取得
        screenHeight = self.master.winfo_screenheight() # 使用しているモニターの縦幅を取得
        startX = int((screenWidth / 2) - (winWidth / 2))
        startY = int((screenHeight / 2) - (winHeight / 2))
        self.master.geometry('{}x{}+{}+{}'.format(winWidth, winHeight, startX, startY))  # f-strings 700px × 570pxの画面が左上(縦Xpx、横Ypx)

        #=== sin波の初期値設定 → def start_up
        self.start_up() # 初期値
        
        #=== ウィジットをつくる → def create_widgets
        self.create_widgets()
        
        #=== pyaudioの開始             
        self.p = pyaudio.PyAudio() 
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)

        #==== 追加:終了したときの処理 → def quite_app
        self.master.protocol("WM_DELETE_WINDOW", self.quit_app)

        #==== スレッドの生成と開始 New
        self.thread1 = threading.Thread(target=self.timer)
        self.thread1.start()
        
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
        global freq # 初期値のためにglobal変数が必要

        freq = 261.26
        self.DURATION = {
            'L1': (60 / 100 * 4),      # 全音符
            'L2': (60 / 100 * 4) / 2,  # 二分音符
            'L4': (60 / 100 * 4) / 4,  # 四分音符
            'L8': (60 / 100 * 4) / 8,  # 八分音符
        }
        self.duration = self.DURATION['L4']
        self.bpm = 100 # New
        
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い

        self.start_pos = 0
        self.end_pos = self.start_pos + self.duration * self.rate
        
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(self.duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)
        
        self.start_flag = False
        self.quitting_flag = False
        self.count = 0
        
        
    #=== ウィジットの定義
    def create_widgets(self):
        #=== フレームをつくる
        self.canvas_frame = tk.Frame(self.master, width=700, height=250, bg="#000080")
        self.canvas_frame.grid(column=0, row=0) # 1行1列に配置
        self.canvas_frame.grid_propagate(0) # フレーム幅の固定
        self.canvas_frame.grid_anchor(tk.CENTER) # gridを中央に表示するには、grid()で配置する要素の親要素にgrid_anchor(tkinter.CENTER)を使う

        self.option_frame = tk.Frame(self.master, width=700, height=320, bd = 5, relief = tk.GROOVE) # New
        self.option_frame.grid(column=0, row=1)
        self.option_frame.grid_propagate(0)
        self.option_frame.grid_anchor(tk.CENTER)
        
        #=== ウィジェット:LabelFrameウィジットをつくる
        self.chunk_labelFrame = tk.LabelFrame(self.canvas_frame, text="WaveForm", fg='white', bg='#444', relief=tk.FLAT)
        self.chunk_labelFrame.grid(column=0, row=0)
        self.chunk_innerBox = tk.Frame(self.chunk_labelFrame)
        self.chunk_innerBox.grid(column=0, row=0)

        #=== Figureインスタンスをつくる
        fig = Figure(figsize=(6.5,2), dpi=100, tight_layout=True, facecolor='#F0F0F0') ## 650 x 200のFigureインスタンスを作成, tight_layoutでオブジェクトの配置を自動調整

        axes_envRange = fig.add_subplot(1, 2, 1)  # 1行2列の最初(1,1)に表示
        axes_envRange.set_title('envRange')
        axes_envRange.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_envRange.set_ylabel('envRange', fontsize=8) # ラベルのオプション

        axes_wave = fig.add_subplot(1, 2, 2)  # 1行2列の2番目(1,2)に表示
        axes_wave.set_title('Sin_wave')
        axes_wave.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylabel('sin_t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylim([-(self.gain + 0.05), (self.gain + 0.05)])

        self.line_envRange, = axes_envRange.plot(self.t, self.envRange)
        self.line_wave, = axes_wave.plot(self.sin_t[:500])       
        
        #=== ウィジェット:FigureCanvasTkAggを宣言し、LabelFrameウィジェット上に配置する
        self.canvas = FigureCanvasTkAgg(fig, master=self.chunk_innerBox)
        self.canvas.get_tk_widget().pack(padx=10, pady=10)
        self.canvas.get_tk_widget().bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        
        #=== ウィジェット:キープレスウィジェット
        self.keypress_labelFrame = tk.LabelFrame(self.option_frame, text="KeyPress", fg='white', bg='#444', relief=tk.FLAT) # New
        self.keypress_labelFrame.grid(column=0, row=0, padx=20 ,sticky=tk.N)
        self.keypress_innerBox = tk.Frame(self.keypress_labelFrame, width=250, height=60)
        self.keypress_innerBox.grid(column=0, row=0)
        self.keypress_innerBox.grid_propagate(0)
        self.keypress_innerBox.grid_anchor(tk.CENTER)      
        
        self.keypress_label = tk.Label(self.keypress_innerBox, text="push key is : ", font=("メイリオ", "16"))
        self.keypress_label.grid() 

        self.buffer = tk.StringVar()
        self.buffer.set('xxx')          
        self.press_key_label = tk.Label(self.keypress_innerBox, textvariable = self.buffer, font=("メイリオ", "16"))
        self.press_key_label.grid() 
        self.press_key_label.bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        self.press_key_label.focus_set()
        
        #=== ウィジェット:BPMスケールバーウィジェット
        self.scale_labelFrame = tk.LabelFrame(self.option_frame, text="BPM", fg='white', bg='#444', relief=tk.FLAT) # New
        self.scale_labelFrame.grid(column=0, row=1, padx=20, sticky=tk.N)
        self.scale_innerBox = tk.Frame(self.scale_labelFrame, width=250, height=50)
        self.scale_innerBox.grid(column=0, row=0)
        self.scale_innerBox.grid_propagate(0)
        self.scale_innerBox.grid_anchor(tk.CENTER)
        
        self.bpm_scale = tk.DoubleVar()
        self.bpm_scale.set(100)
        self.bpm_scalebar = tk.Scale(self.scale_innerBox,
                                    variable=self.bpm_scale,  #変数
                                    from_=10,           #下限値
                                    to=200,             #上限値
                                    resolution=2,       #増減ステップ
                                    orient=tk.HORIZONTAL,
                                    length=150,
                                    command=self.reset_bpm)
        self.bpm_scalebar.grid(column=0,row=0, padx=100, pady=20)
        
        #=== ウィジット:メトロノームウィジェット New
        self.metronom_labelFrame = tk.LabelFrame(self.option_frame, text="Metronom", fg='white', bg='#444', relief=tk.FLAT)
        self.metronom_labelFrame.grid(column=0, row=2, padx=20)
        self.metronom_innerBox = tk.Frame(self.metronom_labelFrame, width=250, height=70)
        self.metronom_innerBox.grid(column=0, row=0)
        self.metronom_innerBox.grid_propagate(0)
        self.metronom_innerBox.grid_anchor(tk.CENTER)
        
        self.start_button = tk.Button(self.metronom_innerBox, text="start")
        self.start_button.grid(column=0, row=0)
        self.stop_button = tk.Button(self.metronom_innerBox, text="stop")
        self.stop_button.grid(column=0, row=1)
        self.metronom_label = tk.Label(self.metronom_innerBox, font=("", 20))
        self.metronom_label.grid(column=1, row=0, padx=20, rowspan=2)
        
        self.start_button.bind("<ButtonPress>", self.start_button_click)
        self.stop_button.bind("<ButtonPress>", self.stop_button_click)
        
        #=== ウィジェット:Tableウィジェット
        self.table_labelFrame = tk.LabelFrame(self.option_frame, text="Table", fg='white', bg='#444', relief=tk.FLAT) # New
        self.table_labelFrame.grid(column=1, row=0, padx=20, rowspan=3) # rowspan
        self.table_innerBox = tk.Frame(self.table_labelFrame, width=250, height=250)
        self.table_innerBox.grid(column=0, row=0)
        self.table_innerBox.grid_propagate(0)
        self.table_innerBox.grid_anchor(tk.CENTER)
        
        self.table_c4 = tk.Label(self.table_innerBox, text = 'a : ド/C4', font=("メイリオ", 16)).grid(column=0, row=0, pady=1, sticky=tk.W)
        self.table_d4 = tk.Label(self.table_innerBox, text = 's : レ/D4', font=("メイリオ", 16)).grid(column=0, row=1, pady=1, sticky=tk.W)
        self.table_e4 = tk.Label(self.table_innerBox, text = 'd : ミ/E4', font=("メイリオ", 16)).grid(column=0, row=2, pady=1, sticky=tk.W)
        self.table_f4 = tk.Label(self.table_innerBox, text = 'f : ファ/F4', font=("メイリオ", 16)).grid(column=0, row=3, pady=1, sticky=tk.W)
        self.table_g4 = tk.Label(self.table_innerBox, text = 'w : ソ/G4', font=("メイリオ", 16)).grid(column=0, row=4, pady=1, sticky=tk.W)
        self.table_a4 = tk.Label(self.table_innerBox, text = 'e : ラ/A4', font=("メイリオ", 16)).grid(column=0, row=5, pady=1, sticky=tk.W)
        self.table_b4 = tk.Label(self.table_innerBox, text = 'r : シ/B4', font=("メイリオ", 16)).grid(column=0, row=6, pady=1, sticky=tk.W)
        self.table_c5 = tk.Label(self.table_innerBox, text = 'A : ド/C5', font=("メイリオ", 16)).grid(column=0, row=7, pady=1, sticky=tk.W)

    def reset_bpm(self, bpm):
        self.bpm = self.bpm_scale.get() # New
        self.DURATION = {            
            'L1': (60 / self.bpm * 4),      # 全音符
            'L2': (60 / self.bpm * 4) / 2,  # 二分音符
            'L4': (60 / self.bpm * 4) / 4,  # 四分音符
            'L8': (60 / self.bpm * 4) / 8,  # 八分音符
        }
        
    # sin波の作成
    def create_sinwave(self, duration, freq):
        self.duration = duration
        self.freq = freq
        
        # 指定周波数のsin波を指定秒数生成        
        self.start_pos = self.end_pos
        self.end_pos = self.start_pos + duration * self.rate
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)

    def press_key(self, event):
        global freq # 初期値として、create_sinwave(FREQ_SCALE['ド/C4'])のために必要

        self.key = event.keysym
        if self.key == 'j':
            self.duration = self.DURATION['L8']
        elif self.key == 'k':
            self.duration = self.DURATION['L4']
        elif self.key == 'l':
            self.duration = self.DURATION['L2']
        elif self.key == 'semicolon':
            self.duration = self.DURATION['L1']

        if self.key == 'a':
            freq = FREQ_SCALE['ド/C4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 's':
            freq = FREQ_SCALE['レ/D4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'd':
            freq = FREQ_SCALE['ミ/E4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'f':
            freq = FREQ_SCALE['ファ/F4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'w':
            freq = FREQ_SCALE['ソ/G4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'e':
            freq = FREQ_SCALE['ラ/A4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'r':
            freq = FREQ_SCALE['シ/B4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 'A':
            freq = FREQ_SCALE['ド/C5']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()
        elif self.key == 's':
            freq = FREQ_SCALE['レ/D4']
            self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
            self.play()

        self.buffer.set(self.key) # New
        self.reset_sinwave()


    # ストリームに渡して再生
    def play(self):
        self.stream.write(self.sin_t.astype(np.float32).tobytes())

    #==== 追加 ===#
    def reset_sinwave(self):
        self.line_wave.set_ydata(self.sin_t[:500])      # グラフのy軸をアップデート
        self.canvas.draw()      

    #==== 追加 ===#
    # 終了ボタンが押された時の処理
    def quit_app(self):
        self.quitting_flag = True # New
        self.stream.close()
        self.master.destroy()
        
    #==== 追加 ===#
    # スタートボタンが押された時の処理
    def start_button_click(self, event):
        self.count = 0
        self.start_flag = True
        
    #==== 追加 ===#
    # ストップボタンが押された時の処理
    def stop_button_click(self, event):
        self.start_flag = False
    
    #==== 追加 ===#
    # タイマー
    def timer(self):
        while not self.quitting_flag:
            if self.start_flag:
                self.metronom_label.config(text=self.count)
                self.count += 1
                if self.count > 4:
                    self.count = 1

                import time
                time.sleep(60/self.bpm)

# パラメータ
FREQ_SCALE = {  # 周波数f0のスケール
    'ド/C4': 261.626,
    'レ/D4': 293.665,
    'ミ/E4': 329.628,
    'ファ/F4': 349.228,
    'ファ#/F#4': 369.994,
    'ソ/G4': 391.995,
    'ソ#/G#4': 415.305,
    'ラ/A4': 440.000,
    'ラ#/A#4': 466.164,
    'シ/B4': 493.883,
    'ド/C5': 523.251,
    'レ/D5': 587.330,
    'ミ/E5': 659.255,
}

root = tk.Tk()
app = Synth_application(master=root)
app.mainloop()

以下、メモです。

threadを用いたタイマーウィジェットは以下の部分です。

まず、__init__()でスレッドを生成し、スタートしています。このスレッドで実行するのはtimer()メソッドです。

    def __init__(self, master=None):
 …
        #==== スレッドの生成と開始 New
        self.thread1 = threading.Thread(target=self.timer)
        self.thread1.start()     

続いて、timerメソッドに通知を送る手段としてstart_flagを設定しています。これがTrueになるときにタイマーのカウント処理が行われます。quitting_flagはスレッド自体を消す際に使います。

    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
…
        self.start_flag = False
        self.quitting_flag = False
        self.count = 0

メインウィンドウには、「スタートボタン」と「ストップボタン」を用意し、それぞれ押されるとstart_flagが変化します。また、カウントされる数字が表示されるテキストボックスも設置しています。

    def create_widgets(self):
…       
        #=== ウィジット:メトロノームウィジェット New
        self.metronom_labelFrame = tk.LabelFrame(self.option_frame, text="Metronom", fg='white', bg='#444', relief=tk.FLAT)
        self.metronom_labelFrame.grid(column=0, row=2, padx=20)
        self.metronom_innerBox = tk.Frame(self.metronom_labelFrame, width=250, height=70)
        self.metronom_innerBox.grid(column=0, row=0)
        self.metronom_innerBox.grid_propagate(0)
        self.metronom_innerBox.grid_anchor(tk.CENTER)
        
        self.start_button = tk.Button(self.metronom_innerBox, text="start")
        self.start_button.grid(column=0, row=0)
        self.stop_button = tk.Button(self.metronom_innerBox, text="stop")
        self.stop_button.grid(column=0, row=1)
        self.metronom_label = tk.Label(self.metronom_innerBox, font=("", 20))
        self.metronom_label.grid(column=1, row=0, padx=20, rowspan=2)
        
        self.start_button.bind("<ButtonPress>", self.start_button_click)
        self.stop_button.bind("<ButtonPress>", self.stop_button_click)     
 …
        
    #==== 追加 ===#
    # スタートボタンが押された時の処理
    def start_button_click(self, event):
        self.count = 0
        self.start_flag = True
        
    #==== 追加 ===#
    # ストップボタンが押された時の処理
    def stop_button_click(self, event):
        self.start_flag = False

timer()では、start_flagがTrueの場合、テンポ(bpm)に応じて数字が「1→2→3→4→1…」とカウントされるようにしています。

    #==== 追加 ===#
    # タイマー
    def timer(self):
        while not self.quitting_flag:
            if self.start_flag:
                self.metronom_label.config(text=self.count)
                self.count += 1
                if self.count > 4:
                    self.count = 1

                import time
                time.sleep(60/self.bpm)

最後に、quit_app()で、アプリを消したときの処理を記述しています。ここで”self.quitting_flag = True”が重要で、これがないとエラーがおこります。

    def __init__(self, master=None):
…
        #==== 追加:終了したときの処理 → def quite_app
        self.master.protocol("WM_DELETE_WINDOW", self.quit_app)
…
    #==== 追加 ===#
    # 終了ボタンが押された時の処理
    def quit_app(self):
        self.quitting_flag = True # New
        self.stream.close()
        self.master.destroy()
…

○課題と解決方法

以下、2つの課題が発生しました。

  1. RuntimeError: main thread is not in main loop
  2. メトロノーム実行時、キーを入力するとタイマーカウントにラグが発生する
RuntimeError: main thread is not in main loop

quit_app()内で”self.quitting_flag = True”を定義しない場合に上記のエラーが発生しました。
これは、アプリを終了した際にスレッド側に終了の通知がいかず、カウントを表示させるメインウィンドウがなくなってしまったため起こるエラーでした。

>メトロノーム実行時、キーを入力するとタイマーカウントにラグが発生する

ラグの原因となっているのは、thread1ではなく、オーディオのstream.write 部分のようです。
なので、メインスレッドをGUI (tkinter + matplotlib の描画部分)のみとし、音声出力もサブスレッドで実施したいと思います。

レシピ③:スレッド、キューを用いてリアルタイム性を調整する

○キューの導入

オーディオ部分もサブスレッドで実行することとしましたが、メトロノームウィジェットと異なる部分があります。
それは、メインスレッド⇔サブスレッドのデータ受け渡しが必要な点です。

ここでいうデータは、波形データ(sin_t)です。この波形データはメインスレッドのグラフプロットにも使いますし、サブスレッドに移そうといている音声出力にも使います。

  • メインスレッド:グラフのプロット ⇛ “self.line_wave, = axes_wave.plot(self.sin_t[:500])”
  • サブスレッド:音声出力 ⇛ “self.stream.write(self.sin_t.astype(np.float32).tobytes())”

このとき、サブスレッドからメインスレッドで定義されているデータ(sin_t)を直参照するのは安全な操作ではないです。

 そこでデータの受け渡しに使うのがQueue(キュー)です。 

キューの概要

キューというのはデータ構造の一種です。特徴として、ある入れ物にデータを格納していく際に最初に格納したデータから順に取り出される方式である点が挙げられます(先入れ先出しFIFOという言い方もします)。

参考までに似たような言葉として、スタック(後入れ先出し、LIFO)というのがあります。

今回やろうとしているメインスレッド⇔サブスレッドの波形データ受け渡しをイメージします。まず、キューという箱をイメージします。メインスレッドでキーが入力されるたびに波形データ(sin_t)をキューに入れていきます。次にサブスレッドでキューから波形データを取り出します。このとき、キューは先入れ先出しのため、連続でキーを入力してもきちんと先に入れた波形データが取り出され音声として再生されます。

キューの実装方法

キューの実装にはqueueというpython標準ライブラリを使います。

キューの構文はとてもシンプルで、キューを箱と考え、q.put()で要素を追加したりq.get()で要素を取り出したりして使います。

(参考)Python入門 (3) -マルチスレッド|npaka|note

# ライブラリのインポート
import queue

# キューの準備
q = queue.Queue()

# キューに要素を追加
q.put() 

# キューから要素を取得
q.get() 

○結果

以下のコードで、グラフプロットと音声出力、メトロノーム間のラグがなくなりました。

ステップ1.キーボード入力で音を鳴らすは完成でよいのではないでしょうか!!!

コードはこちら
 

# ライブラリのインポート
import tkinter as tk
import numpy as np  # sin波
import pyaudio  # メモリ上の音楽を再生
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

import threading
import queue # New

#=== アプリケーションの定義
class Synth_application(tk.Frame):
    #=== 初期設定
    def __init__(self, master=None):
        #=== メインウィンドウの設定
        super().__init__(master) # 親クラス(Application)の __init__ メソッドを実行
        self.master = master
        self.master.title('Synth_Application')
        self.master.resizable(width=False,height=False) #ウィンドウ幅の固定

        #=== オプション:メインウィンドウを画面ん中央に表示させる
        winWidth = 700 # メインウィンドウの幅を定義
        winHeight = 570 # メインウィンドウの高さを定義
        screenWidth = self.master.winfo_screenwidth() # 使用しているモニターの横幅を取得
        screenHeight = self.master.winfo_screenheight() # 使用しているモニターの縦幅を取得
        startX = int((screenWidth / 2) - (winWidth / 2))
        startY = int((screenHeight / 2) - (winHeight / 2))
        self.master.geometry('{}x{}+{}+{}'.format(winWidth, winHeight, startX, startY))  # f-strings 700px × 570pxの画面が左上(縦Xpx、横Ypx)

        #=== sin波の初期値設定 → def start_up
        self.start_up() # 初期値
        
        #=== ウィジットをつくる → def create_widgets
        self.create_widgets()
        
        #=== pyaudioの開始             
        self.p = pyaudio.PyAudio() 
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)

        #=== スレッドの生成と開始 New
        self.thread1 = threading.Thread(target=self.timer, daemon = True)
        self.thread1.start()
        
        self.thread2 = threading.Thread(target=self.play, daemon = True)
        self.thread2.start()        
        self.q = queue.Queue()  # 同期FIFOキューの作成
        
        #==== 終了したときの処理 → def quite_app===#
        self.master.protocol("WM_DELETE_WINDOW", self.quit_app)
        
        
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
        global freq # 初期値のためにglobal変数が必要

        freq = 261.26
        self.DURATION = {
            'L1': (60 / 100 * 4),      # 全音符
            'L2': (60 / 100 * 4) / 2,  # 二分音符
            'L4': (60 / 100 * 4) / 4,  # 四分音符
            'L8': (60 / 100 * 4) / 8,  # 八分音符
        }
        self.duration = self.DURATION['L4']
        self.bpm = 100 # New
        
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い

        self.start_pos = 0
        self.end_pos = self.start_pos + self.duration * self.rate
        
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(self.duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t)
        
        #=== スレッドの初期値
        self.quitting_flag = False # スレッド終了時の同期用
        
        self.start_flag = False # timerスレッドとの同期用
        self.count = 0 # timerスレッドの初期値
        
        self.play_flag = False # playスレッドの同期用
        
    #=== ウィジットの定義
    def create_widgets(self):
        #=== フレームをつくる
        self.canvas_frame = tk.Frame(self.master, width=700, height=250, bg="#000080")
        self.canvas_frame.grid(column=0, row=0) # 1行1列に配置
        self.canvas_frame.grid_propagate(0) # フレーム幅の固定
        self.canvas_frame.grid_anchor(tk.CENTER) # gridを中央に表示するには、grid()で配置する要素の親要素にgrid_anchor(tkinter.CENTER)を使う

        self.option_frame = tk.Frame(self.master, width=700, height=320, bd = 5, relief = tk.GROOVE) # New
        self.option_frame.grid(column=0, row=1)
        self.option_frame.grid_propagate(0)
        self.option_frame.grid_anchor(tk.CENTER)
        
        #=== ウィジェット:LabelFrameウィジットをつくる
        self.chunk_labelFrame = tk.LabelFrame(self.canvas_frame, text="WaveForm", fg='white', bg='#444', relief=tk.FLAT)
        self.chunk_labelFrame.grid(column=0, row=0)
        self.chunk_innerBox = tk.Frame(self.chunk_labelFrame)
        self.chunk_innerBox.grid(column=0, row=0)

        #=== Figureインスタンスをつくる
        fig = Figure(figsize=(6.5,2), dpi=100, tight_layout=True, facecolor='#F0F0F0') # 650 x 200のFigureインスタンスを作成, tight_layoutでオブジェクトの配置を自動調整

        axes_envRange = fig.add_subplot(1, 2, 1)  # 1行2列の最初(1,1)に表示
        axes_envRange.set_title('envRange')
        axes_envRange.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_envRange.set_ylabel('envRange', fontsize=8) # ラベルのオプション

        axes_wave = fig.add_subplot(1, 2, 2)  # 1行2列の2番目(1,2)に表示
        axes_wave.set_title('Sin_wave')
        axes_wave.set_xlabel('t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylabel('sin_t', fontsize=8) # ラベルのオプション
        axes_wave.set_ylim([-(self.gain + 0.05), (self.gain + 0.05)])

        self.line_envRange, = axes_envRange.plot(self.t, self.envRange)
        self.line_wave, = axes_wave.plot(self.sin_t[:500])       
        
        #=== ウィジェット:FigureCanvasTkAggを宣言し、LabelFrameウィジェット上に配置する
        self.canvas = FigureCanvasTkAgg(fig, master=self.chunk_innerBox)
        self.canvas.get_tk_widget().pack(padx=10, pady=10)
        self.canvas.get_tk_widget().bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        
        #=== ウィジェット:キープレスウィジェット
        self.keypress_labelFrame = tk.LabelFrame(self.option_frame, text="KeyPress", fg='white', bg='#444', relief=tk.FLAT) # New
        self.keypress_labelFrame.grid(column=0, row=0, padx=20 ,sticky=tk.N)
        self.keypress_innerBox = tk.Frame(self.keypress_labelFrame, width=250, height=60)
        self.keypress_innerBox.grid(column=0, row=0)
        self.keypress_innerBox.grid_propagate(0)
        self.keypress_innerBox.grid_anchor(tk.CENTER)      
        
        self.keypress_label = tk.Label(self.keypress_innerBox, text="push key is : ", font=("メイリオ", "16"))
        self.keypress_label.grid() 

        self.buffer = tk.StringVar()
        self.buffer.set('xxx')          
        self.press_key_label = tk.Label(self.keypress_innerBox, textvariable = self.buffer, font=("メイリオ", "16"))
        self.press_key_label.grid() 
        self.press_key_label.bind('<Any-KeyPress>', self.press_key) # イベントハンドラと連動
        self.press_key_label.focus_set()
        
        #=== ウィジェット:BPMスケールバーウィジェット
        self.scale_labelFrame = tk.LabelFrame(self.option_frame, text="BPM", fg='white', bg='#444', relief=tk.FLAT) # New
        self.scale_labelFrame.grid(column=0, row=1, padx=20)
        self.scale_innerBox = tk.Frame(self.scale_labelFrame, width=250, height=50)
        self.scale_innerBox.grid(column=0, row=0)
        self.scale_innerBox.grid_propagate(0)
        self.scale_innerBox.grid_anchor(tk.CENTER)
        
        self.bpm_scale = tk.DoubleVar()
        self.bpm_scale.set(100)
        self.bpm_scalebar = tk.Scale(self.scale_innerBox,
                                    variable=self.bpm_scale,  #変数
                                    from_=10,           #下限値
                                    to=200,             #上限値
                                    resolution=2,       #増減ステップ
                                    orient=tk.HORIZONTAL,
                                    length=150,
                                    command=self.reset_bpm)
        self.bpm_scalebar.grid(column=0,row=0, padx=100, pady=20)
        
                
        #=== ウィジット:メトロノームウィジェット
        self.metronom_labelFrame = tk.LabelFrame(self.option_frame, text="Metronom", fg='white', bg='#444', relief=tk.FLAT)
        self.metronom_labelFrame.grid(column=0, row=2, padx=20)
        self.metronom_innerBox = tk.Frame(self.metronom_labelFrame, width=250, height=70)
        self.metronom_innerBox.grid(column=0, row=0)
        self.metronom_innerBox.grid_propagate(0)
        self.metronom_innerBox.grid_anchor(tk.CENTER)
        
        self.start_button = tk.Button(self.metronom_innerBox, text="start")
        self.start_button.grid(column=0, row=0)
        self.stop_button = tk.Button(self.metronom_innerBox, text="stop")
        self.stop_button.grid(column=0, row=1)
        self.metronom_label = tk.Label(self.metronom_innerBox, font=("", 20))
        self.metronom_label.grid(column=1, row=0, padx=20, rowspan=2)
        
        self.start_button.bind("<ButtonPress>", self.start_button_click)
        self.stop_button.bind("<ButtonPress>", self.stop_button_click)

        
        #=== ウィジェット:Tableウィジェット
        self.table_labelFrame = tk.LabelFrame(self.option_frame, text="Table", fg='white', bg='#444', relief=tk.FLAT) # New
        self.table_labelFrame.grid(column=1, row=0, padx=20, rowspan=3) # rowspan
        self.table_innerBox = tk.Frame(self.table_labelFrame, width=250, height=250)
        self.table_innerBox.grid(column=0, row=0)
        self.table_innerBox.grid_propagate(0)
        self.table_innerBox.grid_anchor(tk.CENTER)
        
        self.table_c4 = tk.Label(self.table_innerBox, text = 'a : ド/C4', font=("メイリオ", "16")).grid(column=0, row=0, pady=1, sticky=tk.W)
        self.table_d4 = tk.Label(self.table_innerBox, text = 's : レ/D4', font=("メイリオ", "16")).grid(column=0, row=1, pady=1, sticky=tk.W)
        self.table_e4 = tk.Label(self.table_innerBox, text = 'd : ミ/E4', font=("メイリオ", "16")).grid(column=0, row=2, pady=1, sticky=tk.W)
        self.table_f4 = tk.Label(self.table_innerBox, text = 'f : ファ/F4', font=("メイリオ", "16")).grid(column=0, row=3, pady=1, sticky=tk.W)
        self.table_g4 = tk.Label(self.table_innerBox, text = 'w : ソ/G4', font=("メイリオ", "16")).grid(column=0, row=4, pady=1, sticky=tk.W)
        self.table_a4 = tk.Label(self.table_innerBox, text = 'e : ラ/A4', font=("メイリオ", "16")).grid(column=0, row=5, pady=1, sticky=tk.W)
        self.table_b4 = tk.Label(self.table_innerBox, text = 'r : シ/B4', font=("メイリオ", "16")).grid(column=0, row=6, pady=1, sticky=tk.W)
        self.table_c5 = tk.Label(self.table_innerBox, text = 'A : ド/C5', font=("メイリオ", "16")).grid(column=0, row=7, pady=1, sticky=tk.W)
        self.table_d5 = tk.Label(self.table_innerBox, text = 'S : レ/D5', font=("メイリオ", "16")).grid(column=0, row=8, pady=1, sticky=tk.W)
        self.table_e5 = tk.Label(self.table_innerBox, text = 'D : ミ/E5', font=("メイリオ", "16")).grid(column=0, row=9, pady=1, sticky=tk.W)

    def reset_bpm(self, bpm):
        self.bpm = self.bpm_scale.get() # New
        self.DURATION = {            
            'L1': (60 / self.bpm * 4),      # 全音符
            'L2': (60 / self.bpm * 4) / 2,  # 二分音符
            'L4': (60 / self.bpm * 4) / 4,  # 四分音符
            'L8': (60 / self.bpm * 4) / 8,  # 八分音符
        }
        
    # sin波の作成
    def create_sinwave(self, duration, freq):
        self.duration = duration
        self.freq = freq
        
        # 指定周波数のsin波を指定秒数生成        
        self.start_pos = self.end_pos
        self.end_pos = self.start_pos + duration * self.rate
        self.t = np.arange(self.start_pos, self.end_pos) / self.rate
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)
        
        self.q.put(self.sin_t, block=True, timeout=None) # New


    def press_key(self, event):
        global freq # 初期値として、create_sinwave(FREQ_SCALE['ド/C4'])のために必要

        if self.q.empty():
            self.key = event.keysym
            if self.key == 'j':
                self.duration = self.DURATION['L8']
            elif self.key == 'k':
                self.duration = self.DURATION['L4']
            elif self.key == 'l':
                self.duration = self.DURATION['L2']
            elif self.key == 'semicolon':
                self.duration = self.DURATION['L1']

            if self.key == 'a':
                freq = FREQ_SCALE['ド/C4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 's':
                freq = FREQ_SCALE['レ/D4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'd':
                freq = FREQ_SCALE['ミ/E4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'f':
                freq = FREQ_SCALE['ファ/F4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'w':
                freq = FREQ_SCALE['ソ/G4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'e':
                freq = FREQ_SCALE['ラ/A4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'r':
                freq = FREQ_SCALE['シ/B4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'A':
                freq = FREQ_SCALE['ド/C5']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'S':
                freq = FREQ_SCALE['レ/D5']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 'D':
                freq = FREQ_SCALE['ミ/E5']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True

        self.buffer.set(self.key)
        self.reset_sinwave()

    def play(self):
        while not self.quitting_flag:
            if self.play_flag:
                self.item = self.q.get(block=True, timeout=None) # New
                self.stream.write(self.item.astype(np.float32).tobytes()) # New               
                if self.quitting_flag == True:
                    break
                    
    def reset_sinwave(self):
        self.line_wave.set_ydata(self.sin_t[:500])   # グラフのy軸をアップデート
        self.canvas.draw()      

    # スタートボタンが押された時の処理
    def start_button_click(self, event):
        self.count = 0
        self.start_flag = True

    # ストップボタンが押された時の処理
    def stop_button_click(self, event):
        self.start_flag = False
        
    # タイマー
    def timer(self):
        while not self.quitting_flag:
            if self.start_flag:
                self.metronom_label.config(text=self.count)
                self.count += 1
                if self.count > 4:
                    self.count = 1

                import time
                time.sleep(60/self.bpm)
        
    # 終了ボタンが押された時の処理
    def quit_app(self):
        self.quitting_flag = True
        self.stream.close()
        self.master.destroy()


# パラメータ
FREQ_SCALE = {  # 周波数f0のスケール
    'ド/C4': 261.626,
    'レ/D4': 293.665,
    'ミ/E4': 329.628,
    'ファ/F4': 349.228,
    'ファ#/F#4': 369.994,
    'ソ/G4': 391.995,
    'ソ#/G#4': 415.305,
    'ラ/A4': 440.000,
    'ラ#/A#4': 466.164,
    'シ/B4': 493.883,
    'ド/C5': 523.251,
    'レ/D5': 587.330,
    'ミ/E5': 659.255,
}

root = tk.Tk()
app = Synth_application(master=root)
app.mainloop()

以下、メモです。

play()メソッド実行用に新たにスレッドを作っています。theread2とのやり取りにはplay_flagを用意しました。
また、今回新たに使用するキューを定義しています。

    def __init__(self, master=None):

        #=== スレッドの生成と開始 New
 …
        self.thread2 = threading.Thread(target=self.play, daemon = True)
        self.thread2.start()        
        self.q = queue.Queue()  # 同期FIFOキューの作成
…       
        
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
…       
        #=== スレッドの初期値
…       
        self.play_flag = False # playスレッドの同期用                

キーを入力するたびにplay_flagがTrueになり、play()メソッドが実行されます。

    def press_key(self, event):
…
            if self.key == 'a':
                freq = FREQ_SCALE['ド/C4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
            elif self.key == 's':
                freq = FREQ_SCALE['レ/D4']
                self.create_sinwave(self.duration, freq)   # def create_sinwave()に渡す
                self.play_flag = True
…

キーを押したタイミングで、キューが空の場合、self.sin_tがキューに追加されます。そして、play()メソッドで新たにself.itemという名前でキュー内の波形データを取得し、実行します。

    # sin波の作成
    def create_sinwave(self, duration, freq):
…
        self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t)
        
        self.q.put(self.sin_t, block=True, timeout=None) # New


    def press_key(self, event):
        global freq # 初期値として、create_sinwave(FREQ_SCALE['ド/C4'])のために必要

        if self.q.empty():
…

    def play(self):
        while not self.quitting_flag:
            if self.play_flag:
                self.item = self.q.get(block=True, timeout=None) # New
                self.stream.write(self.item.astype(np.float32).tobytes()) # New               
                if self.quitting_flag == True:
                    break

まとめ&次回予告

○まとめ

今回は前回に引き続き画面(GUI)をカスタマイズしてみました。結果、BPMをスケールバーで変更できたり、押したキーを表示したりとウィジェットが充実してきました。(レシピ②)。

使用したライブラリ(レシピ②)

  • numpy
  • PyAudio
  • Tkinter
  • matplotlib

○今日のエラーと解決方法

エラー

  1. エラーではありませんが、辞書DURATIONをclass Synth_applicationの外に記述した結果、class内のbpmの値を変更しても、bpmを変数に持つDURATIONの値が変わりませんでした。

解決方法

  1. class内のreset_bpm()メソッドに辞書を配置することで、スケールバーにてbpmが変更された際にDURATIONの値も変化するようになりました。

○次回予告

あと欲しい機能としてはメトロノーム機能です。また、音声出力とグラフのプロット部分で、グラフが変化してから音が鳴る形でラグが生じているため、修正していきたいと思います。

  1. キーボード入力で音を鳴らす
    ☑1−1.1つの音を鳴らす
    ☑1-2.音階を鳴らす
    ☑1-3.音の長さを自由に変える
    1-4. 画面をつくる
    1-5.メトロノーム機能をつくる

Leave a Reply

Your email address will not be published. Required fields are marked *