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.