I have a school project that involves making a simple rhythm game in python. I am currently trying to synchronise a flashing dot to a song of a known bpm (120 in this case) using pygame.
The flashing dot is synchronised at first but slowly desyncs with time. I have searched on internet how could a rhythm game be made and I found that I needed to do an audio latency test. However, the code that i came up with is not really working and so I would really like some help with that. I already tried removing the gap at the beginning of my mp3 and exported it as an ogg with no metadata.
import sys, pygame, math, random, time
pygame.init()
size = width, height = 1000, 1000
speed = [2, 2]
black = 0, 0, 0
screen = pygame.display.set_mode(size)
pygame.display.update()
pygame.mixer.init()
pygame.mixer.music.load("metr.ogg")
continuer = True
bpm = 120
t = 60 / bpm
font = pygame.font.Font('freesansbold.ttf', 15)
lasttime = 0
delay = 0
red = (200, 0, 0)
green = (0, 200, 0)
colors = [red, red, red, red]
i = 0
av = []
drawn = False
clock = pygame.time.Clock()
pygame.mixer.music.play()
while continuer:
time = (pygame.mixer.music.get_pos()) / 1000
nextkey = lasttime + t + (delay / 1000)
if time > nextkey:
if drawn:
screen.fill((0, 0, 0))
drawn = False
else:
pygame.draw.circle(screen, colors[i], (500, 500), 20)
drawn = True
if i == 3:
i = 0
else:
i += 1
lasttime += t
screen.blit(font.render('delay = ' + str(delay) + "ms", True, (255, 255, 255), (0, 0, 0)), (500, 600))
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
delay += 1
elif event.key == pygame.K_h:
av.append(nextkey - time)
delay = sum(av) / len(av)
elif event.key == pygame.K_LEFT:
delay -= 1
elif event.key == pygame.K_SPACE:
continuer = False
pygame.display.flip()
clock.tick(60)
pygame.quit()
The file metr.ogg is just a metronome at 120bpm
It's because your variable
nextkeyis a summation of different variables, each of which will contain a small error. So you're always adding on small errors until it eventually becomes noticeable.The way to prevent accumulative errors is to always do calculations directly from the source. In your case
pygame.mixer.music.get_pos().In the example below, we first create a variable
ms_per_beat. This value is a constant that defines how many milliseconds should pass for each beat. For each frame, we calculatetime_since_last_beatbased on how long the song has been played. A rough, incomplete example:The example above assumes that the audio starts exactly on the beat. It also assumes that it actually is 120 bpm. If it's 120.1 bpm, it'll get out of sync eventually. A more proper way would be to analyze the audio for a peak in amplitude and just display the dot then. If your audio is just a metronome, then it could be done with
pygame.mixer.music.get_volume().However, I would recommend a totally different approach. Synchronization is always a hassle, so try to avoid it whenever possible. Instead of synching your game with a metronome, let your game be the metronome. Have a single "bop" audio that you play whenever a certain time has passed. Then your audio and graphics will always be synchronized since both use the same clock. The example below plays the audio be scheduling a user event with the help of
pygame.time.set_timer.If you not comfortable with scheduling events, you could calculate the timing yourself.
Note however that pygame seems to have difficulties when it comes to playing sound at a certain timing. It works better if you decrease the buffer (as explained in the documentation).