I'm trying to add a simple metronome to my cross-platform pyQt program, but having a lot of difficulty getting accurate timing. Playing the sound seems to work the best using PyGame's sound system. I tried Phonon, but it just wasn't consistent at all and QSound doesn't work at all on my system. I've tried handling timing with python's time library and using QTimers, but QTimeLine seems to work the best.
The timing at lower tempo is not too bad... still a hiccup here and there. Higher tempos are not accurate at all however.
Any ideas/suggestions would be greatly appreciated!
Here's some code:
class Metronome(object):
def init_metronome(self):
self.metronome_timer = QtCore.QTimeLine(100000)
self.metronome_timer.valueChanged.connect(self.tick)
self.metronome_timer.setCurveShape(3) #linear curve
self.ui.pushButton_metronome.toggled.connect(self.toggle_metronome)
self.ui.spinBox_metronome_bpm.valueChanged.connect(self.set_metronome_bpm)
pygame.mixer.init()
self.sound = pygame.mixer.Sound("./sounds/tick.wav")
def toggle_metronome(self):
if self.ui.pushButton_metronome.isChecked() == True:
self.set_metronome_bpm()
self.metronome_timer.start()
else:
self.metronome_timer.stop()
def set_metronome_bpm(self):
bpm = self.ui.spinBox_metronome_bpm.value()
self.metronome_timer.setUpdateInterval(60./bpm * 1000)
def tick(self):
QtCore.QCoreApplication.processEvents()
self.sound.play()
I am not very familiar with the interaction with Python and Qt, but I do have experience with timers and threads and Qt.
General Timing Limitations
The timing in Qt is generally made for graphical animations. In Qt Timers docs, it says that the timers are accurate to about 15 ms, but it depends on the platform. In windows, if you read through their documentation on timers and the system clock it says they are accurate to about 11 to 16 ms.
Thread Priority
Also the priority of threads will affect the outcome of your metronome. You should probably look into setting the priority of your thread to time critical (see QThread::Priority
), so that the system gives you better timing and then tell your function to do a sleep(0) or yieldCurrentThread()
call after you finish playing your sound.
QObject Connect Calls
Connect calls in Qt are done using Qt::AutoConnection
, which means Qt decides whether to connect it using the event queue or using a direct call. For timing, a direct call is preferable.
Also, do the processEvents call after playing the sound or not at all. Doing it beforehand tells Qt that you want anything else that is sitting in the Event Queue to get processed before your next call is reached. Check out the documentation on the qt event system.
MIDI Files
Most of the generated sounds in games that I am aware of are done using MIDI files. They are tiny and the rendering of their sound loops are pretty consistent on many platforms. Maybe you could pull something off selecting one of 100 midi files. There are also projects out there like ScaleTempo, but it seems like it is kind of old (last updated on 2008).
Hope that helps.
Disclaimer: Most of the links I posted are from Qt 5. I've been programming mostly in Qt 4.7, but the timers and event system were not scheduled for revamping for Qt 5 (mostly the animation backend optimizations) as far as I am aware.