Precise loop timing in Python

2020-07-29 09:05发布

问题:

For this project I'm designing a sequencer/drummachine that should be able to send MIDI notes with a precise tempo. Example: 16 notes per 2 seconds (i.e. in music terminology sixteen 1/16-notes per bar at BPM 120), i.e. one note every 125 milliseconds.

I'm thinking about:

import time

def midi_note_send(...):
    ....

while True:
    midi_note_send(...)
    time.sleep(0.125)

If I do like this, how to be sure it will be exactly 125 milliseconds? Isn't there a risk that 1000 iterations of this loop will be using 126 seconds instead of 125 seconds? If so, how to have a more precise loop?

Final note: a good drummachine should be able to keep a 120 BPM rythm during 1 hour, with a precision error < 1 second.
Used platform: Linux + RaspberryPi but this question is valid in general.

回答1:

As I showed here

import time
def drummer():
    counter = 0
    # time.sleep(time.time() * 8 % 1 / 8) # enable to sync clock for demo
    while counter < 60 * 8:
        counter += 1
        print time.time()
        time.sleep(.125 - time.time() * 8 % 1 / 8)

This timer adjusts every beat and realigns.

And the adjustment takes almost no time:

timeit.timeit("time.time() * 8 % 1 / 8", number=1000000)
0.2493131160736084

which means every time it does that it takes about 0.25 microseconds

and for accuracy:

1488490895.000160
1488490895.125177
1488490895.250167
1488490895.375151
1488490895.500166
1488490895.625179
1488490895.750178
1488490895.875153

~28 microseconds of drift between notes. Running it locally for a longer time yields ~130μs of total drift (+- 65μs), but, as it syncs to the clock every beat, it won't deviate over time.



回答2:

You could use absolute time (from time.time()) to calculate your sleep time.

starttime = time.time()
for i in range(100):
    midi_note_send(...)
    sleep_duration = (i + 1) * 0.125 - time.time() + starttime
    time.sleep(sleep_duration)


回答3:

At the very least, you should account for the computation time of midi_note_send

import time

# Define a generator for timing
def next_time(t0, dt):
    while 1:
        t0 += dt
        yield t0

# Initialize timer and start loop
timer = next_time(time.time(), 0.125)
while True:
    midi_note_send(...)
    time.sleep(next(timer) - time.time())


回答4:

Here's an alternative that should maintain the beat frequency as accurately as your computer's clock allows, for as long as you want. It comes at the expense of individual beats being up to 1ms off, which is probably worse, but this is just to demonstrate the alternative:

# Time between beats
beat_length = 0.125

# Send first beat and initialize next beat time
midi_note_send()
next_beat = time.time() + beat_length

while True:
    # Poll the time in 1ms increments until enough time has elapsed
    while time.time() < next_beat:
        time.sleep(0.001)
    # Send the next note
    midi_note_send()
    # Increment the next beat time
    next_beat += beat_length

You can increase the accuracy of individual beats by changing the sleep time (to 0.0001 for 0.1ms accuracy, for example), at the expense of CPU use as you poll the time more often.



回答5:

Expanding on other answers which all suggest taking into account the time taken to process your note, here is a rate limiter that can be easily built into a class. It also avoids trying to sleep if the rate required is very close the elapsed time of the processing function.

def rate_limit(rate, previous=0):
   duration = time.time() - previous
   if duration > 0: 
      sleep_time = 1.0 / rate - duration
      if sleep_time > 0.0:
         time.sleep(sleep_time)
    return time.time()

previous = 0
for i in range(120):
   midi_note_send(...)
   previous = rate_limit(120, previous)