PythonでMP3再生(非同期手法の試行錯誤2)
・・・などの続きです。
これまでの経緯
こうしたい・・
a) time.sleep() を使いたくない。
b) playsound3のplaysound(block=true)のようなblockできる関数だけではなく、just_playback のplayのような
(今のところ、time.sleep() もしくは、while playback.active: pass
を書かないとうまく鳴ってくれないような)関数も使いこなしたい。
という、一見相矛盾する要望を「非同期」手法使ったらどうにかならないかなと試行錯誤し始めたところでした。
(加えて c)Tkinter使ってGUIアプリにしたい、もあります。)
情報源は自分のnote?
で、昨晩は、「きっとどこかに、これらにあてはまる『いい感じ』のサンプルコードが公開されているんじゃないの?」と思いCopilot先生に聞いた。そうしたら、三日前の自分のnoteを挙げてきた・・という笑い話。
でも実は、「引用された自分のnote」の中で書いていたのは「非同期」と「(blockが有効な)playsound」とを組み合わせたコードまででした。
「非同期」と「(block機能が無い)just_playback のplay」との組み合わせは「次の段階のテーマ」だな、と思っていたのです。それが
いやいや、答え出ちゃっている。
そして、「次の次の段階のテーマだな」と思っていた「Tkinter使ってGUIアプリに」も
コードができちゃっている。
内心まで見透かされたかのよう。なんか「ゾクっ」と鳥肌がたちます。下書きしていた次の原稿は、不要になっちゃった。
試してみます
答えをもらっちゃったので、試してみます。
1. just-playbackライブラリを使った例
import asyncio
from just_playback import Playback
async def play_mp3():
playback = Playback()
playback.load_file("test.mp3")
playback.play()
while playback.active:
await asyncio.sleep(1)
asyncio.run(play_mp3())
はい、こちらは問題なく全曲最後まで再生してくれましたよ。すごいですね。
2.Tkinterと組み合わせた例
import asyncio
import tkinter as tk
from just_playback import Playback
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("MP3 Player")
self.geometry("300x100")
play_button = tk.Button(self, text="Play", command=self.play_mp3)
play_button.pack(pady=20)
def play_mp3(self):
asyncio.create_task(self.async_play())
async def async_play(self):
playback = Playback()
playback.load_file("test.mp3")
playback.play()
while playback.active:
await asyncio.sleep(1)
app = App()
asyncio.run(app.mainloop())
Playをクリックすると
Exception in Tkinter callback
Traceback (most recent call last):
File "・・・", line 1967, in __call__
return self.func(*args)
^^^^^^^^^^^^^^^^
File "・・・", line 16, in play_mp3
asyncio.create_task(self.async_play())
File "・・・", line 417, in create_task
loop = events.get_running_loop()
^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: no running event loop
ほっほー、エラーになりました。なぜかちょっとホッとしました(笑)
Copilot先生、エラーになったよ(どや顔)
完成版へ
まあ、それから要求や、聞き方を変えたりしながらやり取りを繰り返し、CustomTkinterに変えたり、threadingも導入したり、ようやく、当初要望を全部入れつつ、コードの記述も納得できる内容になりました。
途中経過は省いて結論がこちら。asyncio もthreadingも両方使っちゃってlambda関数とか出てきていますが、総合的には素直でわかりやすいコードだと思います。
import asyncio
from threading import Thread
import customtkinter as ctk
from just_playback import Playback
class App(ctk.CTk):
def __init__(self, title):
# main window
super().__init__()
self.title(title)
# widget
ctk.CTkButton(self, text="再生", command=self.on_button_click).pack(
padx=50, pady=10
)
# mainloop
self.mainloop()
def on_button_click(self):
# 非同期関数を別スレッドで実行
Thread(target=lambda: asyncio.run(self.play_sound())).start()
# 非同期で音を再生する関数
async def play_sound(self):
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.playmp3)
def playmp3(self):
print("playmp3")
playback = Playback()
playback.load_file("test.mp3")
playback.play()
while playback.active: # 再生が終了するまで待機
pass
App("MyMP3Player")
while playback.active:
pass
はありますけど、イベントループの中に入れてしまえば、重くならない(GUIを妨害しない)ようなので、これなら納得です。
実行しますと
非同期なのでGUIも全く固まらず「余裕」を感じさせてくれます(笑)
問題は「再生中」にさらに「再生」をクリックしたときに別スレッドでもうひとつの再生が始まってしまうので音が重なってしまうこと。それを避けるためには、今後ここでSingletonパターンとか使えばいいのだろうか?
まあ、今回の「完成版」を基本にして、ポーズとか、レジュームとか停止とかのボタン・機能を追加していく道筋が見えたような気がします。