PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。
Wikipedia によると、wav を拡張子に持つファイルは、Microsoft とIBM により開発された RIFF waveform Audio Format (WAV) という音声ファイルフォーマットです。クリックやチャイムなど、効果音に利用されている比較的小さなサイズのファイルです。
PySide6 の QSoundEffect クラスは、マニュアル [1] によると、WAV ファイルのような低レイテンシな非圧縮のオーディオファイルを再生することができ、ユーザーのアクションに応答する「フィードバック 」タイプ(クリック音)のサウンド、例えば、仮想キーボードのクリック音、ポップアップダイアログの Yes / No の応答、あるいはゲームの効果音用途に適しているとあります。
さらに、低レイテンシが重要でない場合には、代わりに QMediaPlayer クラスの使用を検討してくださいともあります。
まずは QSoundEffect クラスの使い方を覚えようと、シンプルな Wav Player なるものを作ってみました。
下記の OS 環境で動作確認をしています。
![]() |
RHEL 9.4 | x86_64 |
Python | 3.12.1 | |
PySide6 | 6.7.2 |
なお、QSoundEffect クラスが含まれる QtMultimedia パッケージを利用するために、本 RHEL では追加で libva をインストールする必要がありました。
[bitwalk@rhel9 ~]$ rpm -qa | grep libva
libva-2.20.0-1.el9.x86_64
libva-devel-2.20.0-1.el9.x86_64
[bitwalk@rhel9 ~]$
サンプルを以下に示しました。ひとつにまとめたところ、200 行を越える長めのサンプルになってしまいました。
import sys | |
from PySide6.QtCore import Signal, QUrl | |
from PySide6.QtGui import QIcon | |
from PySide6.QtMultimedia import QSoundEffect | |
from PySide6.QtWidgets import ( | |
QApplication, | |
QDial, | |
QFileDialog, | |
QLineEdit, | |
QMainWindow, | |
QPlainTextEdit, | |
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): | |
wavSelected = Signal(str) | |
wavPlay = Signal() | |
wavStop = Signal() | |
wavVolume = Signal(float) | |
def __init__(self): | |
super().__init__() | |
but_folder = QToolButton() | |
but_folder.setToolTip('Choose wav 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 wav 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 wav 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.wavVolume.emit(value / 100.) | |
def file_dialog(self): | |
dialog = QFileDialog() | |
dialog.setNameFilter('wav file (*.wav)') | |
if dialog.exec(): | |
filename = dialog.selectedFiles()[0] | |
self.but_play.setEnabled(True) | |
self.entry.setText(filename) | |
self.wavSelected.emit(filename) | |
def getVolume(self) -> float: | |
return self.dial.value() / 100. | |
def wav_play(self): | |
self.playStart() | |
self.wavPlay.emit() | |
def wav_stop(self): | |
self.playEnd() | |
self.wavStop.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 MyWavPlayer(QMainWindow): | |
def __init__(self): | |
super().__init__() | |
icon_win = get_icon(self, 'SP_TitleBarMenuButton') | |
self.setWindowIcon(icon_win) | |
self.setWindowTitle('Wav Player') | |
self.effect = None | |
self.toolbar = toolbar = MyToolBar() | |
toolbar.wavSelected.connect(self.source_selected) | |
toolbar.wavPlay.connect(self.sound_play) | |
toolbar.wavStop.connect(self.sound_stop) | |
toolbar.wavVolume.connect(self.set_volume) | |
self.addToolBar(toolbar) | |
self.pte = pte = QPlainTextEdit() | |
pte.setReadOnly(True) | |
pte.setStyleSheet('QPlainTextEdit {background-color: white;}') | |
self.setCentralWidget(pte) | |
def add_msg(self, msg: str): | |
# Reference: | |
# https://stackoverflow.com/questions/14550146/qtextedit-scroll-down-automatically-only-if-the-scrollbar-is-at-the-bottom | |
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 create_sound_effect(self, wav_file: str): | |
self.effect = QSoundEffect() | |
self.effect.loopsRemainingChanged.connect(self.remaining_changed) | |
self.effect.sourceChanged.connect(self.source_changed) | |
self.effect.statusChanged.connect(self.status_changed) | |
self.effect.volumeChanged.connect(self.volume_changed) | |
self.effect.setSource(QUrl.fromLocalFile(wav_file)) | |
self.effect.setVolume(self.toolbar.getVolume()) | |
def remaining_changed(self): | |
if self.effect.loopsRemaining() == 0: | |
qurl: QUrl = self.effect.source() | |
msg = 'End playing "%s".' % qurl.fileName() | |
self.add_msg(msg) | |
self.toolbar.playEnd() | |
def set_volume(self, volume: float): | |
if self.effect is not None: | |
self.effect.setVolume(volume) | |
def source_changed(self): | |
qurl: QUrl = self.effect.source() | |
self.add_msg('Wav file: %s' % qurl.fileName()) | |
def source_selected(self, wav_file: str): | |
self.create_sound_effect(wav_file) | |
def status_changed(self): | |
if self.effect.status() == QSoundEffect.Status.Loading: | |
msg = 'Loading' | |
elif self.effect.status() == QSoundEffect.Status.Ready: | |
msg = 'Ready' | |
elif self.effect.status() == QSoundEffect.Status.Error: | |
msg = 'Error' | |
elif self.effect.status() == QSoundEffect.Status.Null: | |
msg = 'Null' | |
else: | |
msg = 'Unknown' | |
self.add_msg(msg) | |
def sound_play(self): | |
qurl: QUrl = self.effect.source() | |
msg = 'Start playing "%s".' % qurl.fileName() | |
self.add_msg(msg) | |
self.effect.play() | |
def sound_stop(self): | |
qurl: QUrl = self.effect.source() | |
msg = 'Stop playing "%s".' % qurl.fileName() | |
self.add_msg(msg) | |
self.effect.stop() | |
def volume_changed(self): | |
msg = 'Volume = %.2f' % self.effect.volume() | |
self.add_msg(msg) | |
def main(): | |
app = QApplication(sys.argv) | |
ex = MyWavPlayer() | |
ex.show() | |
sys.exit(app.exec()) | |
if __name__ == "__main__": | |
main() |
サンプルの実行例を下記に示しました。
WAV ファイルは、参考サイト [2] からダウンロードできるファイルを利用させていただきました。
サンプルの説明
WAV ファイルを読み込み、PLAY/STOP したり、ボリューム調整をする操作は、すべて QToolBar クラスを継承した MyToolBar クラスにまとめています。
class MyToolBar(QToolBar): wavSelected = Signal( str ) wavPlay = Signal() wavStop = Signal() wavVolume = Signal( float ) def __init__( self ): super ().__init__() but_folder = QToolButton() but_folder.setToolTip( 'Choose wav 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 wav 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) (以下省略) |
QSoundEffect のインスタンスを扱うのは、QMainWindow クラスを継承した MyWavPlayer クラスです。ここでは QPlainTextEdit クラスを利用して、操作イベントのログ表示もしています。
class MyWavPlayer(QMainWindow): def __init__( self ): super ().__init__() icon_win = get_icon( self , 'SP_TitleBarMenuButton' ) self .setWindowIcon(icon_win) self .setWindowTitle( 'Wav Player' ) self .effect = None self .toolbar = toolbar = MyToolBar() toolbar.wavSelected.connect( self .source_selected) toolbar.wavPlay.connect( self .sound_play) toolbar.wavStop.connect( self .sound_stop) toolbar.wavVolume.connect( self .set_volume) self .addToolBar(toolbar) self .pte = pte = QPlainTextEdit() pte.setReadOnly( True ) pte.setStyleSheet( 'QPlainTextEdit {background-color: white;}' ) self .setCentralWidget(pte) (以下省略) |
なお、QSoundEffect のインスタンスに読み込んだ WAV ファイルの変更ができなかったので、別の WAV ファイルを読み込む度に create_sound_effect メソッドでインスタンスを作り直しています。
def create_sound_effect( self , wav_file: str ): self .effect = QSoundEffect() self .effect.loopsRemainingChanged.connect( self .remaining_changed) self .effect.sourceChanged.connect( self .source_changed) self .effect.statusChanged.connect( self .status_changed) self .effect.volumeChanged.connect( self .volume_changed) self .effect.setSource(QUrl.fromLocalFile(wav_file)) self .effect.setVolume( self .toolbar.getVolume()) |
PySide6 に組み込まれている標準のビットマップイメージ (Pixmap) を利用するために、名前を指定してアイコンを取得する関数を冒頭に記載しています。
def get_icon(parent, name: str ) - > QIcon: pixmap = getattr (QStyle.StandardPixmap, name) icon = parent.style().standardIcon(pixmap) return icon |
気づいたこと
QSoundEffect クラスのインスタンス effect について、二点、気になることがありました。
effect = QSoundEffect() |
setSource メソッドで他の WAV ファイルを読み込んでも、再生されるサウンドに反映されなかった。
effect.setSource(QUrl.fromLocalFile( file )) |
再生ループで下記の無限ループの enum (QSoundEffect.Loop.Infinite) が利用できなかった。
effect.setLoopCount(QSoundEffect.Loop.Infinite) |
使い方が間違っているのかもしれませんが、バグがもしれないので問い合わせてみることにします。
参考サイト

にほんブログ村
#オープンソース

0 件のコメント:
コメントを投稿