Accurate timer with PyQt - python

I'm using pyqtgraph to plot a huge number of data that I receive from sensors.
To do so, I made one thread that acquire the data and put in a queue. To plot the data, I check periodically with a timer if the queue is not empty.
The problem is that the accuracy of the timer (QTimer) seems to be really bad. I mean when the load is low (sleep for 1000/100 ms) in the measuring thread, the accuracy is pretty good but when the load increase (sleep for 10ms), my update function used to plot data is not called back with the same period.
Here is an example code:
import sys
import time
from queue import Queue
from random import random
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtWidgets
data_queue = Queue()
class WorkerThread(QtCore.QThread):
def __init__(self, parent):
super(WorkerThread, self).__init__(parent=parent)
def run(self):
t_init = time.time()
while True:
# Generating random data
values = [(time.time()-t_init, random()) for _ in range(200)]
data_queue.put(values)
print("adding data")
self.msleep(10)
class GraphPlot(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(GraphPlot, self).__init__(parent)
self.mainbox = QtWidgets.QWidget()
self.setCentralWidget(self.mainbox)
self.mainbox.setLayout(QtWidgets.QVBoxLayout())
self.canvas = pg.GraphicsLayoutWidget()
self.mainbox.layout().addWidget(self.canvas)
self.analogPlot = self.canvas.addPlot(title='Real-time data')
self.drawplot = self.analogPlot.plot(pen='r')
numPoints = 20000
self.t = np.zeros(numPoints, dtype=int)
self.x = np.zeros(numPoints, dtype=int)
self.worker = WorkerThread(self)
self.worker.start()
self.timer = pg.QtCore.QTimer()
self.timer.setTimerType(QtCore.Qt.PreciseTimer)
self.timer.timeout.connect(self._update)
self.timer.start(1)
def _update(self):
print('start:', time.time())
size = data_queue.qsize()
if size > 0:
for _ in range(size):
values = data_queue.get()
for v in values:
self.t = np.append(self.t[1:], v[0])
self.x = np.append(self.x[1:], v[1])
self.drawplot.setData(self.t, self.x)
print('end:', time.time())
app = QtWidgets.QApplication(sys.argv)
plot = GraphPlot()
plot.show()
sys.exit(app.exec_())
An excerpt of the output:
start: 1572893919.9067862
adding data
end: 1572893919.9217482 <--
adding data
start: 1572893919.9586473 <-- there should be 1ms of difference with last 'end'
actually, there is around 37ms
I want the timer to be synchronous with the same period whatever the load on the measuring thread. I tried to decrease the priority of the former thread but it did not solve the problem.

QTimer documentation partially answers to your issue:
All timer types may time out later than expected if the system is busy
or unable to provide the requested accuracy. In such a case of timeout
overrun, Qt will emit timeout() only once, even if multiple timeouts
have expired, and then will resume the original interval.
The problem is that after you call _update from the timeout, Qt will need some time to process what happens after self.drawplot.setData(), which basically is computing graphical information and actually painting it on the screen.
You're not getting the 1ms delay because Qt just isn't able to work that fast.
Even if a QTimer can work in another thread ("asynchronously", but be careful about the meaning of this word), it always depend on the thread it is created or resides (a QTimer cannot be started or stopped from a thread different than its one). So, since you've created the timer in the window thread (the Qt main event loop), its timeout accuracy depends on the capacity of that loop to handle all its events, and since lot of events are related to GUI painting (which seems fast to our eyes, but is actually slow as it's very demanding for the CPU), you can easily understand why you'll never get that 1ms interval. And don't forget the fact that even if pyqtgraph is very fast we're still talking about Python.
While reaching a better accuracy for a 1ms QTimer is possible (creating a separate thread for it), you wouldn't get any advantage from it anyway: even with a very fast computer, what you're substantially requesting is to update the screen at 1000Hz, while most graphic hardware is not able to go much faster than 100-200Hz; this means that even if you own a high end system, you wouldn't get more than one update each 4ms.
If you need to update the plot each time new data is available, it is probably better to use signals and slots, which also avoids any unnecessary check on the queue and ensures to update the plot as much as needed:
class WorkerThread(QtCore.QThread):
newData = QtCore.pyqtSignal(object)
def __init__(self, parent):
super(WorkerThread, self).__init__(parent=parent)
def run(self):
t_init = time.time()
while True:
# Generating random data
values = [(time.time()-t_init, random()) for _ in range(200)]
print("adding data")
self.newData.emit(values)
self.msleep(10)
class GraphPlot(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(GraphPlot, self).__init__(parent)
self.mainbox = QtWidgets.QWidget()
self.setCentralWidget(self.mainbox)
self.mainbox.setLayout(QtWidgets.QVBoxLayout())
self.canvas = pg.GraphicsLayoutWidget()
self.mainbox.layout().addWidget(self.canvas)
self.analogPlot = self.canvas.addPlot(title='Real-time data')
self.drawplot = self.analogPlot.plot(pen='r')
numPoints = 20000
self.t = np.zeros(numPoints, dtype=int)
self.x = np.zeros(numPoints, dtype=int)
self.worker = WorkerThread(self)
self.worker.newData.connect(self.newData)
self.worker.start()
def newData(self, data):
print('start:', time.time())
for v in data:
self.t = np.append(self.t[1:], v[0])
self.x = np.append(self.x[1:], v[1])
self.drawplot.setData(self.t, self.x)
print('end:', time.time())
You won't get a 1ms update, but there would be no need for it anyway; also, remember that printing at that rate will always affect the performance in some way.
Finally, there is no advantage in setting PreciseTimer with a 1ms interval, since the timer accuracy is about 1ms on most platforms anyway (as explained at the beginning of the same paragraph in the documentation linked before), and setting the precision is only required for longer intervals (I'd say at least 25-50ms).
There's also an interesting answer about QTimer here, explaining what basically happens whenever you create one and it timeouts.

Related

How to use QElapsedTimer without a QTimer?

When I tried to use QElapsedTimer to clear a text in a label I couldn't find a way of using it without a QTimer. Is there a way of connecting a method so that it will do something when the QElapsedTimer reaches a certain value? To be more specific, I want to clear the text I set to lblSendError using the print_username() method after 5 seconds ahve passed. Here I have used the clear_username() method to clear it. Right now I have connected it to a QTimer so that it would run periodically.
This is my current implementation of QElapsedTimer in my code:
class win2(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
uic.loadUi('designs/win2.ui', self)
self.butPrevious.clicked.connect(self.goto_page1)
self.butSend.clicked.connect(self.print_username)
self.clearTimerE = QtCore.QElapsedTimer()
print(dir(self.clearTimerE))
self.clearTimer = QtCore.QTimer()
self.clearTimer.setInterval(1000)
self.clearTimer.timeout.connect(self.clear_username)
def goto_page1(self):
self.hide()
w1.show()
def print_username(self):
self.lblSendError.setText(w1.textUsername.toPlainText())
self.clearTimerE.start()
self.clearTimer.start()
def clear_username(self):
print(self.clearTimerE.elapsed())
if self.clearTimerE.elapsed() >= 4000:
self.lblSendError.setText('')
#self.clearTimerE.restart()
self.clearTimer.stop()
You only need a single-shot timer for this - an elapsed timer isn't needed. Also, I would advise putting all the code for this in your main-window class, as this will make it much easier to access the instances of the other two classes.
Here is a solution based on the code from your previous question:
class MainApp(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('designs/win_stacked.ui', self)
self.win_1 = win1()
self.win_2 = win2()
self.stackedWidget.addWidget(self.win_1)
self.stackedWidget.addWidget(self.win_2)
self.stackedWidget.setCurrentWidget(self.win_2)
self.win_2.butPrevious.clicked.connect(self.goto_page1)
self.win_2.butSend.clicked.connect(self.print_username)
self.clearTimer = QtCore.QTimer()
self.clearTimer.setSingleShot(True)
self.clearTimer.setInterval(5000)
self.clearTimer.timeout.connect(self.clear_username)
def goto_page1(self):
self.stackedWidget.setCurrentWidget(self.win_1)
def print_username(self):
self.win_2.lblSendError.setText(self.win_1.textUsername.toPlainText())
self.clearTimer.start()
def clear_username(self):
self.win_2.lblSendError.setText('')
To expand a bit on #ekhumoro's answer. QElapsedTimer is meant to act like a stopwatch. It only tells you how many milliseconds have elapsed since start(). It does not have a timeout signal like QTimer
timer = QElapsedTimer()
timer.start()
print(timer.elapsed()) # should be very close to zero.
time.sleep(5)
print(timer.elapsed()) # should be very close to 5000

In QThread: calls a function again 1 second after the function finishes

I want to acquire a spectrum of a spectrometer every 1s, and below is the code I wrote for this.
However, when the spectrometer is triggered at less than 1 Hz,
the function asking for a spectrum will wait for a trigger and take longer than 1s to process, while the timer has already timed out.
This cause the thread to be very laggy and eventually freeze.
What is a better way to write it so that it calls the function 'acquire_spectrum' 1s after the function finishes itself, instead of just calling it every 1s?
class Spec_Thread(QThread):
def __init__(self):
QThread.__init__(self)
self.signals = Signals()
self.specth = Spectrometer.from_first_available() #connect to the spectrometer
self.threadtimer = QTimer()
self.threadtimer.moveToThread(self)
self.threadtimer.timeout.connect(self.acquire_spectrum)
def acquire_spectrum(self): #acquire the current spectrum from the spectrometer
print('in thread,', device_running)
if device_running == True:
self.specth.open() #open the usb portal
self.wavelengths = self.specth.wavelengths() #acquire wavelengths (will wait for a trigger)
self.intensities = self.specth.intensities() #acquire intensities (will wait for a trigger)
self.specth.close() #close usb portal
self.signals.new_spectrum.emit(self.wavelengths, self.intensities)
else:
print('Device stopped')
return
def run(self):
self.threadtimer.start(10000)
loop = QEventLoop()
loop.exec_()
You have to start a timer after finishing the task execution, and for this you can threading.Timer():
import threading
from PyQt5 import QtCore
import numpy as np
from seabreeze.spectrometers import Spectrometer
class QSpectrometer(QtCore.QObject):
dataChanged = QtCore.pyqtSignal(np.ndarray, np.ndarray)
def __init__(self, parent=None):
super().__init__(parent)
self.specth = Spectrometer.from_first_available()
def start(self, repeat=False):
threading.Thread(target=self._read, args=(repeat,), daemon=True).start()
def _read(self, repeat):
self.specth.open()
wavelengths = self.specth.wavelengths()
intensities = self.specth.intensities()
self.specth.close()
self.dataChanged.emit(wavelengths, intensities)
if repeat:
threading.Timer(1, self._read, (repeat,)).start()
if __name__ == "__main__":
import sys
app = QtCore.QCoreApplication(sys.argv)
spectometer = QSpectrometer()
spectometer.start(True)
spectometer.dataChanged.connect(print)
sys.exit(app.exec_())

Is it possible to write to a QBuffer that's currently being read?

I'm writing a PyQt5 application, but I think this question is valid for PySide2 and Qt as well. I'm trying to write sound data (sinuosids) to a buffer, then play it on a seamless loop. However, there is always a break when I get to the end of the buffer and seek back to the beginning.
I think I want to continuously read and write to the same buffer, is this possible?
Below is a minimal version of my code:
import struct
import sys
from PyQt5.QtCore import QBuffer, QByteArray, QIODevice
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtMultimedia import QAudio, QAudioFormat, QAudioOutput
sample_rate = 44100
sample_size = 16
frequency = 1000
volume = 3276
class Window(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
format = QAudioFormat()
format.setChannelCount(1)
format.setSampleRate(sample_rate)
format.setSampleSize(sample_size)
format.setCodec("audio/pcm")
format.setByteOrder(QAudioFormat.LittleEndian)
format.setSampleType(QAudioFormat.SignedInt)
self.output = QAudioOutput(format, self)
self.output.stateChanged.connect(self.replay)
self.buffer = QBuffer()
self.buffer.open(QIODevice.ReadWrite)
self.createData()
self.buffer.seek(0)
self.output.start(self.buffer)
def createData(self):
print("writing")
data = QByteArray()
for i in range(round(1 * sample_rate)):
t = i / sample_rate
value = int(volume * sin(2 * pi * frequency * t))
data.append(struct.pack("<h", value))
self.buffer.write(data)
def replay(self):
print("replaying", self.output.state(), QAudio.IdleState)
if self.output.state() == QAudio.IdleState:
self.buffer.seek(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
I think you've slightly misunderstood how QAudioOutput (and audio device objects in general) behaves, reads and plays audio data.
When you play() a QIODevice, the QAudioOutput instance reads a chunk of data according to the audio device buffer setting (but it's not always the same as bufferSize()) and "sends" it to the hardware device which actually plays it: reading data and "playing" are asynchronous. What play() does is to call QIODevice.readData(maxLen), where maxLen is some data length the audio device needs in order to ensure that the audio buffer is continuously filled, otherwise you'll get a buffer underrun, meaning that the device is trying to play but has no data to do it.
In your case it also means that at a certain point the audio device could request some data to the data buffer over its length, so you'll need to add more data to return.
Also, if you wait for the stateChanged signal, it means that there is no more data to read from the data buffer (which is not the audio device buffer); at this point, QAudioDevice stops the audio device and clears its buffer, so if you "replay", you will obviously hear a gap, as the device is being "restarted".
If you want to play some data in loop, you will need to implement your own QIODevice, as it has to continuously feed the audio device once it's reached its end.
Please note that this is a minimal example, you might want to further implement writing to the data buffer (and update its seek position)
class AudioBuffer(QIODevice):
def __init__(self):
QIODevice.__init__(self)
self.bytePos = 0
self.data = QByteArray()
for i in range(round(1 * sample_rate)):
t = i / sample_rate
value = int(volume * sin(2 * pi * frequency * t))
self.data.append(struct.pack("<h", value))
def seek(self, pos):
self.bytePos = pos
return True
def readData(self, maxLen):
data = self.data[self.bytePos:self.bytePos + maxLen]
if len(data) < maxLen:
# we've reached the end of the data, restart from 0
# so the wave is continuing from its beginning
self.bytePos = maxLen - len(data)
data += self.data[:self.bytePos]
else:
self.bytePos += maxLen
return data.data()
class Window(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QHBoxLayout()
self.setLayout(layout)
self.playButton = QPushButton('Play')
self.playButton.setCheckable(True)
self.playButton.toggled.connect(self.togglePlay)
layout.addWidget(self.playButton)
format = QAudioFormat()
format.setChannelCount(1)
format.setSampleRate(sample_rate)
format.setSampleSize(sample_size)
format.setCodec("audio/pcm")
format.setByteOrder(QAudioFormat.LittleEndian)
format.setSampleType(QAudioFormat.SignedInt)
self.output = QAudioOutput(format, self)
self.output.stateChanged.connect(self.stateChanged)
self.buffer = AudioBuffer()
self.buffer.open(QIODevice.ReadWrite)
def togglePlay(self, state):
self.buffer.seek(0)
if state:
self.output.start(self.buffer)
else:
self.output.reset()
def stateChanged(self, state):
self.playButton.blockSignals(True)
self.playButton.setChecked(state == QAudio.ActiveState)
self.playButton.blockSignals(False)
That said, I've played a bit with QAudioDevice and I'm afraid it's not very reliable, at least under PyQt/PySide. While it works fine for small examples and simple cases, it becomes unreliable if you need to do something else that require some processing while playing audio (for example complex widget/QGraphics paintings), and using QThreads won't help you as you would think: for example, under MacOS you cannot moveToThread() a QAudioOutput.
I strongly suggest you to use PyAudio, which has classes that behave in a similar way as QAudioOutput but can work in a different thread. Obviously, if you still need continuous playing, the "readData" issue remains the same, as you'll need some data object that can cycle itself.
PS: The title of this question is a bit off the topic at hand, you might think about changing it. Btw, the answer is no, as reading and writing of a IODevice cannot be concurring: reading should "lock" from writing (but not from further reading) and viceversa, and both operations internally move the seek pos of the IODevice, but since you're not dealing with threads that's not the point at all, also because in your example you've already finished writing data to the buffer before even starting to read from it, and you don't write anything after.
I don't currently have PyQt set up to test it myself, but try the following:
Use the QAudioOutput::notify() signal. Calculate the duration of the buffer's audio in milliseconds. Use that as the interval with setNotifyInterval(). Connect notify instead of stateChanged to your replay method. Do not check for QAudio.IdleState, just rewind the buffer.

GUI with pyqtgraph never refresh

I have been working on a GUI for the beagle bone black that launch a thread when a button is clicked and starts to get data through the SPI.
This function is inside a class called Scanner(QObject) and runs in a different thread when the Start button is clicked.
def scan (self):
thread_name = QThread.currentThread().objectName()
self.sig_msg.emit('Scanning '+thread_name)
for step in range(nsamples):
data = self.read_reg(reg[thread_name])
self.sig_data.emit(step, data)
QThread.currentThread().msleep(50)
app.processEvents()
if self.__abort:
self.sig_msg.emit('scan stopped by user')
break
self.sig_done.emit(thread_name)
sig_msg is a pyqtsignal connected to the following function inside the GUI thread.
#pyqtSlot(int, int)
def on_scaner_data(self, t: int, y: int):
app.processEvents()
self.debugBox.insertPlainText('t: '+str(t)+'y: '+str(y)+'\n')
self.debugBox.ensureCursorVisible()
self.MainGraph.update_fig(t,y)
And finally the MainGraph.update_fig() is called. Inside that function i have used setData(self.datat,self.datay) and app.processEvents() for update the graph, but nothing changes. If i run plot(self.datat,self.datay) instead it redraws the graph but causes a huge performance hit.
class DynamicPlotter(PlotWidget):
def __init__(self,parent=None):
PlotWidget.__init__(self)
self.setParent(parent)
# Use getPlotItem() to get the PlotItem inside PlotWidget.
self.pitem = self.getPlotItem()
#now pitem is our PlotItem
self.pitem.curve=self.pitem.plot()
#curve is a new PlotDataItem added by PlotItem.plot()
self.datat = [1,2]
self.datay = [1,2]
self.pitem.curve.setData(self.datat,self.datay)
#this graph works fine
self.datat = []
self.datay = []
def update_fig(self,t:int,y:int):
self.datat.append(t)
self.datay.append(y)
#it works
self.pitem.curve=self.pitem.plot(self.datat,self.datay)
#it doesn't
self.pitem.curve.setData(self.datat,self.datay)
app.processEvents()
print (self.datat)
log.debug(str(t)+str(y))
def reset_figure(self):
log.debug('clean graph')
self.clear()
I have been following this example from the pyqtplot and my idea was do something similar inside my GUI.
import initExample
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
from pyqtgraph.ptime import time
app = QtGui.QApplication([])
p = pg.plot()
p.setWindowTitle('pyqtgraph example: PlotSpeedTest')
p.setRange(QtCore.QRectF(0, -10, 5000, 20))
p.setLabel('bottom', 'Index', units='B')
curve = p.plot()
data = np.random.normal(size=(50,5000))
ptr = 0
lastTime = time()
fps = None
def update():
global curve, data, ptr, p, lastTime, fps
curve.setData(data[ptr%10])
ptr += 1
now = time()
dt = now - lastTime
lastTime = now
if fps is None:
fps = 1.0/dt
else:
s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s
p.setTitle('%0.2f fps' % fps)
app.processEvents() ## force complete redraw
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)
I have been reading the documentation and right know I'm not sure where is/are the problems. I bet for the threads or the event loop handler but i don't know.
Which are the critical points that i have to review?
Any clue?
Thank you.
After a while i have found the problem by myself.
I fixed the problem changing the way that i was using to reset the graph and stopping the scan thread until elements are plot.
Changes on reset function. self.clear() remove the traces on the PlotWidget and that wasn't what i needed.
def reset_figure(self):
log.debug('clean graph')
self.datat =[]
self.datay=[]
self.pitem.curve.setData(self.datat,self.datay)
Scan was modified to stop while the data is plotted in the other thread. sync_to_plot stop the thread execution until self._wait=False. This value is changed by wait_state.
def scan (self):
thread_name = QThread.currentThread().objectName()
#thread_id = str(QThread.currentThreadId())#review
self.sig_msg.emit('Scanning '+thread_name)
for step in range(nsamples):
data = self.read_reg(reg[thread_name])
self.sig_data.emit(step, data)
#pause while plot values
self.sync_to_plot()
if step % refrate == 0:
log.debug("%5d : %d" % (step, data) )
if self.__abort:
self.sig_msg.emit('scan stoped by user')
break
self.sig_done.emit(thread_name)
def sync_to_plot(self):
self._wait=True
while self._wait:
log.debug("waiting")
QThread.currentThread().msleep(1)
app.processEvents()
def wait_state(self, stat):
self._wait=stat
With that done the last change was on the on_scaner_data that unblocks the thread that is waiting on sync_to_plot.
#pyqtSlot(int, int)
def on_scaner_data(self, t: int, y: int):
app.processEvents()
self.debugBox.insertPlainText('t: '+str(t)+'y: '+str(y)+'\n')
self.debugBox.ensureCursorVisible()
self.MainGraph.update_fig(t,y)
for thread, scaner in self.__threads:
scaner.wait_state(False)
log.debug("scanner false")

How to run a function periodically in python

I have a simple metronome running and for some reason when it is at a lower bpm it is fine, but at higher bpms it is inconsistent and isnt steady.
I don't know what is going on.
I want to try using something to run it periodically. Is there a way to do that?
Here is my code:
class thalam():
def __init__(self,root,e):
self.lag=0.2
self.root=root
self.count=0
self.thread=threading.Thread(target=self.play)
self.thread.daemon=True
self.tempo=60.0/120
self.e=e
self.pause=False
self.tick=open("tick.wav","rb").read()
self.count=0
self.next_call = time.time()
def play(self):
if self.pause:
return
winsound.PlaySound(self.tick,winsound.SND_MEMORY)
self.count+=1
if self.count==990:
self.thread=threading.Thread(target=self.play)
self.thread.daemon=True
self.thread.start()
return
self.next_call+=self.tempo
new=threading.Timer(self.next_call-time.time(),self.play)
new.daemon=True
new.start()
def stop(self):
self.pause=True
winsound.PlaySound(None,winsound.SND_ASYNC)
def start(self):
self.pause=False
def settempo(self,a):
self.tempo=a
class Metronome(Frame):
def __init__(self,root):
Frame.__init__(self,root)
self.first=True
self.root=root
self.e=Entry(self)
self.e.grid(row=0,column=1)
self.e.insert(0,"120")
self.play=Button(self,text="Play",command=self.tick)
self.play.grid(row=1,column=1)
self.l=Button(self,text="<",command=lambda:self.inc("l"))
self.l.grid(row=0,column=0)
self.r=Button(self,text=">",command=lambda:self.inc("r"))
self.r.grid(row=0,column=2)
def tick(self):
self.beat=thalam(root,self.e)
self.beat.thread.start()
self.play.configure(text="Stop",command=self.notick)
def notick(self):
self.play.configure(text="Start",command=self.tick)
self.beat.stop()
def inc(self,a):
if a=="l":
try:
new=str(int(self.e.get())-5)
self.e.delete(0, END)
self.e.insert(0,new)
self.beat.settempo(60.0/(int(self.e.get())))
except:
print "Invalid BPM"
return
elif a=="r":
try:
new=str(int(self.e.get())+5)
self.e.delete(0, END)
self.e.insert(0,new)
self.beat.settempo((60.0/(int(self.e.get()))))
except:
print "Invalid BPM"
return
Playing sound to emulate an ordinary metronome doesn't require "real-time" capabilities.
It looks like you use Tkinter framework to create the GUI. root.after() allows you to call a function with a delay. You could use it to implement ticks:
def tick(interval, function, *args):
root.after(interval - timer() % interval, tick, interval, function, *args)
function(*args) # assume it doesn't block
tick() runs function with given args every interval milliseconds. Duration of individual ticks is affected by root.after() precision but in the long run, the stability depends only on timer() function.
Here's a script that prints some stats, 240 beats per minute:
#!/usr/bin/env python
from __future__ import division, print_function
import sys
from timeit import default_timer
try:
from Tkinter import Tk
except ImportError: # Python 3
from tkinter import Tk
def timer():
return int(default_timer() * 1000 + .5)
def tick(interval, function, *args):
root.after(interval - timer() % interval, tick, interval, function, *args)
function(*args) # assume it doesn't block
def bpm(milliseconds):
"""Beats per minute."""
return 60000 / milliseconds
def print_tempo(last=[timer()], total=[0], count=[0]):
now = timer()
elapsed = now - last[0]
total[0] += elapsed
count[0] += 1
average = total[0] / count[0]
print("{:.1f} BPM, average: {:.0f} BPM, now {}"
.format(bpm(elapsed), bpm(average), now),
end='\r', file=sys.stderr)
last[0] = now
interval = 250 # milliseconds
root = Tk()
root.withdraw() # don't show GUI
root.after(interval - timer() % interval, tick, interval, print_tempo)
root.mainloop()
The tempo osculates only by one beat: 240±1 on my machine.
Here's asyncio analog:
#!/usr/bin/env python3
"""Metronome in asyncio."""
import asyncio
import sys
async def async_main():
"""Entry point for the script."""
timer = asyncio.get_event_loop().time
last = timer()
def print_tempo(now):
nonlocal last
elapsed = now - last
print(f"{60/elapsed:03.1f} BPM", end="\r", file=sys.stderr)
last = now
interval = 0.250 # seconds
while True:
await asyncio.sleep(interval - timer() % interval)
print_tempo(timer())
if __name__ == "__main__":
asyncio.run(async_main())
See Talk: Łukasz Langa - AsyncIO + Music
Doing anything needing time precision is very difficult due to the need for the processor to share itself with other programs. Unfortunately for timing critical programs the operating system is free to switch to another process whenever it chooses. This could mean that it may not return to your program until after a noticeable delay. Using time.sleep after import time is a more consistent way of trying to balance the time between beeps because the processor has less "reason" to switch away. Although sleep on Windows has a default granularity of 15.6ms, but I assume you will not need to play a beat in excess 64Hz. Also it appears that you are using multithreading to try and address your issue, however, the python implementation of threading sometimes forces the threads to run sequentially. This makes matters even worse for switching away from your process.
I feel that the best solution would be to generate sound data containing the metronome beep at the frequency desired. Then you could play the sound data in a way the OS understands well. Since the system knows how to handle sound in a reliable manner your metronome would then work.
Sorry to disappoint but timing critical applications are VERY difficult unless you want to get your hands dirty with the system you are working with.
I would like to tell you that you can't be precise with threads in case of timing because of race conditions (even when you are using locks and semaphores!). Even I have faced the problem.

Categories