2

I am trying to make a very simple metronome on a Raspberry Pi that plays a .wav file at a set interval, but the timing is audibly inaccurate. I really can't figure out why, is python's time module that inaccurate?

I don't think the code that handles playing the audio is the bottleneck since if I put it in a loop with no timer it will rattle consistently. With the simple code below, the sound will play on beat a few times and then one beat will be off randomly, over and over.

import pygame
from time import sleep

pygame.mixer.pre_init(44100, -16, 2, 2048)
pygame.mixer.init()
pygame.init()

BPM = 160
sound = pygame.mixer.Sound('sounds/hihat1.wav')


while True:
    sound.play()
    sleep(60/BPM)

I expect to get the sound to repeat every X milliseconds with an accuracy of at least +/-10ms or so. Is that unrealistic? If so please suggest an alternative.

Louis T
  • 75
  • 8
  • Maybe try it with a different audio file? Just to make sure there's not an issue somewhere with `sounds/hihat1.wav`. – Calvin Godfrey Jan 17 '19 at 13:26
  • I did, same issue unfortunately – Louis T Jan 17 '19 at 13:47
  • Hmm... perhaps try `sleep(60/BPM - sound.get_length())` so that the pause accounts for the length of the audio file – Calvin Godfrey Jan 17 '19 at 13:49
  • This is an almost duplicate of https://stackoverflow.com/questions/28528935/timing-issues-in-metronome-script-in-python-using-pygame and https://stackoverflow.com/questions/26563033/pygame-time-set-timer-4-2-the-floor-click/ - not an _exact_ dup so I'm not voting to close for now but if you find one of those links solve your question please leave a comment so we can close this one as duplicate. – bruno desthuilliers Jan 17 '19 at 14:31
  • As a side note: `sleep()` is definitly **not** the correct tool for timing-critical code, cf the documentation (https://docs.python.org/2/library/time.html#time.sleep): "he actual suspension time may be less than that requested because any caught signal will terminate the sleep() following execution of that signal’s catching routine. Also, the suspension time may be longer than requested by an arbitrary amount because of the scheduling of other activity in the system." – bruno desthuilliers Jan 17 '19 at 14:34

2 Answers2

3

the issue turned out to be using overly large chunk sizes which likely caused pygame to play sounds late as earlier chunks had already been queued. my first suggestion was that I'd expected the OP's code to slowly drift over time, suggesting that something like this would do better:

import pygame
from time import time, sleep
import gc

pygame.mixer.pre_init(44100, -16, 2, 256)
pygame.mixer.init()
pygame.init()

BPM = 160
DELTA = 60/BPM

sound = pygame.mixer.Sound('sounds/hihat1.wav')
goal = time()

while True:
    print(time() - goal)
    sound.play()
    goal += DELTA
    gc.collect()
    sleep(goal - time())

i.e. keep track of the "current time" and adjust sleeps according to how much time has elapsed. I explicitly perform a "garbage collect" (i.e. gc.collect()) before each sleep to keep things more deterministic.

Sam Mason
  • 15,216
  • 1
  • 41
  • 60
  • Thanks for the suggestions, I ran your code and still have the same issue. Beats me. I'll try the same example in bash or nodejs and see if that works. – Louis T Jan 17 '19 at 14:19
  • @LouisT I've added the garbage collect and a print to see how far out the process thinks it is… might help? my laptop (not an RPI) is always within 1ms (and sounds pretty good to my ear) – Sam Mason Jan 17 '19 at 14:24
  • I think bash would be much worse, node might be better but I doubt it. I think you'd need to go to a low level language like C or C++ to get this more deterministic – Sam Mason Jan 17 '19 at 14:25
  • I'll run the code on my desktop to see if the rpi is the culprit, good idea – Louis T Jan 17 '19 at 14:29
  • 2
    just thought, you might want to reduce the buffer size quite a bit as well — hardcoded limit seems to be [256 samples](https://github.com/pygame/pygame/blob/master/src_c/mixer.c#L388), but I'm not sure how pygame handles playing samples when a buffer is queued/inflight – Sam Mason Jan 17 '19 at 14:37
  • You're a genius! That's what the issue was, I lowered the buffer size and now everything works fine. Thank you. – Louis T Jan 17 '19 at 14:50
  • @SamMason suggest updating the answer. – BoboDarph Jan 17 '19 at 14:53
1

When I tested your code on my local machine it seems like the sleep doesnt care about the pygame thread so you will get your sound overlapping itself.

Furthermore I think you should use pygames own timer for delayed actions.

Can you try the following code on your py?

import pygame

pygame.mixer.pre_init(44100, -16, 2, 2048)
pygame.mixer.init()
pygame.init()

BPM = 160
sound = pygame.mixer.Sound('sounds/hihat1.wav')

while True:
    sound.play()
    pygame.time.delay(int(sound.get_length()*1000))
    pygame.time.delay(int(60/BPM*1000))
BoboDarph
  • 2,751
  • 1
  • 10
  • 15
  • The sound itself is very short, if the sound you used is longer than the interval you'll get overlapping. You can replace `sound.play()` with `pygame.mixer.Channel(0).play(sound)` and the sound will cut itself if it's too long – Louis T Jan 17 '19 at 14:03
  • I think it's because the pygame code is on another thread and time.sleep might be screwing you there. That's why they have the delay function in pygame. Check the updated code please. – BoboDarph Jan 17 '19 at 14:04
  • I just ran your updated code and same thing, some beats are still audibly off – Louis T Jan 17 '19 at 14:06
  • I forgot the delay in pygame is in miliseconds. The last edit should fix it. – BoboDarph Jan 17 '19 at 14:14
  • Thank you for the help, unfortunately I just ran it and still doesn't work. If I do this it seems that the 5th beat is always off, which doesn't make sense to me: `pygame.time.delay(int(60/BPM*1000) - int(sound.get_length()*1000))` – Louis T Jan 17 '19 at 14:22