はじめに
Pythonを使ったオーディオプレイヤーの実装過程を順次投稿しています。
前回までの進捗は以下の通りです。
今回実装した内容
- GUI
- オーディオ再生中を表すプログレスバーとタイム表示
- 機能
- 各オーディオコントロールボタンに対応するコールバック関数
- マルチスレッド処理
- オーディオの再生
- プログレスバーとタイム表示の更新
ソースコード
環境
Windows11
Python3.12.7
動作イメージ
音楽 BGMer
ソースコードの補足
イベントループを処理するスレッド以外に2つのスレッドを新たに生成しています。
- オーディオを再生するためのスレッド
- プログレスバーとタイム表示を更新する(ためのイベントを発生させる)スレッド
再生以外の処理が重くなるとスムーズに再生できなくなる恐れがあるため、マルチスレッド処理よりも別プロセス(別のCPUリソース)で処理させたほうが良いかもしれません。
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.__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_play.start()
self.__thread_for_progress_bar.start()
再生ボタンが押されると上記の関数がコールバックされます。オーディオプレイヤーの状態をAudioPlayerState.PLAYINGに変更し、オーディオを再生するためのスレッドと、プログレスバーとタイム表示を更新する(ためのイベントを発生させる)スレッドを生成し、起動します。
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
break
スレッドを終了させるためには、上記の関数を呼びます。オーディオプレイヤーの状態をAudioPlayerState.NOT_READYに変更し、それぞれのスレッドが処理を終えるのを待ちます。Thread.join()を使ってスレッドの終了を待つこともできますが、プログレスバーとタイム表示を更新するためのイベントが残っているとデッドロックになるため注意が必要です。
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
ポーズボタンが押されると上記の関数がコールバックされます。オーディオプレイヤーの状態が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
def __update_audio_progress_bar(self):
current_pos = self._audio.current_pos
self._audio_progress_bar.set(current_pos)
self._audio_progress_label.configure(text=self.__pos_to_time(current_pos) + " / " + self.__pos_to_time(self._audio.nframes-1))
プログレスバーとタイム表示を更新するスレッドは、AudioPlayerState.PLAYINGである場合のみ更新をし続け、それ以外の場合は処理を停止し、スレッドを終了します。
備考
- オーディオファイルはWAVとMP3だけに対応
- 他のフォーマットは次回以降に対応
- マルチプロセス化も検討中
おわりに
次回に続きます。
Qiitaの作法やソースコードに関するアドバイス、質問などがありましたらコメント欄までお願いします。