是不是很好奇听的歌节奏是多少?
这里有一个用psychopy编写的bpm计数器
使用方法
运行程序后,随着音乐节奏敲击【空格键】,觉得差不多了,按下【L键】,便会显示平均敲击间隔(ms)和平均bpm。之后程序会自动打节奏给你看,用于检查计算是否准确,可按【D键】关闭柱状图以提高绘制效率。
柱状图表示每次按键间隔,每个像素表示2ms
【r】键重置记录
【q】键退出
运行示例
已验证 凤凰传奇 的 最炫民族风 bpm 是127
已验证 f.i.r 的 刺鸟 bpm 是 81
代码(复制可用)
# -*- coding: utf-8 -*-
"""
Created on Tue Sep 19 16:59:18 2017
@author: zbg
"""
from psychopy.visual import Window, ImageStim, TextStim, Rect
from psychopy import core, event, gui, clock
import random
import math
class Lyric(object):
'''
记录节奏的时间点,用回归分析
'''
def __init__(self, time_list):
"""
time = k * count + b ,k为节拍间间隔,count为第几个节拍,time为该节拍应在的时间点
由于是从0s计时的,b期望应该为0.但由于人操作有误差,后面会用回归求出b
"""
self.time_avg = self.average(time_list)
self.time_stdev = self.stdev(time_list)
count = len(time_list) + 0.00000001
self.count_avg = self.average(list(range(int(count))))
self.count_stdev = self.stdev(list(range(int(count))))
k = self.time_stdev / self.count_stdev
bpm = 60 / k
bpm = int(bpm + .5) #由于bpm是整数,所以取整后反推k
self.k = 60. / bpm
self.b = (0 - self.count_avg) * self.k + self.time_avg
def count2time(self, n):
return n * self.k + self.b
def guessratio(self, time):
'''
猜测给定time对应的节拍位置,比如半拍,四分之一拍等
'''
n = (time - self.b) / self.k
return n - int(n)
def guessn(self, time):
n = (time - self.b) / self.k
return int(n)
def average(self,lst):
s = sum(lst)
count = 0.000000001 + len(lst)
return s / count
def stdev(self,lst):
if len(lst) < 2:
return 0.000000001
avg = self.average(lst)
s = 0
for t in lst:
s += (t - avg) ** 2
return math.sqrt(s / len(lst))
class FpsCounter(object):
'''
用于计算fps,原理是记录最近n个刷屏所在时间,求1秒内的刷屏数
'''
def __init__(self, n = 10):
if n < 10:
n = 10
self.times = [0] * (n + 1)
self.n = n
def fps(self):
if (self.times[-1] - self.times[0]) <=0:
return 0
return 1 * self.n / (self.times[-1] - self.times[0])
def count(self, time):
self.times = self.times[1:] + [time]
return self.fps()
def lastfliptime(self):
return self.times[-1] - self.times[-2]
def avgfliptime(self):
if self.fps() == 0:
return 0
return 1 / self.fps()
class Bans(object):
'''
提前准备一些不同height的bans,要用的时候选一个合适的长度,放到合适的位置,draw上去
'''
def __init__(self, win, height = 1000, n = 100):
self.win = win
self.height = height
self.bans = {}
for i in range(height):
self.bans[i] = Rect(win, width = 5, height = i, units = "pix")
self.bans[i].setFillColor(color = (255, 0, 0), colorSpace = "rgb255")
self.intervals = [0] * n
def add(self, interval):
'''
interval的单位为ms
'''
self.intervals = self.intervals[1:] + [interval]
def draw(self):
'''
2ms对应1像素
'''
y0 = - self.win.size[1] / 2
x0 = - 300
for i in range(len(self.intervals)):
interval = self.intervals[i]
height = int(interval / 2)
if height not in self.bans:
height = self.height - 1
ban = self.bans[height]
ban.pos = (i * 5 + x0, height / 2 + y0)
ban.draw()
def showrefrect(refrect, time):
height = time / 2
y0 = - refrect.win.size[1] / 2
x0 = - 0
refrect.height = height
refrect.pos = (x0 , height / 2 + y0)
refrect.draw()
win = Window(fullscr = True)
def run():
bans = Bans(win)
bans_draw = True
refrect = Rect(win, width = 30 , height = 0, units = "pix")
refrect.setFillColor(color = (255, 170, 170), colorSpace = "rgb255")
refban = Rect(win, width = 30, height = 0, units = 'pix')
refban.setFillColor(color = (255, 0, 0), colorSpace = "rgb255")
text = TextStim(win, text = u'前10拍热身中', pos = (0, 200), units = "pix" )
background = Rect(win, width = win.size[0], height = win.size[1], units = "pix" )
circlerect = Rect(win, width = 25, height = 25, units = "pix")
circlerect.setFillColor(color = (0, 0, 0), colorSpace = "rgb255")
fpstext = TextStim(win, text = 'hello', pos = (0, 250), units = "pix" )
TextStim(win, text = u'紧跟节奏,敲击空格\nR键重置,Q键退出,', pos = (0, 250), units = "pix" ).draw()
win.flip()
lyric = None
count = 0 #当count = 10时重置计时器clk,重置ban_times
last_times = [0]
fps = FpsCounter(20)
event.waitKeys(keyList = ['space'])
clk = clock.Clock()
done = False
while not done:#因为屏幕刷新率太低,影响getkeys的时间,只能用这种按一次键刷一次屏幕的方式。
keys = []
while not keys:
keys = event.getKeys()
if 'q' in keys:
exit(0)
if 'r' in keys:
return
if 'space' in keys:
time = clk.getTime()
last_times.append(time)
bans.add((time - last_times[-2]) * 1000)
count += 1
if count == 10:
text.text = u"继续敲击,觉得差不多按下L键进入下一环节"
clk.reset()
#ban_times = [0] * 100
last_times = [0]
elif 'l' in keys:
lyric = Lyric(last_times)
done = True
bans.draw()
text.draw()
win.flip()
while True:
#由于调用draw需要时间,因此这里加上了平均刷新时间,用于预测真实刷新时的时间,从而匹配上画面
timeadjust = clk.getTime() + fps.avgfliptime()
for key, time in event.getKeys(timeStamped = clk):
if key == 'space':
last_times.append(time)
delta = time - last_times[-2]
if 0.4 < delta / lyric.k < 1.5:
bans.add(delta * 1000)
elif key == 'q':
exit(0)
elif key =='r':
return
elif 'd' == key:
bans_draw = not bans_draw
ratio = lyric.guessratio(timeadjust)
circlerect.pos = (0 + math.cos((ratio + .75) * 2 * math.pi) * 100, 0 + math.sin((ratio + .75) * 2 * math.pi) * 100)
color = (-20 *(1. - (1- ratio)**3) + 170,) * 3
refban.height = (2. * (abs(.5 - ratio)) * 300)
refban.pos = (0, refban.height / 2- win.size[1] / 2 )
background.setFillColor(color = color, colorSpace = "rgb255")
background.draw()
if bans_draw:
bans.draw()
showrefrect(refrect, 600)
text.text = u"avg = %dms, bpm = %d, b = %dms, 准确度%d%%\n出于性能考虑,按d键可以开启/关闭柱状图" % (lyric.k * 1000, 60 / lyric.k, lyric.b * 1000, 100 - abs(lyric.b / lyric.k * 100))
text.draw()
refban.draw()
#circlerect.draw()
circlerect.pos = (- 300 * (1 - ratio ), 0)
circlerect.draw()
circlerect.pos = ( 300 * (1 - ratio), 0)
circlerect.draw()
fpstext.text = "fps: %d, accuracy: %d" %(fps.count(clk.getTime()), 1000*(clk.getTime() - timeadjust))
fpstext.draw()
win.flip()
while True:
run()
win.flip()
win.close()