2024-07-21

QMediaPlayer によるサウンドプレーヤー ~ PySide6

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。

本ブログ記事 [1] では、QSoundEffect クラスの使い方を覚えようと、シンプルな Wav Player なるものを作ってみました。しかし、シンプルなだけあって、再生の進捗を表示できないのが不満です。

そこで、同じようなサンプルを QMediaPlayer クラスを利用して作ってみました。今回は再生の進捗をモニターできるようにして、更に MP3 のファイルを選択できるようにしました。

下記の OS 環境で動作確認をしています。

RHEL 9.4 x86_64
Python 3.12.1
PySide6 6.7.2

サンプルを以下に示しました。今回も 200 行を越える長めのサンプルになってしまいました。

qt_mediaplayer_sound.py
import sys
from PySide6.QtCore import QUrl, Signal
from PySide6.QtGui import QIcon
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
from PySide6.QtWidgets import (
QApplication,
QDial,
QFileDialog,
QLineEdit,
QMainWindow,
QPlainTextEdit,
QProgressBar,
QStatusBar,
QStyle,
QToolBar,
QToolButton,
)
def get_icon(parent, name: str) -> QIcon:
pixmap = getattr(QStyle.StandardPixmap, name)
icon = parent.style().standardIcon(pixmap)
return icon
class MyToolBar(QToolBar):
soundSelected = Signal(str)
soundPlay = Signal()
soundStop = Signal()
soundVolume = Signal(float)
def __init__(self):
super().__init__()
but_folder = QToolButton()
but_folder.setToolTip('Choose sound file.')
ico_folder = get_icon(self, 'SP_DirIcon')
but_folder.setIcon(ico_folder)
but_folder.clicked.connect(self.file_dialog)
self.addWidget(but_folder)
self.but_play = but_play = QToolButton()
but_play.setToolTip('Start playing sound file.')
ico_play = get_icon(self, 'SP_MediaPlay')
but_play.setIcon(ico_play)
but_play.setEnabled(False)
but_play.clicked.connect(self.wav_play)
self.addWidget(but_play)
self.but_stop = but_stop = QToolButton()
but_stop.setToolTip('Stop playing sound file.')
ico_stop = get_icon(self, 'SP_MediaStop')
but_stop.setIcon(ico_stop)
but_stop.setEnabled(False)
but_stop.clicked.connect(self.wav_stop)
self.addWidget(but_stop)
self.addSeparator()
self.entry = entry = QLineEdit()
entry.setStyleSheet("""
QLineEdit {margin-left: 5; padding: 0 5 0 5;}
QLineEdit:disabled {background-color: white;}
""")
entry.setEnabled(False)
self.addWidget(entry)
self.dial = dial = QDial()
dial.setToolTip('Adjust sound volume.')
dial.setFixedSize(32, 32)
dial.setMinimum(0)
dial.setMaximum(100)
dial.setValue(25)
dial.valueChanged.connect(self.change_dial)
self.addWidget(dial)
def change_dial(self, value: int):
self.soundVolume.emit(value / 100.)
def file_dialog(self):
dialog = QFileDialog()
dialog.setNameFilter('Sound files (*.wav *.mp3)')
if dialog.exec():
filename = dialog.selectedFiles()[0]
self.but_play.setEnabled(True)
self.entry.setText(filename)
self.soundSelected.emit(filename)
def getVolume(self) -> float:
return self.dial.value() / 100.
def wav_play(self):
self.playStart()
self.soundPlay.emit()
def wav_stop(self):
self.playEnd()
self.soundStop.emit()
def playStart(self):
self.but_play.setEnabled(False)
self.but_stop.setEnabled(True)
def playEnd(self):
self.but_play.setEnabled(True)
self.but_stop.setEnabled(False)
class MyStatusBar(QStatusBar):
def __init__(self):
super().__init__()
self.progress = progress = QProgressBar()
self.addWidget(progress, stretch=True)
def clearProgress(self):
self.setProgress(0)
def setDuration(self, maximum: int):
self.progress.setRange(0, maximum)
def setProgress(self, progress: int):
self.progress.setValue(progress)
class MySoundPlayer(QMainWindow):
def __init__(self):
super().__init__()
icon_win = get_icon(self, 'SP_TitleBarMenuButton')
self.setWindowIcon(icon_win)
self.setWindowTitle('Sound Player')
self.toolbar = toolbar = MyToolBar()
toolbar.soundSelected.connect(self.source_selected)
toolbar.soundPlay.connect(self.sound_play)
toolbar.soundStop.connect(self.sound_stop)
toolbar.soundVolume.connect(self.set_volume)
self.addToolBar(toolbar)
self.statusbar = statusbar = MyStatusBar()
self.setStatusBar(statusbar)
self.pte = pte = QPlainTextEdit()
pte.setReadOnly(True)
pte.setStyleSheet('QPlainTextEdit {background-color: white;}')
self.setCentralWidget(pte)
self.output = output = QAudioOutput()
output.setVolume(toolbar.getVolume())
output.volumeChanged.connect(self.volume_changed)
self.player = player = QMediaPlayer()
player.setAudioOutput(output)
player.durationChanged.connect(self.duration_changed)
player.positionChanged.connect(self.position_changed)
player.playbackStateChanged.connect(self.playback_state_changed)
player.sourceChanged.connect(self.source_changed)
def add_msg(self, msg: str):
scr = self.pte.verticalScrollBar()
scr_at_bottom = (scr.value() >= (scr.maximum() - 4))
scr_prev_value = scr.value()
self.pte.insertPlainText('%s\n' % msg)
if scr_at_bottom:
self.pte.ensureCursorVisible()
else:
self.pte.verticalScrollBar().setValue(scr_prev_value)
def duration_changed(self, duration: int):
self.statusbar.setDuration(duration)
def position_changed(self, position: int):
self.statusbar.setProgress(position)
def playback_state_changed(self, status: QMediaPlayer.PlaybackState):
if status == QMediaPlayer.PlaybackState.PausedState:
self.add_msg('Paused')
elif status == QMediaPlayer.PlaybackState.PlayingState:
self.add_msg('Playing')
elif status == QMediaPlayer.PlaybackState.StoppedState:
self.statusbar.clearProgress()
self.add_msg('Stopped')
else:
self.add_msg('Unknown status')
def set_volume(self, volume: float):
self.output.setVolume(volume)
def source_changed(self):
qurl: QUrl = self.player.source()
self.add_msg('Sound file: %s' % qurl.fileName())
def source_selected(self, file: str):
self.player.setSource(QUrl.fromLocalFile(file))
def sound_play(self):
self.player.play()
def sound_stop(self):
self.player.stop()
def volume_changed(self, volume: float):
self.add_msg('Volume = %.2f' % volume)
def main():
app = QApplication(sys.argv)
ex = MySoundPlayer()
ex.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

サンプルの実行例を下記に示しました。

qt_mediaplayer_sound.py の実行例

Amazon.co.jp から購入・ダウンロードした MP3 ファイルを再生してみました。

サンプルの説明

サウンド・ファイルを読み込み、PLAY/STOP したり、ボリューム調整をする操作は、すべて QToolBar クラスを継承した MyToolBar クラスにまとめています。QSoundEffect を利用したサンプル [1]  で使用したクラスとほぼ同じですが、WAV ファイルの他に MP3 ファイルも選択できるようにしました。

class MyToolBar(QToolBar):
    soundSelected = Signal(str)
    soundPlay = Signal()
    soundStop = Signal()
    soundVolume = Signal(float)
 
    def __init__(self):
        super().__init__()
 
        but_folder = QToolButton()
        but_folder.setToolTip('Choose sound file.')
        ico_folder = get_icon(self, 'SP_DirIcon')
        but_folder.setIcon(ico_folder)
        but_folder.clicked.connect(self.file_dialog)
        self.addWidget(but_folder)
 
        (途中省略)
 
    def file_dialog(self):
        dialog = QFileDialog()
        dialog.setNameFilter('Sound files (*.wav *.mp3)')
        if dialog.exec():
            filename = dialog.selectedFiles()[0]
            self.but_play.setEnabled(True)
            self.entry.setText(filename)
            self.soundSelected.emit(filename)
 
        (以下省略)

QStatusBar クラスを継承した MyStatusBar を用意して、QProgressBar クラスのインスタンスで再生時間の進捗を表示するようにしています。

class MyStatusBar(QStatusBar):
    def __init__(self):
        super().__init__()
        self.progress = progress = QProgressBar()
        self.addWidget(progress, stretch=True)
 
    def clearProgress(self):
        self.setProgress(0)
 
    def setDuration(self, maximum: int):
        self.progress.setRange(0, maximum)
 
    def setProgress(self, progress: int):
        self.progress.setValue(progress)

QMediaPlayer のインスタンス self.player を扱うのは、QMainWindow クラスを継承した MySoundPlayer クラスです。QSoundEffect を利用したサンプル [1]  と同様に、操作イベントのログ表示もしています。

class MySoundPlayer(QMainWindow):
    def __init__(self):
        super().__init__()
        icon_win = get_icon(self, 'SP_TitleBarMenuButton')
        self.setWindowIcon(icon_win)
        self.setWindowTitle('Sound Player')
 
        self.toolbar = toolbar = MyToolBar()
        toolbar.soundSelected.connect(self.source_selected)
        toolbar.soundPlay.connect(self.sound_play)
        toolbar.soundStop.connect(self.sound_stop)
        toolbar.soundVolume.connect(self.set_volume)
        self.addToolBar(toolbar)
 
        self.statusbar = statusbar = MyStatusBar()
        self.setStatusBar(statusbar)
 
        self.pte = pte = QPlainTextEdit()
        pte.setReadOnly(True)
        pte.setStyleSheet('QPlainTextEdit {background-color: white;}')
        self.setCentralWidget(pte)
 
        self.output = output = QAudioOutput()
        output.setVolume(toolbar.getVolume())
        output.volumeChanged.connect(self.volume_changed)
 
        self.player = player = QMediaPlayer()
        player.setAudioOutput(output)
        player.durationChanged.connect(self.duration_changed)
        player.positionChanged.connect(self.position_changed)
        player.playbackStateChanged.connect(self.playback_state_changed)
        player.sourceChanged.connect(self.source_changed)
         
        (以下省略)

QMediaPlayer クラスでは、オーディオの出力先に QAudioOutput クラスのインスタンスを指定しています。ここが QSoundEffect クラスの使い方との大きな違いです。なお、ボリューム(音量)の調節は QAudioOutput クラス側でおこないます。

QSoundEffect を利用したサンプル [1]  と同様に、PySide6 に組み込まれている標準のビットマップイメージ (Pixmap) を利用するために、名前を指定してアイコンを取得する関数を冒頭に記載しています。

def get_icon(parent, name: str) -> QIcon:
    pixmap = getattr(QStyle.StandardPixmap, name)
    icon = parent.style().standardIcon(pixmap)
    return icon

今回はシンプルさを優先して、ひとつのファイルを再生するだけの仕様にしましたが、自分用にカスタマイズした音楽プレーヤーアプリを作ることができるでしょう。

参考サイト

  1. bitWalk's: QSoundEffect を利用した Wav Player ~ PySide6 [2024-07-20]
  2. QMediaPlayer - Qt for Python
  3. QAudioOutput - Qt for Python

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



0 件のコメント: