Pythonによる信号処理のGUIアプリ作成挑戦記第2弾は「時間波形読み込み」です。wavやcsvをファイルダイアログで開く方法や信号処理クラスとのやり取りを通して、具体的にGUIプログラミングをする方法を模索します。
こんにちは。wat(@watlablog)です。信号処理GUIアプリ作りPART2は時間波形読み込み部分の実装をします!
はじめに
①アプリ構想とフレーム構築編
「フレーム構築編:wxPythonで信号処理のGUIアプリをつくろう①」の記事で信号処理のGUIアプリを作り始めました。
先の記事はどんなコンセプトでアプリを作るのか、各GUIをどうやって作っていくのか、開発環境や実際に作ったGUIコードを紹介しました。
このページは続きから説明をするため、まだ記事を読んでいない方は是非上記リンクからご覧ください。
この記事で行うこと
この記事では時間波形の読み込み部分としてGUIオブジェクトへのイベント付け、ファイル処理やプロットをコードで記述します。
前回はmain.pyとMainFrame.py、wlFrontPanel.pyの3ファイルを作成しましたが、今回は新たにdspToolkit.pyを新規作成し具体的なコードを書いていきます。
開発環境
開発に使うライブラリは前回に比べcsvファイルを扱うPandasとwavファイルを扱うPySoundFileが追加されます。
Numpyや標準ライブラリでもcsvファイルの取り扱いは可能ですが、Pandasはデータ分析の分野で非常に多用されているため、他のプログラムでも互換性を持たせるために使おうという方針です。
Python | Python 3.9.6 |
---|---|
PyCharm (IDE) | PyCharm CE 2020.1 |
Numpy | 1.21.1 |
matplotlib | 3.4.3 |
wxPython | 4.1.1 |
Pandas | 1.0.5 |
PySoundFile | 0.9.0.post1 |
時間波形をGUIで読み込みプロットまで行うPythonコード
早速前回のコードを編集し、時間波形を読み込んでmatplotlibのプロットを行うプログラミングをしていきましょう。
dspToolkit.pyの新規作成
全コード
dspToolkit.pyはGUI以外の信号処理関連コードをまとめます。
信号処理関連コードとは、波形の入ったファイルを読み込みデータに変換する機能も含みます。
.pyファイルを新規作成した後に、以下のコードを記述します。
このファイルはDspToolkit()というclassを記述しており、この中に変数の初期化であるコンストラクタや各種メソッドを追加しました。
Pythonプログラミング初心者のうちはclass表記のコードを見た瞬間ページを閉じたくなるかも知れません。
その場合、まずは「Pythonのクラスの使い方とオブジェクト指向の考え方を理解する」の記事を一読していただけるとこの書き方に戸惑わなくなると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import soundfile as sf import numpy as np import pandas as pd class DspToolkit(): def __init__(self): '''コンストラクタ''' self.time_y = np.array(0) self.time_x = np.array(0) self.sampling = 0 self.dt = 0 self.length = 0 def open_wav(self, path): '''パスを受け取ってwavファイルを読み込む''' self.time_y, self.sampling = sf.read(path) self.get_time_information() def open_csv(self, path): '''パスを受け取ってcsvファイルを読み込む''' df = pd.read_csv(path, encoding='SHIFT-JIS') self.time_y = df.T.iloc[1] self.sampling = 1 / df.T.iloc[0, 1] self.get_time_information() def get_time_information(self): '''Time plotの表示器に表示させる情報の計算と時間軸作成を行う''' self.dt = 1 / self.sampling self.time_x = np.arange(0, len(self.time_y), 1) * self.dt self.length = len(self.time_x) * self.dt print('Time waveform information was obtained.') |
def __init__(self):
この部分はコンストラクタです。コンストラクタにはclass内で使う変数群であるプロパティをひとまとめにして初期化を行います。
とりあえず今回の時間波形読み込み機能に関しては、波形のxyとデータの情報である刻みdt、サンプリングレートsampling、時間長lengthを設定しています。
def open_wav(self, path):
このメソッドはwavファイルを読み込むだけのものです。PySoundFileによる読み込みはサンプリングレートの情報も一緒に得ることができます。
データ情報の計算はcsvファイル読み込みと同じ処理なので、後述するget_time_informationにまとめる書き方にしてみました。
def open_csv(self, path):
このメソッドはcsvファイルを読み込むだけのものです。
サンプルのcsvファイルは下の方にダウンロード可能としますが、0から始まる時間軸データの2行目(df.T.iloc[1]:時間刻みdt)を抽出します。
wavとまとめて条件分岐しても良いと思いましたが、どっちが良いかはまだ迷っています。
def get_time_information(self):
このメソッドはwavとcsvのどちらでも、開かれたデータに対する時間波形情報を計算します。
Time plotタブにある表示器にデータを渡すところはGUIのイベントをまとめるwlFrontPanel.pyの役割です。
wlFrontPanel.pyの編集
全コード
前回の記事で作成したwlFrontPanel.pyを編集します。
まずは編集後の全コードを以下に示します。前回との差異を確認してみてください。
ファイルの先頭にdspToolkit.pyをimportする文を入れて先ほど新規作成したファイルを参照しています。
また、class WlFrontPanelは既に継承していたMainFrame.FrontPanelに続き、DspToolkitを多重継承しています。
正直ここは多重継承で書いて良かったのか、それとも単純にdspToolkitのインスタンスを作って別々で使った方が良かったのか…ソフトウェア開発経験0の筆者には判断がつきませんでした…(強い人、是非アドバイスを…)。
多重継承をやめてコンストラクタでDspToolkitのインスタンス化をしてみました。self.dsp.…と毎回書きますが、この方が個人的にわかりやすいと思いました。
実際どういう書き方が良いのか模索中…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
"""Subclass of FrontPanel, which is generated by wxFormBuilder.""" import wx import numpy as np import MainFrame from dspToolkit import DspToolkit # Implementing FrontPanel class WlFrontPanel(MainFrame.FrontPanel): def __init__(self, parent): MainFrame.FrontPanel.__init__(self, parent) # 初期化 # ファイル self.input_file = '' # 信号処理クラスのインスタンス化 self.dsp = DspToolkit() # ファイルダイアログの戻り値定数(条件分岐用) self.FLAG_OPEN = 5100 self.FLAG_CANCEL = 5101 self.FLAG_YES = 5103 self.FLAG_NO = 5104 def file_selector(self, message, extension): '''指定した拡張子のファイルパスをダイアログで取得する''' # ダイアログでファイルオープン open_dialog = wx.FileDialog(None, message, wildcard=extension) # FLAG_OPEN=5100, FLAG_CANCEL=5101 file_ans = open_dialog.ShowModal() if file_ans == self.FLAG_OPEN: print('User selected file open operation.') self.input_file = open_dialog.GetPath() print('Selected path is;', self.input_file) return self.FLAG_OPEN else: print('User canceled file open operation.') return self.FLAG_CANCEL def time_data_plot(self, time_x, time_y, length): '''時間波形をプロットしてタブをTime plot(0)に切り替える''' time_data = self.ax1_time.plot(time_x, time_y, color='red') # オートスケールで軸設定する場合 self.ax1_time.set_xlim(0, length) self.ax1_time.set_ylim(np.min(time_y), np.max(time_y)) self.canvas_timeplot.draw() line_time = time_data.pop(0) line_time.remove() # タブ(wx.Notebook)の切り替え self.Tab.SetSelection(0) # Handlers for FrontPanel events. def FrontPanelOnClose(self, event): '''ダイアログボックス選択式のウィンドウクローズ''' # Yes/Noダイアログを使ってユーザに選択させる。 message_dialog = wx.MessageDialog(None, 'Do you really want to close window?', 'Window close dialog', wx.YES_NO) ans = message_dialog.ShowModal() # FLAG_YES=5103, FLAG_NO=5104 if ans == self.FLAG_YES: print('User selected YES button to close window.') self.Destroy() def Button_open_wavOnButtonClick(self, event): '''wavファイルを開き時間波形を表示させる''' print('Open.wav was clicked.') # ファイルダイアログを開きwavファイルのパスを取得する。 ans = self.file_selector(message='Select a wav file.', extension='*.wav') # Open操作の時のみ操作を実行する。 if ans == self.FLAG_OPEN: self.dsp.open_wav(self.input_file) # 時間波形情報をTextCtrlに表示 self.textCtrl_dt.SetValue(str('{:.4e}'.format(self.dsp.dt))) self.textCtrl_Samplingrate.SetValue(str(self.dsp.sampling)) self.textCtrl_length.SetValue(str(np.round(self.dsp.length, 2))) # 時間波形をプロットする self.time_data_plot(self.dsp.time_x, self.dsp.time_y, self.dsp.length) def Button_open_csvOnButtonClick(self, event): '''csvファイルを開き時間波形を表示させる''' print('Open.csv was clicked.') # ファイルダイアログを開きwavファイルのパスを取得する。 ans = self.file_selector(message='Select a csv file.', extension='*.csv') # Open操作の時のみ操作を実行する。 if ans == self.FLAG_OPEN: self.dsp.open_csv(self.input_file) # 時間波形情報をTextCtrlに表示 self.textCtrl_dt.SetValue(str('{:.4e}'.format(self.dsp.dt))) self.textCtrl_Samplingrate.SetValue(str(self.dsp.sampling)) self.textCtrl_length.SetValue(str(np.round(self.dsp.length, 2))) # 時間波形をプロットする self.time_data_plot(self.dsp.time_x, self.dsp.time_y, self.dsp.length) def Button_save_wavOnButtonClick(self, event): # TODO: Implement Button_save_wavOnButtonClick pass def Button_save_csvOnButtonClick(self, event): # TODO: Implement Button_save_csvOnButtonClick pass def Button_Save_fft_csvOnButtonClick(self, event): # TODO: Implement Button_Save_fft_csvOnButtonClick pass def textCtrl_FrameSizeOnText(self, event): # TODO: Implement textCtrl_FrameSizeOnText pass def textCtrl_OverlapOnText(self, event): # TODO: Implement textCtrl_OverlapOnText pass def textCtrl_dBrefOnText(self, event): # TODO: Implement textCtrl_dBrefOnText pass def Button_re_calcOnButtonClick(self, event): # TODO: Implement Button_re_calcOnButtonClick pass def Button_lpOnButtonClick(self, event): # TODO: Implement Button_lpOnButtonClick pass def textCtrl_lp_freqOnText(self, event): # TODO: Implement textCtrl_lp_freqOnText pass def Button_hpOnButtonClick(self, event): # TODO: Implement Button_hpOnButtonClick pass def textCtrl_hp_freqOnText(self, event): # TODO: Implement textCtrl_hp_freqOnText pass def textCtrl_attenuation_passOnText(self, event): # TODO: Implement textCtrl_attenuation_passOnText pass def textCtrl_attenuation_stopOnText(self, event): # TODO: Implement textCtrl_attenuation_stopOnText pass def Button_bpOnButtonClick(self, event): # TODO: Implement Button_bpOnButtonClick pass def textCtrl_bp_freq_lowOnText(self, event): # TODO: Implement textCtrl_bp_freq_lowOnText pass def textCtrl_bp_freq_highOnText(self, event): # TODO: Implement textCtrl_bp_freq_highOnText pass def Button_bsOnButtonClick(self, event): # TODO: Implement Button_bsOnButtonClick pass def textCtrl_bs_freq_lowOnText(self, event): # TODO: Implement textCtrl_bs_freq_lowOnText pass def textCtrl_bs_freq_highOnText(self, event): # TODO: Implement textCtrl_bs_freq_highOnText pass def Button_original_dataOnButtonClick(self, event): # TODO: Implement Button_original_dataOnButtonClick pass def checkbox_time_fixOnCheckBox(self, event): # TODO: Implement checkbox_time_fixOnCheckBox pass def textCtrl_time_xminOnText(self, event): # TODO: Implement textCtrl_time_xminOnText pass def textCtrl_time_xmaxOnText(self, event): # TODO: Implement textCtrl_time_xmaxOnText pass def textCtrl_time_yminOnText(self, event): # TODO: Implement textCtrl_time_yminOnText pass def textCtrl_ymaxOnText(self, event): # TODO: Implement textCtrl_ymaxOnText pass def checkbox_freq_fixOnCheckBox(self, event): # TODO: Implement checkbox_freq_fixOnCheckBox pass def textCtrl_freq_xminOnText(self, event): # TODO: Implement textCtrl_freq_xminOnText pass def textCtrl_freq_xmaxOnText(self, event): # TODO: Implement textCtrl_freq_xmaxOnText pass def textCtrl_freq_yminOnText(self, event): # TODO: Implement textCtrl_freq_yminOnText pass def textCtrl_freq_ymaxOnText(self, event): # TODO: Implement textCtrl_freq_ymaxOnText pass |
def __init__( self, parent ):
このクラスのコンストラクタは入力ファイルパスと共に、ファイルダイアログを使った時の戻り値を定数として登録しています。
何度も条件分岐に使う場合はこのような入れ物に入れておいた方が、修正が必要な時にプロパティ値だけ変更すれば良いと思ったので設定しました。定数なので全て大文字で記載しています。
def file_selector(self, message, extension):
このメソッドはファイルダイアログを使ってパスを取得するためのものですが、wavファイルやcsvファイルを開く時に共通して使えるように書きました。
引数は任意のメッセージmessageと拡張子extensionを与えることで、使う時に好きな文章をユーザに見せることができ、さらに指定した拡張子以外はファイルを選択できなくなります。
ファイルを開いた時とダイアログをキャンセルした場合で条件分岐を設け、return文で結果がどうだったかわかるようにしました。
def time_data_plot(self):
wavファイル読み込みボタンとcsvファイル読み込みボタンで、それぞれ時間波形をプロットする操作は同じです。そのためこのメソッドにまとめてみました。
この部分は「wxPythonでGUIレイアウトを作り込む時に参照するページ」で書いたものと同じですが、どんな時間波形が来ても基本は横軸と縦軸のレンジを合わせるようにする処理を入れています。
もちろん後で軸の設定をカスタムする機能は実装する予定です。
タブの初期位置がどこであっても、時間波形が読み込まれた段階でTime plotタブに戻すことができるようself.Tab.SetSelection(0)を記載しています。
def Button_open_wavOnButtonClick( self, event ):
このイベントはOpen .wavボタンをクリックした時に呼び出されます。
これまでに紹介したメソッドを順番に実行します。
def Button_open_csvOnButtonClick( self, event ):
Open .csvボタンをクリックした時も同様にこれまでのメソッドを順番に実行するだけです。
時間波形読み込み動作のデモ
サンプルファイル
プログラムの動作確認にはwavファイルとcsvファイルが必要です。すぐに試せるようにこちらにサンプルファイルを用意しました。
是非ダウンロードして使ってみてください。
wavファイル
dsp-test01:チャープ信号
「Pythonでチャープ信号!周波数スイープ正弦波の作り方」
dsp-test02:のこぎり波信号
「Pythonでのこぎり波を生成!次数の高調波成分を見てみた」
dsp-test03:録音音声(口笛)
「現場でPC1つ!簡単に録音・FFT・wav保存するPythonコード」
csvファイル
csvはこちらの記事の多チャンネルcsvを1チャンネルずつに分解しただけ。
「ただPythonでcsvから離散フーリエ変換をするだけのコード」
YouTube動画による動作確認結果
上記ファイルの作成や編集を行い、前回記事で紹介したmain.pyとMainFrame.pyと合わせて実行することで、時間波形読み込み機能の実装を確認できます。
例によって静止画だと伝わらないと思い、以下のYouTube動画で雰囲気を掴んでいただければと思います。
想定した動作を確認することができました!
まとめ
前回の記事に続きGUIで作る信号処理アプリは時間波形をwavファイルとcsvファイルから読み込めるようになりました。
ここまでで大枠の型はできた状態になったと思いますので、後はひたすら実装していくだけです。
今回はコンストラクタでクラスのインスタンス化をしたり、ダイアログの選択肢結果をif文で判定したりしていますが、筆者は特にIT系の人ではないことにご注意ください(あくまで趣味や勉強の範囲)。
今回のファイル構成やクラス内処理は独自の構成で、かつ我流感が否めないため、筆者自身も引き続き他の人のコードを参考に勉強をしていきます。
さらに開発現場だったらもっとDocstringを丁寧に書いたりTypeHintを書いたりすると思いますが、ここでは省略しています。
ゆくゆくはもっと効率の良いコードを書いてみたいですが…修行あるのみ!
…GUIコードの良いお手本とかどなたかお持ちでしたら情報お願いします💦
(コメント欄、Twitter等へ!)
イベントドリブンなプログラミングが少しずつわかってきました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!
コメント失礼いたします。
本記事にあるプログラムと全く同じ文を用いて動作させようとしているのですが、
self.textCtrl_dt.SetValue(str(‘{:.4e}’.format(self.dsp.dt)))
AttributeError: ‘wlFrontPanel’ object has no attribute ‘textCtrl_dt’というエラーが出てプロットできない状態です。エラーを出している部分を削除して実行すると正常に動作します。
どういった原因が考えられるでしょうか。
ご訪問ありがとうございます。
AttributeErrorを読むと、textCtrl_dtというAttributeがないと書いてあります。
このtextCtrl_dtというのはdt値をテキストボックスで確認するGUIですが、このGUIウィジェットはMainFrame.pyで宣言しています。
MainFrame.pyに以下のコードは書いてありますか?
・MainFrame.pyでウィジェットを登録
self.textCtrl_dt = wx.TextCtrl(…)
〜
bSizer_time_information.Add(…)
この記事は前の記事からの続きとなっているため、その部分のコードはこちらにあります。
https://watlab-blog.com/2022/05/19/gui-dsp01-frame/