1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

前回に引き続き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()

実行結果

Screenshot from 2024-07-07 15-06-42.png

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

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?