音源再生ライブラリ書き換え(7)
(※本内容は、python初心者による試行錯誤の過程の記録です。整理された内容ではありません。)
動作確認
前回書き換えたこのライブラリファイル
の「解読」の成果を盛り込んで、とりあえずテストコードと組み合わせてみました。例によって動作状況を動画にしました。
で、ご覧の通り一応の動作はしているのですが、再生位置変更などで「もっさり」していて、どうも気に入らない。(このコードは末尾に添付します)
三週間ちょっと前
このときは、スライダーでの再生位置の変更も「超軽かった」。これと同じレベルにしたい。
ふりかえり
そもそも・・
そもそもなんで、「軽かった以前のやり方」を止めて、今、四苦八苦しているのか忘れちゃったよ(^_^;)
三週間前からの自分のnoteを読み返します。
三週間前
・・学習者のタイパを考えて早回し再生ができるようにしたいと思っていたのね。
・・・このころは、snykのスコアを参考に、librosa+sounddeviceで進めようとしていた。ところが
・・・librosaはどうも処理に時間がかかるようだから、やっぱりpydub+sounddeviceにしようと方針変更。
その後再生機能としてはpygameも検証して、速いことを確認しているけど
と書いている。なぜか、pydub+pygame はできないものと先入観を持っていたようです。
二週間前
そして
と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")