Kerem Basaran
Kerem Basaran

Reputation: 5

GUI is blocked when it signals the slot of a QThread object

I am trying to create a QThread object whose task will be to handle all the requests from different widgets to play signals on the sound card. I name this object 'sound_engine' and keep it as the only object using the sounddevice module in the application.

The operation of sound_engine is non-blocking when I start it. I ask it to play a 2 second beep within its 'run' method and it does this without blocking the GUI. Yet when I signal its 'beep' slot to play another sound, it is blocking the object which emitted the signal, in this case my main window 'mw'. I assume this is related to the thread exiting once 'run' returns but then what is the right way to go about this?

import sys
import numpy as np
import sounddevice as sd

from PySide6 import QtWidgets as qtw
from PySide6 import QtCore as qtc

from __feature__ import snake_case
from __feature__ import true_property

class SoundEngine(qtc.QThread):
    def __init__(self):
        super().__init__()
        self.FS = 48000

    def run(self):
        self.start_stream()
        # do a start beep for testing
        self.beep(2, 200)

    def start_stream(self):
        self.stream = sd.Stream(samplerate=self.FS, channels=2)
        self.dtype = self.stream.dtype
        self.channel_count = self.stream.channels[0]
        self.stream.start()

    @qtc.Slot(float, float)
    def beep(self, T, freq):
        t = np.arange(T * self.FS) / self.FS
        y = np.tile(0.1 * np.sin(t * 2 * np.pi * freq), self.channel_count)
        y = y.reshape((len(y) // self.channel_count, self.channel_count)).astype(self.dtype)
        self.stream.write(y)

class MainWindow(qtw.QMainWindow):
    signal_beep = qtc.Signal(float, float)

    def __init__(self, sound_engine):
        super().__init__()
        self.create_widgets()
        self.place_widgets()
        self.make_connections()

    def create_widgets(self):
        self._beep_freq_dial = qtw.QDial(minimum=200,
                                         maximum=2000,
                                         wrapping=False,
                                         )
        self._beep_freq_display = qtw.QLCDNumber()
        self._beep_pusbutton = qtw.QPushButton("Beep")

    def place_widgets(self):
        self._center_layout = qtw.QVBoxLayout()
        self._center_widget = qtw.QWidget()
        self._center_widget.set_layout(self._center_layout)
        self.set_central_widget(self._center_widget)

        self._center_layout.add_widget(self._beep_freq_dial)
        self._center_layout.add_widget(self._beep_freq_display)
        self._center_layout.add_widget(self._beep_pusbutton)

    def make_connections(self):
        self._beep_pusbutton.clicked.connect(
            lambda: self.signal_beep.emit(1, self._beep_freq_dial.value)
            )
        self.signal_beep.connect(sound_engine.beep)

        self._beep_freq_display.display(self._beep_freq_dial.value)
        self._beep_freq_dial.valueChanged.connect(self._beep_freq_display.display)

if __name__ == "__main__":
    app = qtw.QApplication(sys.argv)

    sound_engine = SoundEngine()
    sound_engine.start(qtc.QThread.HighPriority)

    mw = MainWindow(sound_engine)
    mw.show()

    app.exec()

I tried putting in other functions instead of the 'sounddevice' thinking maybe the hardware access causes this. That was not the case, even a simple 'sleep' is blocking.

Upvotes: 0

Views: 58

Answers (1)

Lungen Brötchen
Lungen Brötchen

Reputation: 26

You don't gain anything by subclassing QThread in your case. A more default approach would be to hand over a worker object to a thread and then simply run this thread.

The SoundEngine would become a simple QObject:

...
class SoundEngine(qtc.QObject):
   def __init__(self):
...

The main function would look like that:

if __name__ == "__main__":
    app = qtw.QApplication(sys.argv)

    sound_engine = SoundEngine()
    thread = qtc.QThread()
    sound_engine.move_to_thread(thread)
    thread.started.connect(sound_engine.run)
    thread.start()

mw = MainWindow(sound_engine)
mw.show()

app.exec()

You are doing youself and others no favor by importing modules with aliases and using snake_case for a framework that is built in C++. The chance that anybody finds your issue is lowered dramatically.

Upvotes: 0

Related Questions