Я полностью сбит с толку поведением QThread. Моя идея состоит в том, чтобы получить некоторый аудиосигнал в qthread, сохранить его в объекте python queue
и с помощью QTimer
я читаю очередь и строю его с помощью pyqtgraph
. Это работает, однако, только на скорости около 6-7 кадров в секунду. Однако, когда я использую .terminate()
для завершения потока, поток на самом деле НЕ завершается, а скорее достигает скорости> 100 кадров в секунду, именно то, что я на самом деле хотел.
Мои проблемы:
- почему QThread не завершается / прерывается / закрывается ...?
- что на самом деле делает
.terminate()
- что замедляет нормальное
thread.start()
?
На заметку, я знаю, что я неиспользуя сигнал / слот для проверки, должен ли он все еще работать, я просто хочу понять это странное поведение и почему поток не быстрый с самого начала! Возможно, что-то блокирует правильную функцию и отключается (?!) Функцией .terminate()
...
Мой минимальный рабочий пример (надеюсь, у вас где-нибудь есть звуковая карта / микрофон):
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton
from PyQt5.QtCore import QThread, QTimer
import sounddevice as sd
import queue
import pyqtgraph as pg
import numpy as np
import time
class Record(QThread):
def __init__(self):
super().__init__()
self.q = queue.Queue()
def callback(self, indata, frames, time, status):
self.q.put(indata.copy())
def run(self):
with sd.InputStream(samplerate=48000, device=1, channels=2, callback=self.callback, blocksize=4096):
print('Stream started...')
while True:
pass
print(self.isRunning(), 'Done?') # never called
class Main(QWidget):
def __init__(self):
super().__init__()
self.recording = False
self.r = None
self.x = 0
self.times = list(range(10))
self.setWindowTitle("Record Audio Tester")
self.l = QGridLayout()
self.setLayout(self.l)
self.pl = pg.PlotWidget(autoRange=False)
self.curve1 = self.pl.plot(np.zeros(8000))
self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
self.l.addWidget(self.pl)
self.button_record = QPushButton("Start recording")
self.button_record.clicked.connect(self.record)
self.l.addWidget(self.button_record)
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.terminate()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.r = Record()
self.r.start()
self.t = QTimer()
self.t.timeout.connect(self.plotData)
self.t.start(0)
def plotData(self):
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))
if self.r.q.empty():
return
d = self.r.q.get()
self.curve1.setData(d[:, 0])
self.curve2.setData(d[:, 1]-3)
if __name__ == '__main__':
app = QApplication([])
w = Main()
w.show()
app.exec_()
edit 1
Первым предложением @Dennis Jensen было не подкласс QThread, а скорее использование QObject / QThread / moveToThread. Я сделал это, см. Код ниже, и можно увидеть, что проблема устранена с использованием while
и просто app.processEvents()
или while
с time.sleep(0.1)
, но для ответа вы должны будете использовать в любом случае app.processEvents()
так что этого достаточно. Один оператор pass
потребляет много ресурсов процессора, что приводит к 7-10 кадрам в секунду, но если вы thread.terminate()
этот поток, все по-прежнему работает.
Я добавил дополнительно трассировку, что происходит на какойнить, и обратный вызов всегда находится в отдельном потоке, независимо от того, какой обратный вызов вы используете (вне какого-либо класса, в QObject или в основном потоке), указывая на то, что ответ от @three_pineapples является правильным.
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton, QCheckBox
from PyQt5.QtCore import QThread, QTimer, QObject, pyqtSignal, pyqtSlot
import threading
import sounddevice as sd
import queue
import pyqtgraph as pg
import numpy as np
import time
q = queue.Queue()
# It does not matter at all where the callback is,
# it is always on its own thread...
def callback(indata, frames, time, status):
print("callback", threading.get_ident())
# print()
q.put(indata.copy())
class Record(QObject):
start = pyqtSignal(str)
stop = pyqtSignal()
data = pyqtSignal(np.ndarray)
def __init__(self, do_pass=False, use_terminate=False):
super().__init__()
self.q = queue.Queue()
self.r = None
self.do_pass = do_pass
self.stop_while = False
self.use_terminate = use_terminate
print("QObject -> __init__", threading.get_ident())
def callback(self, indata, frames, time, status):
print("QObject -> callback", threading.get_ident())
self.q.put(indata.copy())
@pyqtSlot()
def stopWhileLoop(self):
self.stop_while = True
@pyqtSlot()
def run(self, m='sth'):
print('QObject -> run', threading.get_ident())
# Currently uses a callback outside this QObject
with sd.InputStream(device=1, channels=2, callback=callback) as stream:
# Test the while pass function
if self.do_pass:
while not self.stop_while:
if self.use_terminate: # see the effect of thread.terminate()...
pass # 7-10 fps
else:
app.processEvents() # makes it real time, and responsive
print("Exited while..")
stream.stop()
else:
while not self.stop_while:
app.processEvents() # makes it responsive to slots
time.sleep(.01) # makes it real time
stream.stop()
print('QObject -> run ended. Finally.')
class Main(QWidget):
def __init__(self):
super().__init__()
self.recording = False
self.r = None
self.x = 0
self.times = list(range(10))
self.q = queue.Queue()
self.setWindowTitle("Record Audio Tester")
self.l = QGridLayout()
self.setLayout(self.l)
self.pl = pg.PlotWidget(autoRange=False)
self.curve1 = self.pl.plot(np.zeros(8000))
self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
self.l.addWidget(self.pl)
self.button_record = QPushButton("Start recording")
self.button_record.clicked.connect(self.record)
self.l.addWidget(self.button_record)
self.pass_or_sleep = QCheckBox("While True: pass")
self.l.addWidget(self.pass_or_sleep)
self.use_terminate = QCheckBox("Use QThread terminate")
self.l.addWidget(self.use_terminate)
print("Main thread", threading.get_ident())
def streamData(self):
self.r = sd.InputStream(device=1, channels=2, callback=self.callback)
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.stop.emit()
# And this is where the magic happens:
if self.use_terminate.isChecked():
self.thr.terminate()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.t = QTimer()
self.t.timeout.connect(self.plotData)
self.t.start(0)
self.thr = QThread()
self.thr.start()
self.r = Record(self.pass_or_sleep.isChecked(), self.use_terminate.isChecked())
self.r.moveToThread(self.thr)
self.r.stop.connect(self.r.stopWhileLoop)
self.r.start.connect(self.r.run)
self.r.start.emit('go!')
def addData(self, data):
# print('got data...')
self.q.put(data)
def callback(self, indata, frames, time, status):
self.q.put(indata.copy())
print("Main thread -> callback", threading.get_ident())
def plotData(self):
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))
if q.empty():
return
d = q.get()
# print("got data ! ...")
self.curve1.setData(d[:, 0])
self.curve2.setData(d[:, 1]-1)
if __name__ == '__main__':
app = QApplication([])
w = Main()
w.show()
app.exec_()
edit 2
Здесь код, который не использует среду QThread, и это работает, как и ожидалось!
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton, QCheckBox
from PyQt5.QtCore import QTimer
import threading
import sounddevice as sd
import queue
import pyqtgraph as pg
import numpy as np
import time
class Main(QWidget):
def __init__(self):
super().__init__()
self.recording = False
self.r = None
self.x = 0
self.times = list(range(10))
self.q = queue.Queue()
self.setWindowTitle("Record Audio Tester")
self.l = QGridLayout()
self.setLayout(self.l)
self.pl = pg.PlotWidget(autoRange=False)
self.curve1 = self.pl.plot(np.zeros(8000))
self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
self.l.addWidget(self.pl)
self.button_record = QPushButton("Start recording")
self.button_record.clicked.connect(self.record)
self.l.addWidget(self.button_record)
print("Main thread", threading.get_ident())
def streamData(self):
self.r = sd.InputStream(device=1, channels=2, callback=self.callback)
self.r.start()
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.stop()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.t = QTimer()
self.t.timeout.connect(self.plotData)
self.t.start(0)
self.streamData()
def callback(self, indata, frames, time, status):
self.q.put(indata.copy())
print("Main thread -> callback", threading.get_ident())
def plotData(self):
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))
if self.q.empty():
return
d = self.q.get()
# print("got data ! ...")
self.curve1.setData(d[:, 0])
self.curve2.setData(d[:, 1]-1)
if __name__ == '__main__':
app = QApplication([])
w = Main()
w.show()
app.exec_()