QMediaPlayer playback issues

Viewed 17

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()
0 Answers
Related