はじめに
Pythonを使ったオーディオプレイヤーの実装過程を順次投稿しています。
前回までの進捗は以下の通りです。
今回実装した内容
- GUI
- オーディオ波形のリアルタイム表示
- 波形自体はmatplolibを利用
- 再生ボタンとポーズボタンの切り替え
- オーディオ波形のリアルタイム表示
- 機能
- マルチスレッド処理の追加
- オーディオ波形をリアルタイムに更新
- オーディオプレイヤーの状態を監視
- マルチスレッド処理の追加
ソースコード
環境
Windows11
Python3.12.7
動作イメージ
音楽 BGMer
ソースコードの補足
マルチスレッド処理
イベントループを処理するスレッド以外に、4つのスレッドを新たに生成するよう仕様を変更
- オーディオを再生するスレッド
- プログレスバーとタイム表示を更新する(ためのイベントを発生させる)スレッド
- オーディオ波形を更新するスレッド
- オーディオプレイヤーの状態を監視するスレッド
オーディオプレイヤーの状態を監視するスレッドは、オーディオを再生するスレッドを別プロセスで走らせるときに、プロセス間通信で利用予定(たぶん)。
class AudioPlayerState(IntEnum):
PLAYING = auto()
POSED = auto()
READY = auto()
NOT_READY = auto()
スレッドを協調して動作させるために、オーディオプレイヤーの状態を上記のIDで管理します。再生中ならAudioPlayerState.PLAYING、停止中ならAudioPlayerState.POSEDのように。アクセスは、Thread.Lock()を使って保護されます。
def __play(self):
state = self._player.state
if state == AudioPlayerState.PLAYING:
return
if state == AudioPlayerState.NOT_READY:
return
self._player.state = AudioPlayerState.PLAYING
self._audio_pose_button.grid()
self._audio_play_button.grid_remove()
self.__thread_for_play = Thread(target=self._player.play, daemon=False)
self.__thread_for_progress_bar = Thread(target=self.__update_audio_progress_bar_while_playing, daemon=False)
self.__thread_for_audio_form = Thread(target=self.__update_audio_form_while_playing, daemon=False)
self.__thread_for_state = Thread(target=self.__check_state_while_playing, daemon=False)
self.__thread_for_play.start()
self.__thread_for_progress_bar.start()
self.__thread_for_audio_form.start()
self.__thread_for_state.start()
再生ボタンが押されると上記の関数がコールバックされます。オーディオプレイヤーの状態をAudioPlayerState.PLAYINGに変更し、4つのスレッドを生成し、起動します。また、再生ボタンをポーズボタンに置き換えます。
def close(self): # kill living threads
self._player.state = AudioPlayerState.NOT_READY
while True:
self.update()
if self.__thread_for_play.is_alive():
time.sleep(0.01)
continue
if self.__thread_for_progress_bar.is_alive():
time.sleep(0.01)
continue
if self.__thread_for_audio_form.is_alive():
time.sleep(0.01)
continue
if self.__thread_for_state.is_alive():
time.sleep(0.01)
continue
break
スレッドを終了させるためには、上記の関数を呼びます。オーディオプレイヤーの状態をAudioPlayerState.NOT_READYに変更し、それぞれのスレッドが処理を終えるのを待ちます。アプリの初期化や終了時にも呼ばれます。
def __pose(self): # kill living threads
state = self._player.state
if state == AudioPlayerState.POSED:
return
if state == AudioPlayerState.NOT_READY:
return
if state == AudioPlayerState.PLAYING:
self.close()
self._player.state = AudioPlayerState.POSED
self._audio_play_button.grid()
self._audio_pose_button.grid_remove()
ポーズボタンが押されると上記の関数がコールバックされます。オーディオプレイヤーの状態がAudioPlayerState.PLAYINGだった場合、スレッドを終了し、AudioPlayerState.POSEDに変更します。また、ポーズボタンを再生ボタンに置き換えます。
def play(self):
try:
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(self.__audio.samplewidth), channels=self.__audio.nchannels, rate=self.__audio.framerate, output=True)
while True:
data = self.__audio.read_frames(CHUNK_SIZE)
if len(data) == 0:
self.__audio.rewind()
break
stream.write(data)
if self.state != AudioPlayerState.PLAYING:
break
stream.close()
p.terminate()
self.state = AudioPlayerState.READY
except:
print('Error: cannot play the audio')
return
オーディオを再生するスレッドは、AudioPlayerState.PLAYINGである場合のみ再生をし続け、それ以外の場合は再生を停止し、スレッドを終了します。再生が自然終了した場合も、スレッドを終了します。
def __update_audio_progress_bar_while_playing(self):
while True:
self.__update_audio_progress_bar()
time.sleep(0.01)
state = self._player.state
if state != AudioPlayerState.PLAYING:
break
self.__update_audio_progress_bar()
プログレスバーとタイム表示を更新するスレッドは、AudioPlayerState.PLAYINGである場合のみ更新をし続け、それ以外の場合は処理を停止し、スレッドを終了します。
def __update_audio_form_while_playing(self):
while True:
self.master._audio_form_frame.update_audio_form()
time.sleep(0.1)
state = self._player.state
if state != AudioPlayerState.PLAYING:
break
self.master._audio_form_frame.update_audio_form()
オーディオ波形を更新するスレッドは、AudioPlayerState.PLAYINGである場合のみ更新をし続け、それ以外の場合は処理を停止し、スレッドを終了します。
def __check_state_while_playing(self):
while True:
time.sleep(0.01)
state = self._player.state
if state != AudioPlayerState.PLAYING:
self._audio_play_button.grid()
self._audio_pose_button.grid_remove()
break
オーディオの状態を監視するスレッドは、AudioPlayerState.PLAYINGである場合のみ監視し続け、それ以外の場合は処理を停止し、スレッドを終了します。再生ボタンと停止ボタンの切り替えもやっていますが、再生が自然終了したときに停止ボタンが再生ボタンに置き換わらないためのバグ対策です。
オーディオ波形の表示
self.__fig, self.__ax = plt.subplots(figsize=(4, 1), dpi=100, tight_layout=True, facecolor='#2B2B2B')
self.__ax.axis("off")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.__canvas = FigureCanvasTkAgg(self.__fig, master=self)
self.__canvas.get_tk_widget().grid(row=0, column=0, padx=(5, 5), pady=(5, 5), sticky='WE')
まず、matplotlibで生成したグラフをCustomTkinterに埋め込みます。
def update_audio_form(self):
current_pos = self.__audio.current_pos
y = self.__read_frames(current_pos)
if y is None:
return
x = np.arange(-self.__audio_form_radius, self.__audio_form_radius + 1)
self.__ax.cla()
self.__ax.axis('off')
self.__ax.set_xlim(-self.__audio_form_radius, self.__audio_form_radius)
self.__ax.set_ylim(-1, 1)
self.__ax.vlines(0, -1, 1, colors='#888888', linewidth=1.5)
self.__ax.plot(x, y)
self.__canvas.draw()
上記は、オーディオデータをグラフにプロットする関数です。オーディオ波形を更新するスレッドから呼ばれ、時々刻々変化する波形をプロットしていきます。draw()は処理が重く、リアルタイム表示には不適かもしれません。
備考
- オーディオファイルはWAVとMP3だけに対応
- 他のフォーマットは次回以降に対応
- マルチプロセス化を検討中
- オーディオ波形を表示する処理が重く、音切れが発生するため、オーディオを再生するスレッドを別プロセスにすることは必須
おわりに
次回に続きます。
Qiitaの作法やソースコードに関するアドバイス、質問などがありましたらコメント欄までお願いします。