見出し画像

音源再生ライブラリ書き換え(7)

(※本内容は、python初心者による試行錯誤の過程の記録です。整理された内容ではありません。)


動作確認

前回書き換えたこのライブラリファイル

の「解読」の成果を盛り込んで、とりあえずテストコードと組み合わせてみました。例によって動作状況を動画にしました。

で、ご覧の通り一応の動作はしているのですが、再生位置変更などで「もっさり」していて、どうも気に入らない。(このコードは末尾に添付します)

三週間ちょっと前

このときは、スライダーでの再生位置の変更も「超軽かった」。これと同じレベルにしたい。

ふりかえり

そもそも・・

 そもそもなんで、「軽かった以前のやり方」を止めて、今、四苦八苦しているのか忘れちゃったよ(^_^;)
 三週間前からの自分のnoteを読み返します。

三週間前

・・学習者のタイパを考えて早回し再生ができるようにしたいと思っていたのね。

・・・このころは、snykのスコアを参考に、librosa+sounddeviceで進めようとしていた。ところが

・・・librosaはどうも処理に時間がかかるようだから、やっぱりpydub+sounddeviceにしようと方針変更。

その後再生機能としてはpygameも検証して、速いことを確認しているけど

結論
ただ再生するだけなら(1.5倍速再生とかしなければ)pygame最強。
加工するならpydub +sounddevice 。

と書いている。なぜか、pydub+pygame はできないものと先入観を持っていたようです。

二週間前

そして

 前回まで分かったこと。

 pygameの読み込みの速さは群を抜いており、魅力ですが、再生速度の変更はできない。そういう機能も今後使っていこうとすると、若干読み込みが遅くなってもpydub +sounddeviceの組み合わせがよさそうです。

音源再生ライブラリ書き換え(1)

とpydub +sounddeviceに囚われて今日にいたるというわけでした(笑) いったい何故頑なにそう思い込んでしまったのか?

pydub+pygameでいいじゃん

pydubの加工データをpygameに渡す例 (Copilot先生との対話の中から)

import io
import time

import pygame
from pydub import AudioSegment

# サンプルオーディオデータの作成
audio = AudioSegment.from_file("test.mp3")

# オーディオの加工(例:音量を2倍にする)
processed_audio = audio + 6  # +6dBで音量を2倍にする

# メモリ上のバイトバッファにオーディオを書き出す
audio_buffer = io.BytesIO()
processed_audio.export(audio_buffer, format="wav")
audio_buffer.seek(0)  # バッファの先頭に移動

# pygameでオーディオを再生
pygame.mixer.init()
pygame.mixer.music.load(audio_buffer)
pygame.mixer.music.play(start=10)
time.sleep(10)

いやはや、問題なく再生できるじゃん。それも、ファイルに書き出さずに。

またまた方針変更。今後は、pydub+pygameで作るよ。

冒頭でテストしたもっさりコード

「試行錯誤の記録」として、冒頭でテストしたpydub +sounddeviceのテストコードを残しておきます。まあ、今回で一区切りです(笑)
 そもそもpauseの処理を自前で実装するなんてことをしている時点で、「筋が悪いぞ」、と、気づくべきなのでした。

from threading import Thread

import customtkinter as ctk
import numpy as np
import sounddevice as sd
from pydub import AudioSegment


class MyPlayer:
    def __init__(self):
        super().__init__()
        self.filename = None
        self.audio = None
        self.audio_data = None  # オーディオデータ
        self.sr = None  # サンプルレート
        self.is_playing = False
        self.is_paused = False
        self.playstarttime = None
        self.pause_time = 0
        self.pause_sample = 0

    # ファイル読み込み
    def set_mp3file(self, filename):
        self.filename = filename
        self.audio = AudioSegment.from_mp3(filename)
        self.audio_data = np.array(self.audio.get_array_of_samples())
        if self.audio.channels == 2:
            self.audio_data = self.audio_data.reshape((-1, 2))
        self.sr = self.audio.frame_rate

    def play_audio(self):
        if self.is_paused:
            start_sample = self.pause_sample
            self.is_paused = False
        else:
            start_sample = 0
        self.is_playing = True
        sd.play(self.audio_data[start_sample:], samplerate=self.sr)
        sd.wait()
        self.is_playing = False

    def play(self):
        if not self.is_playing:
            self.playstarttime = sd.Stream().time - self.pause_time
            Thread(target=self.play_audio).start()

    def pause(self):
        if self.is_playing:
            self.is_paused = True
            self.pause_time = sd.Stream().time - self.playstarttime
            self.pause_sample = int(self.pause_time * self.sr)
            sd.stop()

    def stop(self):
        if self.is_playing or self.is_paused:
            sd.stop()
            self.is_playing = False
            self.is_paused = False
            self.pause_time = 0
            self.pause_sample = 0
            self.playstarttime = None

    @property
    def paused(self):
        # return self.playback.paused
        return self.is_paused

    @property
    def duration(self):
        return self.audio.duration_seconds

    @property
    def pos(self):
        if self.is_playing:
            return sd.Stream().time - self.playstarttime
        elif self.is_paused:
            return self.pause_time
        else:
            return 0

    @pos.setter
    def pos(self, seekpos):
        if self.is_playing:
            self.pause()
            self.pause_time = seekpos
            self.pause_sample = int(self.pause_time * self.sr)
            self.play()
        elif self.is_paused:
            self.pause_time = seekpos
            self.pause_sample = int(self.pause_time * self.sr)


class TTimer(ctk.CTkFrame):
    def __init__(self, parent, task_function, interval=1000, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.parent = parent
        self.task_function = task_function
        self.interval = interval
        self.enabled = True  # 初期状態を有効に設定
        self.background_task()

    def background_task(self):
        if self.enabled:
            self.task_function()
        self.parent.after(
            self.interval, self.background_task
        )  # 動的に間隔を変更可能

    def set_interval(self, new_interval):
        self.interval = new_interval

    def set_enabled(self, enabled):
        self.enabled = enabled


if __name__ == "__main__":

    class App(ctk.CTk):

        def __init__(self, title):

            super().__init__()
            self.title(title)

            # player
            self.player = MyPlayer()

            # 再生する曲は当面固定
            self.player.set_mp3file("test2.mp3")

            # widget
            # 再生ボタン
            ctk.CTkButton(self, text="再生", command=self.on_play_click).pack(
                padx=50, pady=10
            )

            # ポーズボタン
            self.pause_button = ctk.CTkButton(
                self, text="一時停止:解除", command=self.on_pause_click
            )
            self.pause_button.pack(padx=50, pady=10)

            # 停止ボタン
            ctk.CTkButton(self, text="停止", command=self.player.stop).pack(
                padx=50, pady=10
            )

            # 全体長さ表示
            self.label_duration = ctk.CTkLabel(self, text="曲の長さ")
            self.label_duration.pack()

            # 再生位置表示
            self.label_currpos = ctk.CTkLabel(self, text="再生位置")
            self.label_currpos.pack()

            # プログレスバーWidget⇒スライダーWidget
            self.slider_pos = ctk.CTkSlider(
                self,
                width=200,
                height=8,
                from_=0,
                to=1.0,
                command=self.slider_pos_event,
            )
            self.slider_pos.pack(padx=50, pady=10)

            # 表示更新用
            self.timer1 = TTimer(self, self.task_updatedisp, interval=1000)

            # mainloop
            self.protocol("WM_DELETE_WINDOW", self.on_closing)
            self.mainloop()

        def on_play_click(self):
            self.player.play()

        def slider_pos_event(self, value):
            if self.player.is_playing:
                self.player.pause()
                self.player.pos = self.player.duration * value
                self.player.play()
            elif self.player.is_paused:
                self.player.pos = self.player.duration * value

        def on_pause_click(self):
            if self.player.is_playing:
                self.player.pause()
            elif self.player.is_paused:
                self.player.play()

            if self.player.paused:
                self.pause_button.configure(text="一時停止:停止中")
            else:
                self.pause_button.configure(text="一時停止:解除")

        def on_closing(self):
            # 閉じる前にやること
            self.player.stop()
            # 閉じる
            self.destroy()

        def task_updatedisp(self):
            duration = self.player.duration
            self.label_duration.configure(text=f"長さ:{duration:.1f}")
            curr_pos = self.player.pos
            self.label_currpos.configure(text=f"位置:{curr_pos:.1f}")
            self.slider_pos.set(curr_pos / duration)

    App("MyMP3Player20241112")



いいなと思ったら応援しよう!