0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オーディオプレイヤーを作る DAY5

Last updated at Posted at 2024-12-08

はじめに

Pythonを使ったオーディオプレイヤーの実装過程を順次投稿しています。
前回までの進捗は以下の通りです。

今回実装した内容

  • GUI
    • ボリュームコントロールボタン
      • CTkToplevelを使って、ボリュームを調整するスライドバーをポップアップ表示
    • ループ再生のオンオフボタン
    • プログレスバーの再実装
      • CTkSliderからCTkProgressBarに変更
        • CTkSliderのマウスイベントをバインドできなかったため
  • 機能
    • ボリュームコントロール
      • 人間の聴覚特性を考慮して、指数関数的に調整
    • ループ再生のオンオフ
    • プログレスバーのマウスイベントに、再生を一時停止する機能をバインド
      • 再生中にプログレスバーをマウス操作するとフリッカーが発生していたため、マウスイベントの発生中、再生を一時停止する機能を追加
    • マルチスレッド処理の見直し
      • 単純に仕様が悪く、デバッグも困難だったため
      • 複数のスレッドがオーディオ波形を同時に更新する場合があったため、ロックを追加
        • matplotlibがスレッドセーフではない?と思われるが、ロックがあると問題なく動作している(ようにみえる)

ソースコード

環境

Windows11
Python3.12.7

動作イメージ

音楽 BGMer

ソースコードの補足

マルチスレッド処理

イベントループを処理するスレッド以外に、以下の4つのスレッドを非同期で動かしています。

  • オーディオを再生するスレッド
  • プログレスバーとタイム表示を更新する(ためのイベントを発生させる)スレッド
  • オーディオ波形を更新するスレッド
  • 再生/停止ボタンを切り替えるスレッド

スレッドを協調して動作させるために、オーディオプレイヤーの状態(State)をEnumクラスAudioPlayerStateを使って管理していました。しかしながら、Stateが色々な場所・タイミングで非同期に更新されるためバグがたびたび発生していました。そこで、オーディオプレイヤーへの命令(Instraction)を管理するためにEnumクラスAudioPlayerInstructionを新たに追加。メインスレッドがGUI操作に応じてInstructionを更新し、オーディオを再生するスレッドがInstructionに応じた処理を行いStateを更新、その他のスレッドはStateに応じた処理を行うように仕様を変更しました。以前は、複数のスレッドがStateを非同期に更新していたためデバッグが困難でした。

audio.py
class AudioPlayerState(IntEnum):
    PLAYING = auto()
    POSED = auto()
    READY = auto()
    NOT_READY = auto()
    CLOSING = auto()
    CLOSED = auto()

class AudioPlayerInstruction(IntEnum):
    PLAY = auto()
    POSE = auto()
    CLOSE = auto()
    STOP = auto()

ボリュームコントロール

人間の聴覚は音の強さの対数に比例すると言われているので、ボリュームを指数関数的に調整する。リニアの場合よりも明らかに自然な変化をする。
実装がリニアのままだった…

audio.py
    def __controll_volume(self, data:bytes):
        if data == []:
            return data
        
        volume = self.volume
        if volume == 100:
            return data

        volume =  0 if volume == 0 else 10 ** (volume / 50)
        nchannels = self.__audio.nchannels
        volume = self.volume
        channels = [np.frombuffer(data, dtype=np.int16)[i::nchannels] for i in range(nchannels)]
        channels = [channel.astype(dtype=np.float32) * volume / 100 for channel in channels]
        channels = [channel.astype(dtype=np.int16) for channel in channels]
        
        return np.column_stack(channels).tobytes()

備考

  • オーディオファイルはWAVとMP3だけに対応
    • 他のフォーマットは次回以降に対応
  • マルチプロセス化を検討中
    • 試しにマルチプロセス化の実装を行ったが、信頼性に問題があるのでペンディング

おわりに

次回に続きます。
Qiitaの作法やソースコードに関するアドバイス、質問などがありましたらコメント欄までお願いします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?