見出し画像

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

にある?ようだけど、もはや全く手に負えない領域・・・

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