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

## はじめに
Qiitaに投稿するのはこれが初めてになります。

投稿ネタとして、Pythonを使ったオーディオプレイヤ―の実装過程を勉強がでら順次投稿する予定です。オーディオプレイヤーを選択した理由は２つあります。以前、英語の勉強のためにオーディオファイルを任意の場所で分割するコードを実装したことがあります。音声データの取り扱いについて多少理解をしているものの、あいまいな部分も多く、いつか時間が取れた時に確認したいと思ってました。これが１つ目の理由です。また、Pythonを使ったGUIの実装に不慣れだというのが２つ目の理由です。

なお、ソースコードはGitHubに公開する予定です。

## GUIライブラリ
今回は、CustomTkinerを使用します。Pythonに標準的に組み込まれているTkinterをベースに開発されたモダンなデザインのGUIライブラリです。TkinterもモダンなデザインのGUIを実装可能ですが、コストをかけずに洗練されたデザインのGUIを実装できるところがCustomTkinerの良いポイントだと思います。また、Tkinterをベースにしているため、Tkinterを使った経験がそのまま活用できますし、CustomTkinerについてわからないところもTkinterで検索すれば解決策がすぐに見つかるところも良いです。

https://github.com/TomSchimansky/CustomTkinter

## 今回実装した内容
* GUI
    * CTkFrameで３つのサブフレーム
        * CoverArtDisplayFrame
            * タイトルやアルバム名などのメタ情報やカバーアートを表示する領域
        * AudioPlayerControllerFrame
            * プレイヤーをコントロールするボタンなどを配置する領域
        * FileDialogFrame
            * ファイルダイアログを配置する領域
    * CTkButtonで再生やポーズなどを行うためのボタン
    * CTkLabelでメタ情報を表示するラベル
    * CTkEntryでファイルパスを表示するテキストエントリ
* 機能
    * WAVファイルの入力と再生

![widgets.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483587/6559f6eb-7778-fa2b-cc44-e1c35af946a1.jpeg)
 
## ソースコード

https://github.com/junpiyo/Simple-Audio-Player-with-Python/tree/main/Day1

##  環境
Windows11
Python3.12.7

## ソースコードの補足
### アルバムアートの表示
```python: simple_audio_player_ver0.1.py
    def set_cover_art(self):
        if self._default_light_image == None and self._default_dark_image == None:
            None
        else:
            self._cover_art_display_label.configure(image=customtkinter.CTkImage(light_image=self._default_light_image, dark_image=self._default_dark_image, size=(250, 300)))
            self._cover_art_display_label.configure(text='')
```
カバーアートを含まないオーディオファイルに対して、代替画像を表示するコードです。
CTkLabelで作成したラベルself._cover_art_display_labelに、画像customtkinter.CTkImage(...)を表示しています。画像を表示するときは、CTkLabelではなくtkinter.Canvasあるいはcutomtkinter.CTkCanvasに表示することが多いと思いますが、今回はこのようにしました。cutomtkinterは外観に関してダークモードとライトモードの切り替えが容易であり、CTkImageを使えばそれぞれのモードに対応した画像も自動的に切り替えてくれるからです。

### オーディオの再生
```python: simple_audio_player_ver0.1.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)
            self._audio.rewind()
            while True:
                data = self._audio.read_frames(CHUNK_SIZE)
                if len(data) == 0:
                    self._audio.rewind()
                    break
                stream.write(data)

            p.terminate()
        except:
            print('Error: cannot play the audio')
            return
```
オーディオファイルの再生に、[PyAudio](https://people.csail.mit.edu/hubert/pyaudio/)を使用しました。
再生中にイベントループが停止するため、上記の部分をマルチスレッド処理あるいはマルチプロセス処理させる必要があります。

### ファイルダイアログ
```python: simple_audio_player_ver0.1.py
input_path = customtkinter.filedialog.askopenfilename(filetypes=[('Audio File', '*.wav')])
```
customtkinterの部分をtkとしても全く同じです。同様の関数を使用すれば、フォルダ単位の入力や複数のファイル入力にも対応できます。

## 備考
* オーディオファイルはWAVだけに対応
    * MP3などの他のフォーマットは次回以降に対応
* WAVのメタ情報にそもそもカバーアートが含まれていないため、代替画像を表示
    * プレイヤー側が対応していれば、アートワークを含めることも可能
* WAVのメタ情報を簡単に取得するPythonパッケージがなかったため、代替テキストを表示
    * [mutagen](https://github.com/mutagen-io/mutagen)では取得できなかった
    * [tinytag](https://github.com/tinytag/tinytag)では取得できたが、日本語で文字化けが発生した
    * 時間があればWAVのメタ情報をパースするPythonコードを実装する
* threadingあるいはmultiprocessingを使用していないため、オーディオを再生中にイベントループの処理が止まる。つまり、ポーズボタンを押しても停止させることができず、再生ボタン以外は意味をなしていない。強制終了以外に、アプリを終了すことすらできない
    * 次回以降に対応

## おわりに
次回に続きます。
Qiitaの作法やソースコードに関するアドバイス、質問などがありましたらコメント欄までお願いします。