pythonaudiometronome

What are the ways to improve my metronome written in Python?


I'm trying to write a python based metronome with librosa and sounddevice but I've came across some problems with it's accuracy. Here's the code:

from time import sleep, perf_counter
import librosa
import sounddevice as sd

    bpm = 200
    delay = 60/bpm
    tone = librosa.tone(440, sr=22050, length=1000)
    
    try:
        while True:
            sd.play(tone, 22050)
            sleep(delay)
    except KeyboardInterrupt:
        pass

First of all, the upper limit for properly functioning metronome seems to be around 180bpm - if you set the bpm to be any higher then 200bpm then there is no sound produced. In slower tempos I can hear that metronme is not so consistent as it should be with spacing in between the clicks. I've runned the script from this topic and my results were pretty poor compared to the author of this answer(which was using "old single core 32 bit 2GHz machine" against my six-core 3.9GHz 64bit windows running):

150.0 bpm
+0.007575200
+0.006221200
-0.012907700
+0.001935400
+0.002982700
+0.006840000
-0.009625400
+0.003260200
+0.005553100
+0.000668100
-0.010895100
+0.017142500
-0.012933300
+0.001465200
+0.004203100
+0.004769100
-0.012183100
+0.002174500
+0.002301000
-0.001611100 

So i wonder if my metronome problems are somehow correlated to these poor results and what I can do to fix it. The second problem that I encounter is the way in which the metronome is switched off - I want it to be running up until the point where the user inputs a specific button, or in my case(no GUI) a specific value from the keyboard - let's say the space key. So as you can see now it works only with ctrl + c, but I have no idea how to implement interrupt with a specified key.


Solution

  • Running your code on a mac, the timing inconsistencies are noticeable but also the tempo was of quite a bit off from the set bpm.

    This is mostly because sleep() isn't that accurate, but also because you have to account for the time that has elapsed since the last event. e.g. how much time did it take to call sd.play()

    I don't know on what operating system you did run this, but most operating systems have a special timer for precise callbacks (e.g. Multimedia Timers on Windows). if you don't want a platform specific solution to improve the timing you could do a "busy wait" instead on sleep(). To do this you could sleep for have the delay, and then go into a loop where you constantly check the time elapsed.

    lastTime = perf_counter()
    while True:
        currentTime = perf_counter()
        delta = abs(lastTime - currentTime)
        sleep(delay / 2.0)
    
        while True:
            currentTime = perf_counter()
            if (currentTime - lastTime >= delay):
                sd.play(tone, 22050)
                lastTime = currentTime
                break
    

    Not a perfect solution but it'll get you closer.

    You can further optimise the fraction of the delay that is spent sleeping to take load of the CPU.