如何在Python中定期运行函数

1 投票
3 回答
6213 浏览
提问于 2025-04-18 09:30

我有一个简单的节拍器,运行得还不错。但是当节拍(bpm)较低时,它工作得很好;而当节拍提高时,它就变得不稳定,节奏也不稳。我不知道这是为什么。

我想尝试用一些方法让它定期运行。有没有什么办法可以做到这一点呢?

这是我的代码:

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

3 个回答

0

我想告诉你,在处理线程时,时间上的精确性是很难保证的,因为会出现竞争条件(即使你使用了锁和信号量也不行!)。我自己也遇到过这个问题。

1

做任何需要精确时间的事情都很难,因为处理器需要和其他程序一起工作。对于那些对时间要求很高的程序来说,操作系统可以随时切换到其他进程,这可能导致你的程序在明显的延迟后才会被再次执行。使用 time.sleep 这个方法,在 import time 之后,可以更稳定地控制声音之间的时间间隔,因为处理器没有太多理由去切换到其他程序。虽然在Windows上,sleep的默认时间粒度是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()每隔interval毫秒就运行一次function,并传入给定的args。每个节拍的持续时间受限于root.after()的精度,但从长远来看,稳定性只依赖于timer()函数。

这里有一个脚本,可以打印一些统计信息,设置为每分钟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。

这里有一个asyncio的类似实现:

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

可以看看Talk: Łukasz Langa - AsyncIO + Music

撰写回答