0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オーディオプレイヤーを作る DAY4

Posted at

はじめに

Pythonを使ったオーディオプレイヤーの実装過程を順次投稿しています。
前回までの進捗は以下の通りです。

今回実装した内容

  • GUI
    • オーディオ波形のリアルタイム表示
      • 波形自体はmatplolibを利用
    • 再生ボタンとポーズボタンの切り替え
  • 機能
    • マルチスレッド処理の追加
      • オーディオ波形をリアルタイムに更新
      • オーディオプレイヤーの状態を監視

ソースコード

環境

Windows11
Python3.12.7

動作イメージ

音楽 BGMer

ソースコードの補足

マルチスレッド処理

イベントループを処理するスレッド以外に、4つのスレッドを新たに生成するよう仕様を変更

  • オーディオを再生するスレッド
  • プログレスバーとタイム表示を更新する(ためのイベントを発生させる)スレッド
  • オーディオ波形を更新するスレッド
  • オーディオプレイヤーの状態を監視するスレッド

オーディオプレイヤーの状態を監視するスレッドは、オーディオを再生するスレッドを別プロセスで走らせるときに、プロセス間通信で利用予定(たぶん)。

audio.py
class AudioPlayerState(IntEnum):
    PLAYING = auto()
    POSED = auto()
    READY = auto()
    NOT_READY = auto()

スレッドを協調して動作させるために、オーディオプレイヤーの状態を上記のIDで管理します。再生中ならAudioPlayerState.PLAYING、停止中ならAudioPlayerState.POSEDのように。アクセスは、Thread.Lock()を使って保護されます。

simple_audio_player_ver0.4.py
    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つのスレッドを生成し、起動します。また、再生ボタンをポーズボタンに置き換えます。

simple_audio_player_ver0.4.py
    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に変更し、それぞれのスレッドが処理を終えるのを待ちます。アプリの初期化や終了時にも呼ばれます。

simple_audio_player_ver0.4.py
    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に変更します。また、ポーズボタンを再生ボタンに置き換えます。

audio.py
    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である場合のみ再生をし続け、それ以外の場合は再生を停止し、スレッドを終了します。再生が自然終了した場合も、スレッドを終了します。

simple_audio_player_ver0.4.py
    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である場合のみ更新をし続け、それ以外の場合は処理を停止し、スレッドを終了します。

simple_audio_player_ver0.4.py
    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である場合のみ更新をし続け、それ以外の場合は処理を停止し、スレッドを終了します。

simple_audio_player_ver0.4.py
    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である場合のみ監視し続け、それ以外の場合は処理を停止し、スレッドを終了します。再生ボタンと停止ボタンの切り替えもやっていますが、再生が自然終了したときに停止ボタンが再生ボタンに置き換わらないためのバグ対策です。

オーディオ波形の表示

audio_form_frame.py
        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に埋め込みます。

audio_form_frame.py
    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の作法やソースコードに関するアドバイス、質問などがありましたらコメント欄までお願いします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?