python の練習3。 オーディプレイヤーにレベルメーターをつける

前回に引き続きpythonのオーディオプレイヤーの練習です。

https://qiita.com/mebi/items/72ae43b3c33e74cce09d


時間をコントロールして表示するほうほうがわかったので、
今度はレベルメーターをつけてみます。

---

## オーディオレベルの取得

pygameのオーディオから取得できるパラメータを見てみましたが、
現在の再生秒数（ミリ秒）くらいしか取れないようです。

https://westplain.sakura.ne.jp/translate/pygame/Music.cgi

となったので、新たに音量レベルを取得するロジックを考える必要がありました。

→　古参（？）の音声解析ができるlibrosaパッケージがpythonにはありますので、
これを使って、変換された波形データにアクセスして
レベル計算すれば良さそうです。

---

```python
import librosa

audio, sr = librosa.load(file_path, dtype=np.float64) 
```

サンプリングレートも、波形ファイルの設定でとれますし、
バイナリとしてアクセスしなくてよくて最大振幅１のndarrayとして取れるのでらくちんです。ありがたや。

これでレベル計算していきます

```python
        # 現在の再生位置を取得
        start_sec = pygame.mixer.music.get_pos() / 1000.0  # ミリ秒から秒に変換
        start_index = int(start_sec * self.audio_sr)
        end_index = int((start_sec + search_len_sec) * self.audio_sr)

        if end_index > len(self.audio_data):
            end_index = len(self.audio_data)

        # サーチ範囲の平均として取得
        current_audio_segment = self.audio_data[start_index:end_index]
        level = np.abs(current_audio_segment).mean()
```

こういうのってdB値を出すのがいいのか、RMS値を出すのがいいのか
あんまり知見なくてよく分からんのですが、
オーディオプレイヤとして仕上げる方向優先として、雑に波形振幅の平均をレベルとして出すようにしました。

描画には、前回と同様に、Threadで非同期に実行してレベルメーターを更新します。
描画はtk.Canvasを用意して、そこに描画するようにしました。

```python
    # 描画領域の準備
    self.level_meter_canvas = tk.Canvas(self.tk_root, width=400, height=50, bg="black")
    self.level_meter_canvas.pack(pady=5)
```

```python
    # 描画更新
    if level < 0.5:
        lm_color = "green"
    elif level < 0.8:
        lm_color = "yellow"
    else:
        lm_color = "red"

    self.level_meter_canvas.delete("all")
    self.level_meter_canvas.create_rectangle(0, 0, 400 * level, 50, fill=lm_color)
```


---

全体のプログラムでは次のようになりました。

```python
import pygame
import tkinter as tk
from tkinter import filedialog
from threading import Thread
import time
import numpy as np
import librosa

class AudioPlayer:
    TIME_INIT_STR = "--:--"
    LV_METER_UPDATE_SEC = 0.033

    def __init__(self, tk_root):
        self.tk_root = tk_root
        self.tk_root.title("Audio Player")

        self.tk_root.geometry("480x320")
        self.tk_root.resizable(False, False)
        
        self.init_pygame()
        self.create_widgets()

    def init_pygame(self):
        self.audio_file_path = ""
        pygame.mixer.init()
        self.audio_data = None
        self.audio_sr = None

    def load_audio(self):
        self.audio_file_path = filedialog.askopenfilename(filetypes=[("Audio Files", "*.mp3")])
        if self.audio_file_path:
            self.stop_audio()
            self.load_audio_data(self.audio_file_path)
            pygame.mixer.music.load(self.audio_file_path)
            self.status_label.config(text=f"Loaded: {self.audio_file_path}")
            self.elapsed_time = 0
            self.update_time_label()

    def load_audio_data(self, file_path):
        self.audio_data, self.audio_sr = librosa.load(file_path, dtype=np.float64) 

    def play_audio(self):
        if len(self.audio_file_path) > 0:
            pygame.mixer.music.play()
            audio_file_name = self.audio_file_path.split('/')[-1]
            self.status_label.config(text=f"Playing... {audio_file_name}")
            self.start_time_counter()
            self.start_level_meter()

    def stop_audio(self):
        pygame.mixer.music.stop()
        self.elapsed_time = 0
        self.status_label.config(text="Stopped")
        self.time_label.config(text=AudioPlayer.TIME_INIT_STR)
        self.level_meter_canvas.delete("all")

    def start_time_counter(self):
        def count_time():
            self.elapsed_time = 0
            while pygame.mixer.music.get_busy():
                self.update_time_label()
                time.sleep(1)
                self.elapsed_time += 1
            self.time_label.config(text=AudioPlayer.TIME_INIT_STR)

        Thread(target=count_time, daemon=True).start()

    def update_time_label(self):
        minutes, seconds = divmod(self.elapsed_time, 60)
        time_str = f"{minutes:02}:{seconds:02}"
        self.time_label.config(text=time_str)

    def create_widgets(self):
        load_button = tk.Button(self.tk_root, text="Load", command=self.load_audio)
        play_button = tk.Button(self.tk_root, text="Play", command=self.play_audio)
        stop_button = tk.Button(self.tk_root, text="Stop", command=self.stop_audio)
        
        load_button.pack(pady=5)
        play_button.pack(pady=5)
        stop_button.pack(pady=5)
        
        self.status_label = tk.Label(self.tk_root, text="No file loaded")
        self.status_label.pack(pady=5)

        self.time_label = tk.Label(self.tk_root, text=AudioPlayer.TIME_INIT_STR, font=("Helvetica", 16))
        self.time_label.pack(pady=5)
        
        self.level_meter_canvas = tk.Canvas(self.tk_root, width=400, height=50, bg="black")
        self.level_meter_canvas.pack(pady=5)

    def start_level_meter(self):
        def update_level():
            while pygame.mixer.music.get_busy():
                self.draw_level_meter(AudioPlayer.LV_METER_UPDATE_SEC)
                time.sleep(AudioPlayer.LV_METER_UPDATE_SEC)
            self.level_meter_canvas.delete("all")

        Thread(target=update_level, daemon=True).start()

    def draw_level_meter(self, search_len_sec):
        if self.audio_data is None:
            return

        # 現在の再生位置を取得
        start_sec = pygame.mixer.music.get_pos() / 1000.0  # ミリ秒から秒に変換
        start_index = int(start_sec * self.audio_sr)
        end_index = int((start_sec + search_len_sec) * self.audio_sr)

        if end_index > len(self.audio_data):
            end_index = len(self.audio_data)


        current_audio_segment = self.audio_data[start_index:end_index]
        level = np.abs(current_audio_segment).mean()

        if level < 0.5:
            lm_color = "green"
        elif level < 0.8:
            lm_color = "yellow"
        else:
            lm_color = "red"

        self.level_meter_canvas.delete("all")
        self.level_meter_canvas.create_rectangle(0, 0, 400 * level, 50, fill=lm_color)

if __name__ == '__main__':
    tk_root = tk.Tk()
    app = AudioPlayer(tk_root)
    tk_root.mainloop()

```


実行結果

![Screenshot from 2024-07-07 15-06-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3812996/84a1c003-ce1b-5022-c25d-f43128ae545c.png)


例によってスクショじゃわかりませんが、レベルメーターも動くようになりました。
やっぱり30FPSはほしいですね。