プログラミング習得への道のり #10 画面をカスタマイズする ~PCキーボードで演奏したい~

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

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

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

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

前前前回から、ステップ1. キーボード入力で音を鳴らすのうち、”アプリケーション画面(GUI部分)をつくりはじめる“にチャレンジしています。

前回の記事で“キープレスに応じてGUI画面上の波形をリアルタイムに変化させる”ことにチャレンジし、キープレスに検出にTkinterを用いることでラグはあるものの実装できました。せっかく画面が少しずつ良い感じになって来ましたので、機能面でのラグの解消は次回の記事に回すこととし、本記事では、前回の応用として “TKinterを用いて画面(GUI部分)をカスタマイズする” にチャレンジしました。

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. 曲を作る

☑︎本記事の内容

  • 完成イメージ
  • 必要な材料:Tkinterで画面をカスタマイズする
  • レシピ①:静的なウィジェットを画面に配置する
  • レシピ②:動的なウィジェットを画面に配置する

☑︎著者の経験

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

Twitterもはじめました。

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

○ライブラリ

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

使用したライブラリ

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

完成イメージ

改めて、作りたい画面イメージは以下です。

作りたい画面イメージはこちら

まずは、PCキーボード演奏アプリのステップ1で必要と考えられる要素だけ実装したいと思います。
どんな機能が必要か考えた結果、以下のウィジェットが画面に欲しいと考えました。

つくりたい機能(ウィジェット)

  • キーボードと音の対比を知りたい
  • 押したキーを知りたい
  • テンポを決めたい
  • テンポを知りたい

これらを画面に表示していきたいと思います。それぞれのウィジェットの配置イメージは以下です。

なお、ウィジェットの配置方法は下記の記事で紹介しています。

レシピ①:静的なウィジェットを画面に配置する

まずは簡単な静的なウィジェットの配置からやりました。静的なウィジェットはつくりたい機能のうち

  • キーボードと音の対比を知りたい →  PCキーと出力音の対比テーブル 

が該当します。これは、Labelウィジェットで実現できると考えました。

○結果

以下のコードで、PCキーと出力音の対比を表すTableウィジェットを追加しました。

コードはこちら

# ライブラリのインポート
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 duration # 初期値のためにglobal変数が必要
        duration = DURATION['L4']
        
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い

        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_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)      
                
        #=== ウィジェット: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.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)


        
    # 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 duration, freq # 初期値として、create_sinwave(DURATION['L4'], FREQ_SCALE['ド/C4'])のために必要
        self.key = event.keysym
        if self.key == 'j':
            duration = DURATION['L8']
        elif self.key == 'k':
            duration = DURATION['L4']
        elif self.key == 'l':
            duration = DURATION['L2']
        elif self.key == ';':
            duration = DURATION['L1']

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

        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()


# パラメータ
bpm = int(input('BPMを入力してください : '))
DURATION = {
    'L1': (60 / bpm * 4),      # 全音符
    'L2': (60 / bpm * 4) / 2,  # 二分音符
    'L4': (60 / bpm * 4) / 4,  # 四分音符
    'L8': (60 / bpm * 4) / 8,  # 八分音符
}
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()

以下、メモです。

まずは、ウィジェットごとにフレームを用意し、配置バランスを決めました。

#=== アプリケーションの定義
class Synth_application(tk.Frame):
…        
    #=== ウィジットの定義
    def create_widgets(self):
 …  
        #=== ウィジェット:キープレスウィジェット
        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)      
                
        #=== ウィジェット: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.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)
…

配置のコツは前回の記事で紹介しています。

次に、Tableウィジェットは複数のLabelウィジェットを並べることで実装しました。

        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)

○課題と解決方法

Tableウィジェットをつくる方法としてLabelウィジェットを複数並べましたが、1つのラベルウィジェット内で改行してもよいのでは?と考えました。

# 複数のラベルウィジェット(現在のコード)
        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)

# 1つのラベルウィジェット
        self.table=tk.Label(self.table_innerBox,
                            text="a : ド/C4 \ns : レ/D4 \nd : ミ/E4 \nf : ファ/F4 \nw : ソ/G4 \ne : ラ/A4 \nr : シ/B4 \nA : ド/C5",
                           justify=tk.LEFT,
                           font=("メイリオ", "16"))
        self.table.grid()

しかし、この方法では行間が詰まった印象になってしまいました。色々調べましたが、Labelウィジェット内の行間設定オプションはないようです。一方、Labelウィジェットを複数つくる方法だとオプションpadyで行間設定が可能です。よってコード的には行が増えてしまいますが、この方法で進めようと思います。

動的なウィジェットを画面に配置する

続いて、動的なウィジェットに挑戦しました。つくりたい機能のうち、以下に該当する部分です。

  • 押したキーを知りたい →  押されたPCキーのリアルタイム表示 
  • テンポを決めたい →  テンポを決めるBPM設定画面 

押したキーを知る方法は以前、前回の記事で似たようなことを試したので、それを参考に実装できそうです。また、テンポを決める方法は、Scaleウィジェットでスケールバーを実装することで実現できると考えました。

なお、以下はステップ1-5部分なので次回に回します。

  • テンポを知りたい →  メトロノーム表示画面 

○結果

以下のコードで押したキーの表示とBPMの設定が可能になりました。

コードはこちら

# ライブラリのインポート
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()

        self.buffer.set(self.key) # New
        self.line_wave.set_ydata(self.sin_t[:500])      # グラフのy軸をアップデート
        self.canvas.draw()                    # グラフのアップデート
#        print('bpm: %s' % self.bpm) #jikken
#        print('duration: %s' % self.DURATION) # jikken
#        print('freq: %s' % freq) # jikken

    # ストリームに渡して再生
    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()

以下、メモです。

押されたPCキーのリアルタイム表示

これはbindメソッドとコールバック関数を設定することで実装しました。

#=== アプリケーションの定義
class Synth_application(tk.Frame):
…
    #=== ウィジットの定義
    def create_widgets(self):
…       
        #=== ウィジェット:キープレスウィジェット
…           
        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) # bindメソッド
        self.press_key_label.focus_set()
        
…
    def press_key(self, event): # コールバック関数
        global freq # 初期値として、create_sinwave(FREQ_SCALE['ド/C4'])のために必要

        self.key = event.keysym
…
        if self.key == 'a':
            freq = FREQ_SCALE['ド/C4']
            self.create_sinwave(self.duration, freq) 
            self.play()
        elif self.key == 's':
            freq = FREQ_SCALE['レ/D4']
            self.create_sinwave(self.duration, freq) 
            self.play()
…
        self.buffer.set(self.key) # New
…

なお、詳しいやり方は前回の記事に記載しています。

前回記事

(参考)キー入力とバインド – Python/Tkinter プログラミング

テンポを決めるBPM設定画面

①スケールバーを実装し、②設定されたBPMの値をもとに辞書(DURATION)を更新する必要があるため、

以下のように記述しました。

#=== アプリケーションの定義
class Synth_application(tk.Frame):
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
…
        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
…        
    #=== ウィジットの定義
    def create_widgets(self):
        #=== ウィジェット:BPMスケールバーウィジェット
…        
        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)
…  
    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,  # 八分音符
        }
…

なお、ここで色々苦戦しましたが、課題と解決方法で記述しようと思います。

○課題と解決方法

BPM設定用のスケールバーを実装した際に、辞書(DURATION)をクラスSynth_Application()の外に配置したままにしたところ、bpmは更新されてもbpmを変数に持つDURATIONの値が変わりませんでした。

#=== アプリケーションの定義
class Synth_application(tk.Frame):
    #=== 初期値
    def start_up(self, gain=0.25, rate=44100, chunk_size=1024):
        global duration # 初期値のため
        self.DURATION = {
            'L1': (60 / 100 * 4),      # 全音符
            'L2': (60 / 100 * 4) / 2,  # 二分音符
            'L4': (60 / 100 * 4) / 4,  # 四分音符
            'L8': (60 / 100 * 4) / 8,  # 八分音符
        }
        duration = self.DURATION['L4']
#        duration = DURATION['L4']       
        self.bpm = 100 # New
…        
    #=== ウィジットの定義
    def create_widgets(self):
 …       
        #=== ウィジェット:BPMスケールバーウィジェット
 …      
        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)
 …       
  
    def reset_bpm(self, bpm):
        self.bpm = self.bpm_scale.get() # New
…
    def press_key(self, event):
…
        print('bpm: %s' % self.bpm) # 確認
        print('duration: %s' % self.DURATION) # 確認
…

# パラメータ
#bpm = int(input('BPMを入力してください : '))
DURATION = {
    'L1': (60 / bpm * 4),      # 全音符
    'L2': (60 / bpm * 4) / 2,  # 二分音符
    'L4': (60 / bpm * 4) / 4,  # 四分音符
    'L8': (60 / bpm * 4) / 8,  # 八分音符
}
…

これはPythonの仕様上、辞書は定義されたときに、辞書内の値を評価(計算)して格納します。
そのため、値として入れるものが変数を含む式であったとしても、それ以降に自動的に変更されることはありません。

そのため、変数bpmが更新されるタイミングで辞書DURATIONの値が更新されるように、クラスApplication内のreset_bpm()メソッドにDURATIONを配置しました。

    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,  # 八分音符
        }

まとめ&次回予告

○まとめ

今回は前回に引き続き画面(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 *