I'm trying to wrtie a metronome script which gives me audio feedback as well as sending MIDI messages to a synthesizer. I use Python 2.7.5+ and Pygame 1.9.1.release on Linux Mint 16. I'm okay with the MIDI part. What troubles me is the timing.
First of all, here is basically what I do:
import time
import pygame.mixer
pygame.mixer.init()
sound = pygame.mixer.Sound('click.wav')
interval = 0.5 #should be equivalent to 120 bpm
t0 = time.time()
while True: #infinite loop, use keyboard interrupt Ctrl-C
if time.time() - t0 >= interval:
sound.play()
t0 = time.time()
However, this is fairly unstable and impossible to play an instrument to.
I also looked into time.clock():
import time
import pygame.mixer
pygame.mixer.init()
sound = pygame.mixer.Sound('click.wav')
interval = 0.5 #should be equivalent to 120 bpm
t0 = time.clock()
while True:
if time.clock() - t0 >= interval:
sound.play()
t0 = time.clock()
...as well as pygame.time.Clock(), specifically pygame.time.Clock.tick_busy_loop(), which supposedly provides better precision by eating more into the processor:
import pygame.time
import pygame.mixer
pygame.mixer.init()
#pygame.time has no init function
sound = pygame.mixer.Sound('click.wav')
clock = pygame.time.Clock()
interval = 0.5 #should be equivalent to 120 bpm
time_passed = 0
clock.tick_busy_loop()
while True: #infinite loop, use keyboard interrupt Ctrl-C
time_passed += clock.tick_busy_loop()
if time_passed >= interval:
sound.play()
time_passed = 0
All of which yielded the same issues. Although, I have to admit, when I tried these minimal examples, the pygame.time solution was very stable. Problem here is that in my actual script I do some calculation (like incrementing counters and sending MIDI messages) within the while loop which appear to influence the time the loop takes and the click start to stutter and stumble.
I looked at this answer, but I didn't quite get the idea of the midi files. May that would help here? Some explanation would be most helpful.
Moreover, I tried to put
import os
os.nice(-19)
at the beginning, to gain a higher process priority, but no improvement was noticeable.
I wonder how I can achive a stable metronome like common DAWs like Cubase, Ableton and Logic do. I read about the approach to generate or prerecord audio samples of the tempo in question and chain those samples to achieve a higher prescision, but I would really like to avoid this approach as it seems very laborious.
Is there some clever way I may use the stable pygame.time variant and do the processing elsewhere?
If it is of any use: here is the actual while loop I'm running:
bpm = 80.0
beats_in_one_second = bpm/60.0
midi_send_interval = beats_in_one_second/24.0 #MIDI convention
playing = True
player.write_short(250) #start
frame_count = 0
quarter_count = 0
time_0 = time.time()
while playing:
if time.time() - time_0 >= midi_send_interval:
player.write_short(248) #clock tick
time_0 = time.time()
frame_count += 1
if frame_count == 24:
sound.play()
quarter_count += 1
frame_count = 0
if quarter_count == 16:
playing = False
player.write_short(252) #stop
The problem is that the MIDI standard demands for 24 messages per quarter note, which increases the demand for precision significantly.
sound.play()periodically (say no more than 60 times per second) then you could try the code example from my answer to related question. Here's a couple of related code examples: a metronome skeleton in tkinter and scripts that generate data at a constant rate.