How do I appropriately use QWaitCondition and QMutex? - python

I have two classes, a consumer and a producer. To prevent a race condition between threads, I've tried using QWaitCondition and QMutex, but I keep running into timing issues. The producer object fills my home object's buffer and a buffer of its own. The consumer object copies this buffer and displays the data in the buffer. As it is displaying, I would like the producer object to fill the buffer again and then wait until the displaying is complete. Immediately, the consumer object would copy the buffer and continue the display. Rinse and repeat. However, I can not figure out the timing sequence with my code.
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import QWaitCondition, QMutex
mutex = QMutex()
displayFramesDone = QWaitCondition()
copyFramesDone = QWaitCondition()
sequenceDone = QWaitCondition()
class Producer(QThread):
finished = pyqtSignal()
def __init__(self, home_obj):
self.parent = self
self.home = home_obj
self.prod_buffer = []
"""
functions
"""
def run(self):
self.runSequence()
sequenceDone.wakeAll()
while self.home.condition == True:
self.runSequence()
mutex.lock()
displayFramesDone.wait(mutex)
mutex.unlock()
sequenceDone.wakeAll()
mutex.lock()
copyFramesDone.wait(mutex)
mutex.unlock()
self.finished.emit()
class Consumer(QThread):
finished = pyqtSignal()
def __init__(self, home_obj):
self.parent = self
self.home = home_obj
self.con_buffer = []
"""
functions
"""
def run(self):
while self.home.condition == True:
mutex.lock()
sequenceDone.wait(mutex)
mutex.unlock()
self.copyFrames()
copyFramesDone.wakeAll()
self.displayFrames()
displayFramesDone.wakeAll()
self.finished.emit()
class Home(QWidget):
def __init__(self):
super().__init__()
self.condition == True
self.buffer = []
I've never used QWaitCondition or QMutex before, so if I'm using them appropriately is also another matter, however, this seems to work only occasionally when the timing manages to align with the required sequence.
Additionally, I'd like to know if anyone could answer the following:
Must a wake signal be inside of a mutex block?
Is QWaitCondition/QMutex a good tool for this type of timing problem?

Related

Is a signal emit waits for the receiving end to finish?

Does PySide2 signal emit waits for the receiving end to finish?
I always thought that emit just sends a message and ends there, but when I wrote this code I can see that isn't
from PySide2.QtCore import QObject, Signal, Slot
class MySignal(QObject):
data_signal = Signal(int)
class FirstClass:
def __init__(self):
self.sig = MySignal()
self.data_signal = self.sig.data_signal
def next_line(self):
self.data_signal.emit(1)
class SecondClass:
def __init__(self):
self.count = 0
self.first_class = FirstClass()
self.first_class.data_signal.connect(self.get_data)
self.get_data(1)
#Slot(int)
def get_data(self, data):
print(self.count)
if not self.count > 100:
self.first_class.next_line()
self.count += 1
sec = SecondClass()
I get a long list of 0 (the counter never increases)
and at the end this error
>>> RecursionError: maximum recursion depth exceeded while calling a Python object
What's the best to performe this in PySide2, I know the for this example if I just preformed a yield or return I wouldn't have this problem but I want to understand PySide2 signals and slots.
By default the connection type is Qt :: AutoConnection which decides the type of connection depending on which thread the sender and receiver belong to. In this case, they both belong to the same thread, so using the Qt :: DirectConnection connection that will invoke the slot at the same moment that the signal is emitted, that is, your code is equivalent to:
#Slot(int)
def get_data(self, data):
print(self.count)
if not self.count > 100:
self.get_data(1)
self.count += 1
And obviously that code is recursive since self.count += 1 never executed and therefore the while loop does not end.
The solution is to make the execution not direct but an instant later (when the eventloop is executed) for this the connection must be of type Qt::QueuedConnection and use QCoreApplication.
from PySide2.QtCore import QObject, Signal, Slot, QCoreApplication, Qt
class MySignal(QObject):
data_signal = Signal(int)
class FirstClass:
def __init__(self):
self.sig = MySignal()
self.data_signal = self.sig.data_signal
def next_line(self):
self.data_signal.emit(1)
class SecondClass:
def __init__(self):
self.count = 0
self.first_class = FirstClass()
self.first_class.data_signal.connect(self.get_data, Qt.QueuedConnection)
self.get_data(1)
#Slot(int)
def get_data(self, data):
print(self.count)
if not self.count > 100:
self.first_class.next_line()
self.count += 1
app = QCoreApplication()
sec = SecondClass()
app.exec_()

PyQt5 run same thread dynamically

I am useing python 3.7 and pyqt5
What I want to do is run same QObject with different thread many times.
here is my main
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import threading
import sys
from Worker_Starting import Worker_Starting
class Main(QMainWindow):
def __init__(self) :
super().__init__()
self.setupUI()
self.initSignal()
def setupUI(self):
self.resize(400, 400)
self.pushButton = QPushButton("Start", self)
self.pushButton_2 = QPushButton("Stop", self)
self.pushButton.move(0,0)
self.pushButton_2.move(120,0)
def initSignal(self) :
self.pushButton.clicked.connect(self.Start)
self.pushButton_2.clicked.connect(self.Stop)
#pyqtSlot()
def Start(self) :
for i in range(3) :
print('main', threading.get_ident())
input = ["userID", "userNAME"]
self.my_thread = QThread()
self.my_worker = Worker_Starting(passing_list=input)
self.my_worker.moveToThread(self.my_thread)
self.my_thread.started.connect(self.my_worker.my_fn)
self.my_thread.start()
time.sleep(3)
#pyqtSlot()
def Stop(self) :
pass
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Main()
window.show()
app.exec_()
and my Qobject
from PyQt5 import QtCore
from PyQt5.QtCore import *
import time
import threading
class Worker_Starting(QObject):
def __init__(self, passing_list, parent=None):
QtCore.QThread.__init__(self, parent=parent)
self.userid = passing_list[0]
self.username = passing_list[1]
def my_fn(self):
print(self.username, threading.get_ident())
for i in range(10) :
Now = time.strftime("%Y/%m/%d %H:%M:%S")
print(str(i) + " : " + self.username + " running" + " : " + Now)
time.sleep(0.5)
I put for i in range(3): under def start(self):
because in my actual main script other thread emit input to def start(self): several times.
if I emit only one time, def start(self): works fine.
but when I emit several times, it chrashs.
since I have to emit several times and I need my_thread to work simultaneously and continuously
is there anyway to set same Qobject to different thread?
and how to know or set thread id.
and how to stop thread by thread id.
*I have tried QRunnable, I now it works multi thread. but I have to emit signal back continouously to my main. as far as I know QRunnable are not suitable for custom signal.
The problem is that you're constantly creating new threads by overwriting the existing one, which results in the previous being garbage collected before it's completed, hence the crash.
If you want to execute the same "worker" function concurrently, you cannot use a single object for more threads, as you should use moveToThread each time and this is not possible.
The solution is to use QRunnable instead, implement the function within its run() method, and call QThreadPool to run it.
class Worker_Starting(QRunnable):
def __init__(self, passing_list):
QRunnable.__init__(self)
self.setAutoDelete(False)
self.userid = passing_list[0]
self.username = passing_list[1]
def run(self):
print(self.username, threading.get_ident())
for i in range(10) :
Now = time.strftime("%Y/%m/%d %H:%M:%S")
print(str(i) + " : " + self.username + " running" + " : " + Now)
time.sleep(0.5)
class Main(QMainWindow):
def __init__(self) :
super().__init__()
self.setupUI()
self.initSignal()
self.worker = None
# ...
def Start(self) :
if not self.worker:
input = ["userID", "userNAME"]
self.worker = Worker_Starting(passing_list=input)
for i in range(3):
print('main', threading.get_ident())
QThreadPool.globalInstance().start(self.worker)
Remember that you should not use blocking functions within the main Qt thread (that's why I removed time.sleep(3) from Start()). If you want to start the function more than once at regular intervals, use a QTimer:
interval = 3000 # interval is in milliseconds
for i in range(3):
print('main', threading.get_ident())
QTimer.singleShot(interval * i, lambda:
QThreadPool.globalInstance().start(self.worker))
Note that the pyqtSlot() decorator is usually needed only for special cases and advanced implementation, so you should remove it. Also note that only classes and constants should have capitalized names, while functions, variables and attributes should be lower cased (prefer start and stop instead of Start and Stop).

Using multithreading mistakenly pyqt5 [duplicate]

I am trying to figure out why this code crashes if I try to run the threads for a second time once they are completed.
The first time I click "Start 5 Threads" It runs just fine and finishes. But if I click it again. The entire program crashes and I get the QThread: Destroyed while thread is still running Error
This code was found on the web. I am trying to learn from it.
import time
import sys
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QPushButton, QTextEdit, QVBoxLayout, QWidget
def trap_exc_during_debug(*args):
# when app raises uncaught exception, print info
print(args)
# install exception hook: without this, uncaught exception would cause application to exit
sys.excepthook = trap_exc_during_debug
class Worker(QObject):
"""
Must derive from QObject in order to emit signals, connect slots to other signals, and operate in a QThread.
"""
sig_step = pyqtSignal(int, str) # worker id, step description: emitted every step through work() loop
sig_done = pyqtSignal(int) # worker id: emitted at end of work()
sig_msg = pyqtSignal(str) # message to be shown to user
def __init__(self, id: int):
super().__init__()
self.__id = id
self.__abort = False
#pyqtSlot()
def work(self):
"""
Pretend this worker method does work that takes a long time. During this time, the thread's
event loop is blocked, except if the application's processEvents() is called: this gives every
thread (incl. main) a chance to process events, which in this sample means processing signals
received from GUI (such as abort).
"""
thread_name = QThread.currentThread().objectName()
thread_id = int(QThread.currentThreadId()) # cast to int() is necessary
self.sig_msg.emit('Running worker #{} from thread "{}" (#{})'.format(self.__id, thread_name, thread_id))
for step in range(100):
time.sleep(0.1)
self.sig_step.emit(self.__id, 'step ' + str(step))
# check if we need to abort the loop; need to process events to receive signals;
app.processEvents() # this could cause change to self.__abort
if self.__abort:
# note that "step" value will not necessarily be same for every thread
self.sig_msg.emit('Worker #{} aborting work at step {}'.format(self.__id, step))
break
self.sig_done.emit(self.__id)
def abort(self):
self.sig_msg.emit('Worker #{} notified to abort'.format(self.__id))
self.__abort = True
class MyWidget(QWidget):
NUM_THREADS = 5
# sig_start = pyqtSignal() # needed only due to PyCharm debugger bug (!)
sig_abort_workers = pyqtSignal()
def __init__(self):
super().__init__()
self.setWindowTitle("Thread Example")
form_layout = QVBoxLayout()
self.setLayout(form_layout)
self.resize(400, 800)
self.button_start_threads = QPushButton()
self.button_start_threads.clicked.connect(self.start_threads)
self.button_start_threads.setText("Start {} threads".format(self.NUM_THREADS))
form_layout.addWidget(self.button_start_threads)
self.button_stop_threads = QPushButton()
self.button_stop_threads.clicked.connect(self.abort_workers)
self.button_stop_threads.setText("Stop threads")
self.button_stop_threads.setDisabled(True)
form_layout.addWidget(self.button_stop_threads)
self.log = QTextEdit()
form_layout.addWidget(self.log)
self.progress = QTextEdit()
form_layout.addWidget(self.progress)
QThread.currentThread().setObjectName('main') # threads can be named, useful for log output
self.__workers_done = None
self.__threads = None
def start_threads(self):
self.log.append('starting {} threads'.format(self.NUM_THREADS))
self.button_start_threads.setDisabled(True)
self.button_stop_threads.setEnabled(True)
self.__workers_done = 0
self.__threads = []
for idx in range(self.NUM_THREADS):
worker = Worker(idx)
thread = QThread()
thread.setObjectName('thread_' + str(idx))
self.__threads.append((thread, worker)) # need to store worker too otherwise will be gc'd
worker.moveToThread(thread)
# get progress messages from worker:
worker.sig_step.connect(self.on_worker_step)
worker.sig_done.connect(self.on_worker_done)
worker.sig_msg.connect(self.log.append)
# control worker:
self.sig_abort_workers.connect(worker.abort)
# get read to start worker:
# self.sig_start.connect(worker.work) # needed due to PyCharm debugger bug (!); comment out next line
thread.started.connect(worker.work)
thread.start() # this will emit 'started' and start thread's event loop
# self.sig_start.emit() # needed due to PyCharm debugger bug (!)
#pyqtSlot(int, str)
def on_worker_step(self, worker_id: int, data: str):
self.log.append('Worker #{}: {}'.format(worker_id, data))
self.progress.append('{}: {}'.format(worker_id, data))
#pyqtSlot(int)
def on_worker_done(self, worker_id):
self.log.append('worker #{} done'.format(worker_id))
self.progress.append('-- Worker {} DONE'.format(worker_id))
self.__workers_done += 1
if self.__workers_done == self.NUM_THREADS:
self.log.append('No more workers active')
self.button_start_threads.setEnabled(True)
self.button_stop_threads.setDisabled(True)
# self.__threads = None
#pyqtSlot()
def abort_workers(self):
self.sig_abort_workers.emit()
self.log.append('Asking each worker to abort')
for thread, worker in self.__threads: # note nice unpacking by Python, avoids indexing
thread.quit() # this will quit **as soon as thread event loop unblocks**
thread.wait() # <- so you need to wait for it to *actually* quit
# even though threads have exited, there may still be messages on the main thread's
# queue (messages that threads emitted before the abort):
self.log.append('All threads exited')
if __name__ == "__main__":
app = QApplication([])
form = MyWidget()
form.show()
sys.exit(app.exec_())
The problem is solved by passing him as a parent to self. You must change:
thread = QThread()
to:
thread = QThread(parent=self)

PyQt Qthread automatic restart

I'm trying to understand how thread works, and i'm stuck with this problem. That's my program explained:
i made a simple GUI in pyqt that use a QObject as a worker class. When i press the botton start the gui read a random value from a list and pass it to the thread, that print the
next five number. When the thread finish the work, it pass the data to the gui. Now i want the GUI to restart automatically a new thread with a new start value. I can restart the thread by pressing start again, but i need to start it without human interaction. Are there
any method?
thanks in advance
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import time
import sys
import numpy as np
class SomeObject(QObject):
finished = pyqtSignal(object)
valore = pyqtSignal(object)
vector = pyqtSignal(object)
def __init():
super(SomeObject, self).__init__()
def longRunning(self):
vec = []
end = self.count + 5
while self.count < end:
time.sleep(1)
vec.append(self.count)
self.valore.emit(self.count)
self.count += 1
self.finished.emit(vec)
#self.vector.emit()
def setCount(self, num):
self.count = num
class GUI(QDialog):
def __init__(self, parent = None):
super(GUI, self).__init__(parent)
#declare QThread object
self.objThread = QThread()
#declare SomeObject type, and move it to thread
self.obj = SomeObject()
self.obj.moveToThread(self.objThread)
#connect finished signal to nextVector method
self.obj.finished.connect(self.nextVector)
#connect valore to self.prova method
self.obj.valore.connect(self.prova)
#self.obj.vector.connect(self.nextVector)
#Connect thread.start to the method long running
self.objThread.started.connect(self.obj.longRunning)
botton = QPushButton("start")
self.connect(botton, SIGNAL("clicked()"), self.showcount)
box = QHBoxLayout()
box.addWidget(botton)
self.setLayout(box)
#a list of random number
a = np.random.randint(10, size = 5)
self.iter = iter(a)
def showcount(self):
"""
When botton clicked, read the next value from iter, pass it to
setCount and when start the thread
"""
try:
a = self.iter.next()
print a
self.obj.setCount(a)
self.objThread.start()
except StopIteration:
print "finito"
#self.obj.setCount(a)
#self.objThread.start()
#print self.objThread.currentThreadId()
def prova(self, value):
"""
Connected to signal valore, print the value
"""
print value
def nextVector(self, vec):
"""
Print the whole vector
"""
print vec
self.objThread.quit()
try:
a = self.iter.next()
print a
self.obj.setCount(a)
self.objThread.start()
except StopIteration:
print "finito"
app = QApplication(sys.argv)
form = GUI()
form.show()
app.exec_()
You already have it set up. When your thread is finished it emits the finished signal which calls the nextVector method, so just call the start method at the end of nextVector.
def nextVector(self, vec):
...
self.showcount()
# end nextVector
You may also want to change to the new signal connection for your QPushButton
button.clicked.connect(self.showcount)

QThread speeds up after terminating instead of terminating

I am totally confused by the QThread behavior. My idea is to acquire some audio signal in a qthread, save it in a python queue object and with a QTimer I read the queue and plot it using pyqtgraph. It works, however, only at around 6-7 fps. However, when I use .terminate() to terminate the thread, the thread does actually NOT terminate, but rather speeds up to > 100 fps, exactly what I actually wanted.
My issues:
why does the QThread not terminate/is aborted/closed...?
what is .terminate() actually doing?
what is slowing down the normal thread.start()?
On a side note, I know that I am not using a Signal/Slot for checking if it should still run or not, I just want to understand this strange behavior, and why the thread is not fast from the very beginning! Something is maybe blocking the proper function and is turned off (?!) by the .terminate() function...
My minimal working example (hope you guys have a soundcard/mic somewhere):
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
The first suggestion #Dennis Jensen was to not subclass QThread, but use rather QObject/QThread/moveToThread. I did this, see code below, and one can see that the issue is gone with either using while and just app.processEvents() or while with time.sleep(0.1), but to make it response you have to use anyway app.processEvents(), so this is sufficient. The pass statement alone eats up alot of CPU processing power, resulting in 7-10 fps, but if you thread.terminate() this thread, everything still runs.
I added additionally a trace, what happens on which thread, and the callback is always on a separate thread, regardless which callback you use (outside any class, in QObject or in the main thread), indicating that the answer from #three_pineapples is correct.
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
Here the code that uses no QThread environment, and this works as expected!
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_()
The problem is due to the while True: pass line in your thread. To understand why, you need to understand how PortAudio (the library wrapped by sounddevice) works.
Anything passed a callback like you are doing with InputStream is likely calling the provided method from a separate thread (not the main thread or your QThread). Now from what I can tell, whether the callback is called from a separate thread or some sort of interrupt is platform dependent, but either way it is operating somewhat independently of your QThread even though the method exists inside that class.
The while True: pass is going to consume close to 100% of your CPU, limiting what any other thread can do. That is until you terminate it! Which frees up resources for whatever is actually calling the callback to work faster. While you might expect the audio capture to be killed along with your thread, chances are it hasn't been garbage collected yet (and garbage collection gets complicated when dealing with C/C++ wrapped libraries, nevermind when you have two of them! [PortAudio and Qt] - And there is a good chance that garbage collection in Python might not actually free the resources in your case anyway!)
So this explains why things get faster when you terminate the thread.
The solution is to change your loop to while True: time.sleep(.1) which will ensure it doesn't consume resources unnecessarily! You could also look into whether you actually need that thread at all (depending on how PortAudio works on your platform). If you move to the signal/slot architecture and do away with the with statement (managing the open/close of the resource in separate slots) that would also work as you wouldn't need the problematic loop at all.
There are some small issues in your code, all concurring to the "low" framerate, mostly due to the fact that you're using a blocksize of 4096 (which is too high if you want frequent updates) and are also trying to update the GUI too fast while also processing data.
On a slightly old computer as mine, your code completely hangs the interface as soon as I start recording, and stopping is almost impossible if not by killing the app at all.
What you see is not the thread "speeding" up, but rather the QTimer that has much more "time" (cycles) to call its timeout more frequently.
First of all, you shouldn't use terminate, but maybe use a Queue to send a "quit" command to the while loop, allowing it to exit gracefully.
Then, it's better to use signals/slots to retrieve and process the output data, as it's more intuitive and improves the overall cpu load.
Finally, if you want to get the fps of the received data, there's no use in a QTimer set to 0 (which will make it only run as fast as possible even if unnecessary, making the cpu spike needlessly).
class Record(QThread):
audioData = pyqtSignal(object)
def __init__(self):
super().__init__()
self.stopper = queue.Queue()
def callback(self, indata, frames, time, status):
self.audioData.emit(indata.copy())
def run(self):
with sd.InputStream(samplerate=48000, channels=2, callback=self.callback, blocksize=1024):
print('Stream started...')
while True:
try:
if self.stopper.get(timeout=.1):
break
except:
pass
print(self.isRunning(), 'Done?') # never called
def stop(self):
self.stopper.put(True)
class Main(QWidget):
# ...
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.r = Record()
self.r.audioData.connect(self.plotData)
self.r.start()
def plotData(self, data):
self.curve1.setData(data[:, 0])
self.curve2.setData(data[:, 1]-3)
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)))

Categories

Resources