プログラミング習得への道のり #10 画面をカスタマイズする ~PCキーボードで演奏したい~
プログラミング完全初心者の私が、プログラミングを習得するまでの全過程(主に悩み…)を記事にしていきます。
プログラミング学習にあたり、以下の順で進めています。
①アウトプットを決める、②基礎を学習する、③アウトプットを作り始める
アウトプットは PCキーボードで演奏できるアプリケーション に決めました。
前前前回から、ステップ1. キーボード入力で音を鳴らすのうち、”アプリケーション画面(GUI部分)をつくりはじめる“にチャレンジしています。
前回の記事で“キープレスに応じて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作成ライブラリ → 画面をつくる
完成イメージ
改めて、作りたい画面イメージは以下です。
まずは、PCキーボード演奏アプリのステップ1で必要と考えられる要素だけ実装したいと思います。
どんな機能が必要か考えた結果、以下のウィジェットが画面に欲しいと考えました。
つくりたい機能(ウィジェット)
- キーボードと音の対比を知りたい
- 押したキーを知りたい
- テンポを決めたい
- テンポを知りたい
これらを画面に表示していきたいと思います。それぞれのウィジェットの配置イメージは以下です。
なお、ウィジェットの配置方法は下記の記事で紹介しています。
レシピ①:静的なウィジェットを画面に配置する
まずは簡単な静的なウィジェットの配置からやりました。静的なウィジェットはつくりたい機能のうち
- キーボードと音の対比を知りたい → PCキーと出力音の対比テーブル
が該当します。これは、Labelウィジェットで実現できると考えました。
○結果
以下のコードで、PCキーと出力音の対比を表すTableウィジェットを追加しました。
以下、メモです。
まずは、ウィジェットごとにフレームを用意し、配置バランスを決めました。
#=== アプリケーションの定義
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の設定が可能になりました。
以下、メモです。
>押された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
○今日のエラーと解決方法
エラー
- エラーではありませんが、辞書DURATIONをclass Synth_applicationの外に記述した結果、class内のbpmの値を変更しても、bpmを変数に持つDURATIONの値が変わりませんでした。
解決方法
- class内のreset_bpm()メソッドに辞書を配置することで、スケールバーにてbpmが変更された際にDURATIONの値も変化するようになりました。
○次回予告
あと欲しい機能としてはメトロノーム機能です。また、音声出力とグラフのプロット部分で、グラフが変化してから音が鳴る形でラグが生じているため、修正していきたいと思います。
- キーボード入力で音を鳴らす
☑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日