プログラミング習得への道のり #9 アプリの画面をつくる② ~PCキーボードで演奏したい~
プログラミング完全初心者の私が、プログラミングを習得するまでの全過程(主に悩み…)を記事にしていきます。
プログラミング学習にあたり、以下の順で進めています。
①アウトプットを決める、②基礎を学習する、③アウトプットを作り始める
アウトプットは PCキーボードで演奏できるアプリケーション に決めました。
前々回から、ステップ1. キーボード入力で音を鳴らすのうち、”アプリケーション画面(GUI部分)をつくりはじめる“にチャレンジしています。
はじめにpygameというライブラリを用いところ、画面がないためキーを押した瞬間にグラフは出現せず、リアルタイム性がないという課題がでました。そこで前回、Pythonで画面(GUI)をつくるためにTkinterというライブラリの基礎構造をびました。本記事では、前回の応用として “TKinterを用いてキープレスに応じて波形が変化する画面(GUI部分)をつくる” にチャレンジしました。
PCキーボード演奏アプリの作成ステップ
- キーボード入力で音を鳴らす
☑1−1.1つの音を鳴らす
☑1-2.音階を鳴らす
☑1-3.音の長さを自由に変える
1-4.画面をつくる
1-5.メトロノーム機能をつくる - 音を制御する
2-1.波形を変える
2-2.フィルタリング機能を追加する
2-3.複数の音を同時に鳴らす - 外部から音を取り込む
3-1.単音の音源サンプルから音を取り込む(1楽器)
3-2.音源から楽器を取り出す - 機能を追加する
4-1.録音&再生する(パターンをつくる)
4-2.外部と連携する - 曲を演奏する
- 曲を作る
☑︎本記事の内容
- 必要な材料:Tkinterで画面をつくる
- レシピ①:静的なグラフを画面に配置する
- レシピ②:動的なグラフを画面に配置する(キープレスに応じて波形を変化させる)
☑︎著者の経験
この記事を書いている私は、非情報系の大学院を卒業後、通信関係の企業に3年勤めた後、現在までコンサルティング会社に勤めています。
学生時代〜現職までプログラミングとは縁がなく、前職でルーターの設定を少しかじった程度のプログラミング完全初心者です。
こういった私が、解説していきます。
Twitterもはじめました。
必要な材料:GUI部分の作成
○ライブラリ
今回使用するライブラリは以下です。
使用したライブラリ
- numpy:数値演算用ライブラリ → sin波を作る
- PyAudio:オーディオ関連ライブラリ → 音を鳴らす
- matplotlib:グラフ作成ライブラリ → 波形をプロットする
- Tkinter:GUI作成ライブラリ → 画面をつくる
レシピ①:静的なグラフを画面に配置する
改めて、作りたい画面イメージは以下です。
前の記事でTkinterに静的なグラフを表示させることはできました。
今回やりたいことは以前の記事であげたとおり、sin波およびゲインを画面上にプロットすることです。そして、 sin波をキープレスに応じて動的に変化させたい と考えています。
※なお、ゲインの制御は、ステップ2. 音を制御するで検討するため、ステップ1では一旦ゲインの変化はなしで進めます。
とはいえ、まずはTkinterの基本的な構造を確かめるために、静的なグラフを画面に表示してみようと思います。
○参考コードの確認
1からコードを書く前に、参考にしていたGitHubのコードから、sin波以外の部分を削ぎ落として今回のポイントを確認してみました。
参考コード(synth_gui.py)の実行結果
上記から、そのまま流用できるところと改良が必要なところを洗い出してみました。
そのまま流用できるところ
- 画面をPCの真ん中に表示させている
- plt.figureじゃなくてFigureを使う
これらについては、すぐにわかったので以下、メモに残します。
改良が必要なところ、よくわからないところ
- 配置がずれているため、真ん中に配置したい
- classの範囲がよくわからない
- classの因数にtk.Frameが含まれている理由がわからない
これらを考えるうえで、方針として、
- classなしで配置を整える
- class化する
で考えていくことにしました。
○結果①:配置を整える(classなし)
以下のコードで波形のグラフ配置を整えることができました。
ポイントは、前回の記事に沿って、メインウィンドウにいきなりグラフのウィジェットを配置するのではなく、①ウィジェットごとに配置を整えられうようにフレームを配置し、②その上にグラフウィジェット用のフレーム(FrameとLabelFrameを組み合わせたもの)を配置し、③その上にグラフウィジェットを配置してる点です。
①と②の違いは、①はメインウィンドウの整形用で、今回は見える形にしていますが見えなくてもよいかなと思っています。一方、②は見栄えをよくするためのフレームです。
以下、メモです。
ウィジェットを配置するときはウィジェットの第1引数に子ウィジェットを指定し、親ウィジェットと子ウィジェットの関係を示します。たとえば、メインウィンドウにframを配置するときは、frame=tk.Frame(root, …)となり、frameウィジェットにborderウィジェットを配置するときは、border=tk.LabelFrame(frame, …)のような感じです。
# 1. メインウィンドウの作成
root = tk.Tk()
…
#=== オプション:フレームをつくる
frame = tk.Frame(root, …)
frame.grid(column=0, row=0) # 1行1列に配置
…
#=== オプション:ラベルフレームウィジットをつくる
border = tk.LabelFrame(frame, …)
border.grid(column=0, row=0)
innerBox = tk.Frame(border)
innerBox.grid()
# 2. Matplotlibライブラリを利用して、グラフを作成する
#=== 2-1.Figureインスタンスをつくる
fig = Figure(…)
# 3. FigureCanvasTkAggを宣言し、Frameウィジェット上に配置する
canvas = FigureCanvasTkAgg(fig, master=innerBox)
…
○課題と解決方法
.grid()を使って”frame = tk.Frame()”の中心に”border = tk.LabelFrame()”を表示させようと思いしたが、tk.LabelFrameの親ウィジットに”frame”を指定するとなぜかLabelFrameが左上に配置され、オプションのanchorなどで位置を指定しても変わりませんでした。また、ネイビー色に設定したはずのframeも消えてしまいました。
失敗結果
課題
- frameの枠が消える
- borderがメインウィンドウの左上に表示される
これを解決するのにとても時間を費やしました。。。
重要なポイントは、 gridのサイズは子要素に合わせられるということです。
今回の階層構造(親要素と子要素)は以下となっています。
階層構造
- root ・・・第1階層
- frame ・・・第2階層
- border ・・・第3階層
- innerBox・・・第4階層
- border ・・・第3階層
- frame ・・・第2階層
このとき、borderのサイズは子要素innerBoxに合わさった状態となり、innerBoxは親要素borderにgridで配置されます。同様にframeのサイズは子要素borderに合わさった状態となり、borderは親要素frameにgridで配置されます。
これらの親要素と子要素の関係、そしてサイズは子要素に合わせられることからから、結果的にborderがメインウィンドウの左上に配置されたように見えたようです。
これを解消する方法として、以下の手順が必要です。
- frameの枠が消える → 第2階層frameのフレームサイズを固定する
- borderがメインウィンドウの左上に表示される → 第3階層borderのgrid配置位置を第2階層frameで定義する
これを説明するにあたり、簡単な例を考えました。
階層構造
- root ・・・第1階層(メインウィンドウ)
- frame1 ・・・第2階層(赤色フレーム)
- frame2 ・・・第3階層(青色フレーム)
- frame1 ・・・第2階層(赤色フレーム)
まず、単純にそれぞれを配置すると以下のように、第2階層の赤色フレームが見えず、第3階層の青色フレームが左上に配置されます。
次に、第2階層frame1のフレームサイズをgrid_propagate(0)で固定します。
frame1.grid_propagate(0) # フレーム幅の固定
すると、第2階層が見えるようになりました。しかし、第3階層は右上のままです。
最後に、第3階層frame2のgrid配置位置を調整します。gridを中央に表示するには、grid()で配置する要素の親要素にgrid_anchor(tkinter.CENTER)を使います。
frame1.grid_anchor(tk.CENTER) # gridを中央に表示するには、grid()で配置する要素の親要
すると、第3階層が第2階層の中央に配置されました。
○結果②:配置を整える(class化)
classを用いずに静的なグラフをいい感じに配置することができました。次にこれをclass化しました。classについて前記事で記載しましたが、正直ふんわりしかわかってませんので、確かめながらやりました。
まず、メインウィンドウ部分だけクラス”Application”としてclass化してみました。
うまくいったので、create_widgets()メソッドを定義しました。こちらもうまくいきました。
__init__()内でにself.create_widgets()を記述することで、class実装時にcreate_widgets()メソッドが実装されます。
○課題と解決方法
Applicationの引数にtk.Frameを入れないとTypeErrorになりました。
TypeError: object.__init__() takes exactly one argument (the instance to initialize)
調べたところ、“class Application(tk.Frame)”の様な書き方の場合、 「継承」 と言い、tk.Frameを渡すことで、Frameオブジェクトを継承しているようです。実際にはメインウィンドウ用のインスタンスを作成したあと、そのオブジェクトをrootに渡すことになります。
正直まだあまりピンときてませんが、とりあえず入れるものという理解でいきたいと思います。(詳しくわかったら追記します。)
動的なグラフを画面に配置する(キープレスに応じて波形を変化させる)
静的なグラフを表示できたので、いよいよキープレスに応じて音を変化させるアプリに応用させます。
○キー入力とバインド
前の記事で、Tkinterはイベント駆動式のライブラリであることを記述しました。つまり、何かしらの処理(イベント)が起こると、そのイベントをきっかけにプログラムが実行されることを言います。
Tkinterの動作をもう一度記述します。
- 初期化
- イベント取得
- イベントに応じた処理の振り分け
- 後続プログラム実行
- 2 に戻る
今回新しくコードに加えるのは、2~4のところです。これに必要なのがバインディングとイベントハンドラです。
- バインディング:振り分け時の処理機能(上記の2,3に対応) → bind()メソッド
- イベントハンドラ:バインディングによって実行される後続プラグラム(上記の4) → コールバック関数
今回、イベントはキー入力になります。参考になる記事があったので、まずは簡単なコードで試してみたいと思います。
(参考)キー入力とバインド – Python/Tkinter プログラミング
キープレスを検出し、入力されたキーを画面に表示されるコードを書きました。結果は以下です。
import tkinter as tk
from tkinter import *
# メインウィンドウ
root = tk.Tk()
root.option_add('*font', ('FixedSys', 14)) # オプション:フォント、文字サイズ
# 式を格納するオブジェクト
buffer = StringVar() # "from tkinter import * " が必要
buffer.set('')
# キーの表示
def print_key(event): # イベントハンドラ(コールバック関数)
key = event.keysym
buffer.set('push key is %s' % key)
# ラベルの設定
Label(root, text = '*** push any key ***').pack()
a = Label(root, textvariable = buffer)
a.pack()
a.bind('<Any-KeyPress>', print_key) # バインディング
a.focus_set()
root.mainloop()
bind()メソッドは以下の構文で記述します。
widget.bind(eventsequence, callback)eventsequenceのイベント情報を受け、callbackに記述された後続のプログラムが実行されるという意味です。
また、イベントの指定(eventsequence)は次のような構文を持っています。
<modifier-modifier-type-detail>キープレス検出の一部を紹介します。
- <KeyPress-a>:キー”a”が入力されたときのイベント
- <Button-1>:キー”1”が入力されたときのイベント ※数字キーはKeyPressではなくButtonを使う
- <Control-d> :コントロールキーと d キーを同時に入力されたときのイベント
この情報をもとに、キープレスを検出し、入力されたキーによって音を変えるようにしたいと思います。
○結果
以下のコードで、入力キーによって出力される音が変わり、更に画面の波形も変化しました。
ただし、波形の変化と音の再生の間にラグが生じています。
以下、メモです。
- self.canvas.get_tk_widget().bind(‘<Any-KeyPress>’, self.press_key)がイベントハンドラで、何らかのキープレスがあった際に、コールバックself.press_keyに続きます。
- def press_key(self, event)がコールバック関数です。注意点として、 コールバック関数はイベントハンドラの後続処理なので、__init__()に記述は必要ありません。
- self.key = event.keysymとしたあと、if self.key == ‘a’のような書き方をすることで、押されたキーを指定することができます。
- self.line_wave.set_ydata(self.sin_t[:500])とself.canvas.draw()がないとグラフがアップデートされません。
#=== アプリケーションの定義
class Synth_application(tk.Frame):
#=== 初期設定
def __init__(self, master=None):
…
#=== ウィジットの定義
def create_widgets(self):
…
#=== ウィジェット: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) # イベントハンドラ
…
def press_key(self, event): # コールバック関数
global duration, freq # 初期値として、create_sinwave(DURATION['L4'], FREQ_SCALE['ド/C4'])のために必要
self.key = event.keysym
…
if self.key == 'a':
freq = FREQ_SCALE['ド/C4']
self.create_sinwave(duration, freq) # def create_sinwave()に渡す
self.play()
…
self.line_wave.set_ydata(self.sin_t[:500]) # グラフのy軸をアップデート
self.canvas.draw() # グラフのアップデート
○課題と解決方法
class Synth_application内のcreate_sinwave()メソッドにて、波の式を書きとしたところエラーとなりました。
# 指定周波数のsin波を指定秒数生成
t = np.arange(0, duration * self.rate) / self.rate
envRange = np.ones(int(duration * self.rate)) # 1字配列
sin_t = self.gain * np.sin(2 * np.pi * freq * t)
NameError: name ‘sin_t’ is not defined
一方、create_sinwave()内のtやsin_tに”self.”をつけるとエラーが無くなりました。
ここで、sin_tはcreate_sinwave()のアトリビュート(引数)ではない認識でした。そのため、app = Synth_application()で実装したあと、create_sinwave()メソッドとplay()の順に実行できると思ったところ、creat_sinwave()メソッドで定義しているsint_tがplay()メソッド実行時にNameErrorになりました。
class内の関数(メソッド)のどのような場合に ”self.”が必要か不要かわからなくなりました。
○クラス内の変数
まず、公式ドキュメント読んでみました。が、まだよくわからなかったので、実際に色々試してみました。
(参考)公式ドキュメント チュートリアル 9.3.5. クラスとインスタンス変数
(参考)Pythonで、グローバル変数、インスタンス変数、ローカル変数 …
第一に、クラス直下とコンストラクタの変数です。
class Animal:
kind = 'dog' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
dog1 = Animal('Pochi')
dog2 = Animal('Taro')
###
print(dog1.kind) # shared by all animals
print(dog2.kind) # shared by all animals
print(dog1.name) # unique to dog1
print(dog2.name) # unique to dog2
結果は以下のとおりで、 クラス直下はすべてのインスタンスに適応され、コンストラクタではインスタンス固有になることがわかります。
print(dog1.kind) → dog
print(dog2.kind) → dog
print(dog1.name) → Pochi
print(dog2.name) → Taro
また、クラス直下の変数はメソッド内でも使用できました。メソッドaaa()でself.kindとすることでクラス直下のメソッドを呼び出せることがわかります。
class Animal:
kind = 'dog' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
def aaa(self):
print(self.kind)
dog1 = Animal('Pochi')
dog2 = Animal('Taro')
###
dog1.aaa()
dog2.aaa()
dog1.aaa() → dog
dog2.aaa() → dog
次にメソッドをいくつか用意し、メソッド間の動きを確かめます。
class Animal:
kind = 'dog' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
def aaa(self):
print(self.kind)
def bbb(self):
xxx = self.name
self.yyy = self.name
print("xxx:", xxx)
print("self.yyy:", self.yyy)
def ccc(self):
# zz1 = xxx # Pattern1
# self.zz2 = xxx # Pattern2
# zz3 = self.yyy # Pattern3
# self.zz4 = self.yyy # Pattern4
# print("zz1:" ,zz1)
# print("self.zz2:" ,self.zz2)
# print("zz3:" ,zz3)
# print("self.zz4:" ,self.zz4)
dog1 = Animal('Pochi')
###
dog1.bbb()
dog1.ccc()
__init__()とメソッドbbb()
メソッドbbb()に__init__()内のnameを呼び出すコードを書きました。
ここで、”self.”の有無を比較したところ、どちらでもnameを呼び出すことができました。
xxx: Pochi
self.yyy: Pochi
メソッドbbb()とメソッドccc()
次に、メソッド間の変数のやりとりを確かめて見ました。パターンは以下の4つです。
①両方にself.をつけない
②呼び出す側(メソッドccc())のみself.をつける
③呼び出される側(メソッドbbb())のみself.をつける
④両方にself.をつける
結果は以下のとおりでした。
① → NameError: name ‘xxx’ is not defined
② → NameError: name ‘xxx’ is not defined
③ → zz3: Pochi
④ → self.zz4: Pochi
これらの結果からわかったことは以下です。
self.がない場合:ローカル変数としてクラス内の他のメソッドに渡せないself.がある場合:インスタンス変数としてクラス内の他のメソッドに渡せる
※クラス直下の変数もインスタンス変数としてメソッド内のself.と同じ
まとめ&次回予告
○まとめ
PCのキープレスによってsin波の周波数を変えたのち、画面上のグラフにプロットすることができました(レシピ②)。
ただし、グラフが変化してから音が鳴る形でラグが生じています。これは新たな課題として今後解決していこうと思います。
使用したライブラリ(レシピ②)
- numpy
- PyAudio
- Tkinter
- matplotlib
ポイント
- レイアウト調整用にメインウィンドウ上にフレームを配置し、その上にウィジェットを配置しています。
- イベントハンドラ”self.canvas.get_tk_widget().bind(‘<Any-KeyPress>’, self.press_key)”とコールバック関数“def press_key(self, event)”でキープレスがあった際に、その種類によって音を変えています。
○今日のエラーと解決方法
エラー
- エラーではありませんが、ウィジェットがメインウィンドウの左上に寄ってしまった。
- TypeError: object.init() takes exactly one argument (the instance to initialize)
- NameError: name ‘sin_t’ is not defined
解決方法
- gridのサイズは子要素に合わせられるため、フレームサイズをgrid_propagate(0)で固定し、フレーム内の配置場所をgrid_anchor()で指定することで解消しました。
frame1.grid_propagate(0) # フレーム幅の固定
frame1.grid_anchor(tk.CENTER) # gridを中央に表示するには、grid()で配置する要素の親要
- “class Application(tk.Frame)”のようにclassをtk.Frameに「継承」させることで解消しました。
- class内のアトリビュート(変数)に”self.”をつけることでメソッド間で変数を参照することができました。
○次回予告
ラグの修正もそうですが、せっかく画面ができたので次回はもう少し画面をカスタマイズしてみたいと思います。
- キーボード入力で音を鳴らす
☑1−1.1つの音を鳴らす
☑1-2.音階を鳴らす
☑1-3.音の長さを自由に変える
1-4. 画面をつくる
1-5.メトロノーム機能をつくる
lteru
最新記事 by lteru (全て見る)
- 【ノーコード・ローコード】OutSystemsでアプリづくり#04 ~マッチングアプリをつくる①~ - 2022年8月13日
- 【ノーコード・ローコード】OutSystemsでアプリづくり#03 ~簡単なリスト型アプリをつくる③~ - 2022年7月24日
- 【ノーコード・ローコード】OutSystemsでアプリづくり#02 ~簡単なリスト型アプリをつくる②~ - 2022年7月22日