bpm js 计算 音乐_psychopy编写bpm计数器 音乐

介绍了一个使用Psychopy编写的简单BPM(每分钟节拍数)计数器,通过跟随音乐节奏敲击键盘来测量歌曲的节奏。该程序能够显示平均敲击间隔和BPM,并提供视觉反馈以验证准确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

是不是很好奇听的歌节奏是多少?

这里有一个用psychopy编写的bpm计数器

使用方法

运行程序后,随着音乐节奏敲击【空格键】,觉得差不多了,按下【L键】,便会显示平均敲击间隔(ms)和平均bpm。之后程序会自动打节奏给你看,用于检查计算是否准确,可按【D键】关闭柱状图以提高绘制效率。

柱状图表示每次按键间隔,每个像素表示2ms

【r】键重置记录

【q】键退出

运行示例

已验证 凤凰传奇 的 最炫民族风 bpm 是127

已验证 f.i.r 的 刺鸟 bpm 是 81

5594dddac20d89eb5cd9bd0407a4f696.png

代码(复制可用)

# -*- 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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值