Как периодически запускать функцию в python

У меня есть простой метроном, и по какой-то причине, когда он находится на более низком удвоении, это нормально, но при более высоких значениях bpms это противоречиво и не устойчиво. Я не знаю, что происходит. Я хочу попробовать что-то, чтобы запускать его периодически. Есть ли способ сделать это?

Вот мой код:

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

Ответ 1

Выполнение чего-либо, требующего точности во времени, очень сложно из-за необходимости того, чтобы процессор делился с другими программами. К сожалению, для критически важных программ времени операционная система может переключаться на другой процесс, когда он выбирает. Это может означать, что он не может вернуться в вашу программу до тех пор, пока не произойдет заметная задержка. Использование time.sleep после времени импорта - более последовательный способ попытаться сбалансировать время между звуковыми сигналами, потому что процессор имеет меньше "разума" для переключения. Хотя сон в Windows имеет гранулярность по умолчанию 15,6 мс, но я предполагаю, что вам не нужно будет проигрывать более 64 Гц. Также кажется, что вы используете многопоточность, чтобы попытаться решить вашу проблему, однако реализация потоковой обработки python иногда заставляет потоки запускаться последовательно. Это еще более осложняет переход от вашего процесса.

Я считаю, что лучшее решение будет генерировать звуковые данные, содержащие звуковой сигнал метронома на требуемой частоте. Затем вы можете воспроизводить звуковые данные так, как хорошо понимает ОС. Поскольку система знает, как правильно обрабатывать звук, ваш метроном будет работать.

Извините, что разочаровывает, но критически важные приложения - это ОЧЕНЬ, если вы не хотите, чтобы ваши руки были грязными с системой, с которой вы работаете.

Ответ 2

Воспроизведение звука для эмулирования обычного метронома не требует возможности "реального времени".

Похоже, вы используете инфраструктуру Tkinter для создания графического интерфейса. root.after() позволяет вызывать функцию с задержкой. Вы можете использовать его для реализации тиков:

def tick(interval, function, *args):
    root.after(interval - timer() % interval, tick, interval, function, *args)
    function(*args) # assume it doesn't block

tick() запускает function с заданным args каждые interval миллисекунд. Длительность отдельных тиков зависит от точности root.after(), но в долгосрочной перспективе стабильность зависит только от функции timer().

Здесь script, который печатает некоторую статистику, 240 ударов в минуту:

#!/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()

Темп осциллирует только одним ударом: 240 ± 1 на моей машине.