How to run a function periodically in python

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

Leave a Comment