I recently switched to QMediaPlayer in Qt6 for a music player. I noticed that playback sometimes stopped if seeked in the file. I noticed playback speed problems after pausing a track (would resume at too fast of a rate..and then settle down after a few seconds). Rather than try and debug my application I decided to reproduce the issues in some fresh code. I did as well as some others. Have I missed something in how i setup the player? I wanted to get some verification on that as well as the problems I'm seeing. My guess is I've stumbled into a multitude of bugs, but before filing I wanted to double check here. Thanks in advance!
To make testing easier, I generate a test wav file the first time the code is run. It is just 10 seconds of a constant beep. It makes it real easy to tell when the playback rate is wrong.
# Problems:
# 1) First play sounds corrupt at beginning, crackling and wrong playback speed...settles down after a few seconds and sounds fine on second loop
# 2) Pause near end of track usually resumes without sound (even after reloading media)
# 3) Repeated play / pause causes playback to stop sometimes (and can't be continued without a reload of the source media)
# 4) Seeking multiple times has the same problem as play / pause
# 5) Playback sometimes just stops after a few loops without any interaction.
# ...reproduced on multimple machines. Kubuntu Desktop 22.04 and Live Boot of Kubuntu Desktop 22.04.
# Added check error button, but it always reports Error.NoError
# Python 3.10.4, PyQt6 6.3.0 (PyQt6 6.3.1 on live image)
import math
import os.path
import struct
import wave
from PyQt6 import QtCore, QtWidgets
from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer
TEST_SOUND = 'beeps.wav'
def create_test_wav():
# 10 seconds of half a second beep and half a second silence
SAMPLES_PER_SECOND = 44100.0
MILLISECONDS_PER_SECOND = 1000.0
BEEP_FREQUENCY = 440.0 # Standard A tone
samples: list[float] = []
pulse_width_ms = 500
num_samples = int(pulse_width_ms * (SAMPLES_PER_SECOND / MILLISECONDS_PER_SECOND))
volume = 0.10 # Full volume (1.0) is just too loud
for _ in range(10):
# Generate a sine wave beep
for s in range(num_samples):
samples.append(volume * math.sin(2 * math.pi * BEEP_FREQUENCY * (s / SAMPLES_PER_SECOND)))
# Generate silence
samples.extend([0.0] * num_samples)
wav_file = wave.open(TEST_SOUND, 'w')
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(SAMPLES_PER_SECOND)
wav_file.setnframes(len(samples))
# Sample data is in range -1.0 to 1.0
max_value = (1 << 16) // 2 - 1 # 32,767
for sample in samples:
wav_file.writeframes(struct.pack('h', int(sample * max_value))) # h = signed short integer (16 bit)
wav_file.close()
if not os.path.exists(TEST_SOUND):
print(f'Creating test sound file...{TEST_SOUND}')
create_test_wav()
class QMediaPlayerTest(QtWidgets.QDialog):
def __init__(self):
super().__init__()
self._layout = QtWidgets.QVBoxLayout()
self._button_play = QtWidgets.QPushButton('Play')
self._layout.addWidget(self._button_play)
self._button_restart = QtWidgets.QPushButton('Restart')
self._layout.addWidget(self._button_restart)
self._button_error = QtWidgets.QPushButton('Check Error')
self._layout.addWidget(self._button_error)
self._progress = QtWidgets.QSlider(self)
self._layout.addWidget(self._progress)
self._messages = QtWidgets.QListWidget(self)
self._layout.addWidget(self._messages)
self._button_play.setMaximumWidth(100)
self._button_restart.setMaximumWidth(100)
self._button_error.setMaximumWidth(100)
self._progress.setMouseTracking(True)
self._progress.setOrientation(QtCore.Qt.Orientation.Horizontal)
self._progress.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBothSides)
self._progress.setMinimum(0)
self._progress.setMaximum(100) # Updated when duration change message is received
self._progress.setValue(0)
self._player = QMediaPlayer()
self._audioOutput = QAudioOutput()
self._player.setAudioOutput(self._audioOutput)
self._button_play.clicked.connect(self._play)
self._button_restart.clicked.connect(self._restart)
self._button_error.clicked.connect(self._error)
self._progress.valueChanged.connect(self.progress_value_changed)
self._player.playbackStateChanged.connect(self._play_state_changed)
self._player.bufferProgressChanged.connect(self._buffer_progress_changed)
self._player.positionChanged.connect(self._position_changed)
self._player.durationChanged.connect(self._duration_changed)
self._player.seekableChanged.connect(self._seekable_changed)
self._player.mediaStatusChanged.connect(self._media_status_changed)
self._player.errorOccurred.connect(self._error_occurred)
self._player.errorChanged.connect(self._error_changed)
self._player.hasAudioChanged.connect(self._has_audio_changed)
self._player.playbackRateChanged.connect(self._playback_rate_changed)
self._player.sourceChanged.connect(self._source_changed)
self._player.loopsChanged.connect(self._loops_changed)
self._set_source()
self._player.setLoops(QMediaPlayer.Loops.Infinite)
self._audioOutput.setVolume(0.1)
self.setGeometry(QtCore.QRect(1, 1, 500, 500))
self.setLayout(self._layout)
self._progress.installEventFilter(self)
def eventFilter(self, obj, e):
# Return True if the event should be filtered, (i.e. stopped); otherwise it must return False.
if obj == self._progress and self._progress.isEnabled():
event_type = e.type()
# Make slider jump to clicked position
if event_type in (QtCore.QEvent.Type.MouseButtonPress, QtCore.QEvent.Type.MouseMove):
value = QtWidgets.QStyle.sliderValueFromPosition(self._progress.minimum(), self._progress.maximum(), int(e.position().x()), self._progress.width())
if e.buttons() & QtCore.Qt.MouseButton.LeftButton:
self._progress.setValue(value)
e.accept()
return True
return super().eventFilter(obj, e)
def _add_message(self, text: str):
self._messages.addItem(text)
self._messages.scrollToBottom()
def _set_source(self):
if 1:
self._player.setSource(QtCore.QUrl(TEST_SOUND))
else: # I thought manual buffering of the audio would help...it did not
file = QtCore.QFile(TEST_SOUND)
if not file.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
self._add_message('File not opened')
self._add_message(f'File size: {file.size()}')
ba: QtCore.QByteArray = file.readAll()
self._add_message(f'ByteArray size: {ba.size()}')
buffer = QtCore.QBuffer(self)
buffer.setData(ba)
if not buffer.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
self._add_message('Buffer not opened')
self._add_message(f'Buffer size: {buffer.size()}')
buffer.seek(0)
self._player.setSourceDevice(buffer, QtCore.QUrl(TEST_SOUND))
def progress_value_changed(self, pos: int):
self._add_message(f'progress_value_changed {pos=}')
self._seek(pos)
def _seek(self, seek_ms):
self._add_message(f'_seek {seek_ms=}')
self._player.setPosition(seek_ms)
def _play(self):
if self._button_play.text() == 'Play':
self._button_play.setText('Pause')
self._player.play()
else:
self._button_play.setText('Play')
self._player.pause()
def _restart(self):
self._player.stop()
self._set_source()
self._button_play.setText('Play')
def _error(self):
self._add_message(f'error = {self._player.error()}')
def _play_state_changed(self, new_state: QMediaPlayer.PlaybackState):
self._add_message(f'_play_state_changed {new_state.name}')
def _error_occurred(self, error: QMediaPlayer.Error, text: str):
self._add_message(f'_error_occurred {error.name} {text}')
def _error_changed(self):
self._add_message(f'_error_changed {self._player.error()}')
def _has_audio_changed(self, audio: bool):
self._add_message(f'_has_audio_changed {audio}')
def _playback_rate_changed(self, rate):
self._add_message(f'_playback_rate_changed {rate}')
def _source_changed(self, source: QtCore.QUrl):
self._add_message(f'_source_changed {source}')
def _loops_changed(self):
self._add_message(f'_loops_changed {self._player.loops()}')
def _media_status_changed(self, state: QMediaPlayer.MediaStatus):
self._add_message(f'_media_status_changed {state.name}')
def _buffer_progress_changed(self, percent: float):
self._add_message(f'_buffer_progress_changed {percent=}')
def _position_changed(self, pos_ms: int):
self._progress.blockSignals(True) # Prevent seek slider changed event
self._progress.setValue(pos_ms)
self._progress.blockSignals(False)
def _seekable_changed(self, seekable: bool):
self._add_message(f'_seekable_changed {seekable}')
def _duration_changed(self, duration_ms: int):
self._add_message(f'_duration_changed {duration_ms}')
self._progress.setMaximum(duration_ms)
self._progress.setTickInterval(1000)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QMediaPlayerTest()
w.show()
app.exec()