psychopy编写bpm计数器 音乐

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

这里有一个用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()

 

 

 

转载于:https://my.oschina.net/zbaigao/blog/1540012

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值