PythonでMP3再生(非同期手法の試行錯誤4)
今回は、前回非同期処理にthreadingを使った部分に「async/await構文を適用したらどうなるのか」という検討をしたけど、「どうもうまく行かなかった」という記録です。
前回
では
from threading import Thread
を使って
>今の時点ではこれが最適解と思っております。
と書きました。
threadingかasyncioか
ただ、ちょっと気になる点があります。前回、非同期処理にthreading モジュールを使っていますがasyncioモジュールは使いませんでした。
以前読んだところでは、たしか、非同期処理に関して「従来より直感的な記述ができるようにした」のが「asyncioのasync/await構文」ということでした。ならば、これをうまくつかえば、「より良い記述」が可能なのではないか?
かつて自分は、asyncioとthreadingとを混ぜて書いたこともありました。そういえば、あのときって何か「おいしかった」のだろうか?
Copilot先生に聞く
そこで「他人のふり」をして第三者的立場から、Copilotに投げかけてみました。
なるほど、Customtkinterを継承しているAppクラスの記述で、initの最後で
>self.mainloop()
としていたのをクラスの外に出してupdateメソッドをループで回している。
# 非同期イベントループの設定
loop = asyncio.get_event_loop()
# tkinterメインループとasyncioイベントループを統合する
async def tkinter_loop(root):
while True:
root.update()
await asyncio.sleep(0.01)
app = App("MyMP3Player20241116")
loop.create_task(tkinter_loop(app))
loop.run_forever()
updateメソッドを繰り返すことで置き換えて、asyncioのイベントループの中で回すようにしている、ということかな?
やってみる
エラーが出て、Copilot先生に聞いて修正・・というのを繰り返してGUIの動作は見た目問題ないところまで来ました。ですが、ターミナル上で表示されるこういうエラーが解決しないのです。
invalid command name "2481525118976update"
while executing
"2481525118976update"
("after" script)
Copilot先生はいろいろ改善提案を出してくれて、それを反映して行ったらこんなコードに・・
import asyncio
import contextlib
import io
import customtkinter as ctk
import pygame
from pydub import AudioSegment
class MyPlayer:
def __init__(self):
super().__init__()
self.audio = None # pydub AudioSegment
self.mixer = pygame.mixer
self.mixer.init()
# ファイル読み込み
def set_mp3file(self, filename):
self.audio = AudioSegment.from_mp3(filename)
async def play_audio(self):
self.audio_buffer = io.BytesIO()
self.audio.export(self.audio_buffer, format="wav")
self.audio_buffer.seek(0)
self.mixer.music.load(self.audio_buffer)
self.mixer.music.play()
while self.mixer.music.get_busy():
await asyncio.sleep(0.2)
self.stop()
def play(self):
asyncio.create_task(self.play_audio())
def stop(self):
self.mixer.music.stop()
# テストコード
if __name__ == "__main__":
class App(ctk.CTk):
def __init__(self, title):
super().__init__()
self.title(title)
self.player = MyPlayer()
self.player.set_mp3file("zangyousenshi.mp3")
# widget
# 再生ボタン
ctk.CTkButton(self, text="再生", command=self.player.play).pack(
padx=50, pady=10
)
# 停止ボタン
ctk.CTkButton(self, text="停止", command=self.player.stop).pack(
padx=50, pady=10
)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.running = True
# self.mainloop()
def on_closing(self):
# 閉じる前にやること
self.player.stop()
self.running = False
self.quit()
self.update()
loop.stop()
# 閉じる
self.destroy()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def ctk_loop(root):
while root.running:
root.update()
await asyncio.sleep(0.01)
app = App("MyMP3Player20241117")
loop.create_task(ctk_loop(app))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
pending = asyncio.all_tasks(loop)
for task in pending:
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
loop.run_until_complete(task)
loop.close()
いろいろごてごてと修正コードがくっついて、それでもまだエラーが解決していない。どうも筋が悪そうなので、もう、ここまでにします。
前回やったthreadingを使う方法に戻って進めることにします。
追記
諦めが悪くて、tkinterのソースコードを探索
https://github.com/python/cpython/blob/3.13/Lib/tkinter/__init__.py
の2434行目に
class Tk(Misc, Wm):
というクラスがあって、そのinitで2459行目
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
とやっている。
_tkinterはC言語のmodule。
https://github.com/python/cpython/blob/main/Modules/_tkinter.c
にある?ようだけど、もはや全く手に負えない領域・・・