前回に引き続きpythonのオーディオプレイヤーの練習です。
時間をコントロールして表示するほうほうがわかったので、
今度はレベルメーターをつけてみます。
オーディオレベルの取得
pygameのオーディオから取得できるパラメータを見てみましたが、
現在の再生秒数(ミリ秒)くらいしか取れないようです。
となったので、新たに音量レベルを取得するロジックを考える必要がありました。
→ 古参(?)の音声解析ができるlibrosaパッケージがpythonにはありますので、
これを使って、変換された波形データにアクセスして
レベル計算すれば良さそうです。
import librosa
audio, sr = librosa.load(file_path, dtype=np.float64)
サンプリングレートも、波形ファイルの設定でとれますし、
バイナリとしてアクセスしなくてよくて最大振幅1のndarrayとして取れるのでらくちんです。ありがたや。
これでレベル計算していきます
# 現在の再生位置を取得
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を用意して、そこに描画するようにしました。
# 描画領域の準備
self.level_meter_canvas = tk.Canvas(self.tk_root, width=400, height=50, bg="black")
self.level_meter_canvas.pack(pady=5)
# 描画更新
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)
全体のプログラムでは次のようになりました。
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()
実行結果
例によってスクショじゃわかりませんが、レベルメーターも動くようになりました。
やっぱり30FPSはほしいですね。