├── README.md ├── ui.py ├── _ui2py.pyw ├── ui_plot.ui ├── ui_plot.py ├── recorder.py ├── .gitignore └── beatDetector.py /README.md: -------------------------------------------------------------------------------- 1 | # python-beat-detector 2 | Real-time detection of beats for audio, calculates BPM and flashes LED strip in time with music. 3 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | import ui_plot 2 | 3 | import sys 4 | 5 | from PyQt4 import QtCore, QtGui 6 | 7 | if __name__ == "__main__": 8 | app = QtGui.QApplication(sys.argv) 9 | 10 | ### SET-UP WINDOWS 11 | 12 | # WINDOW plot 13 | win_plot = ui_plot.QtGui.QMainWindow() 14 | uiplot = ui_plot.Ui_win_plot() 15 | uiplot.setupUi(win_plot) 16 | 17 | ### DISPLAY WINDOWS 18 | win_plot.show() 19 | 20 | #WAIT UNTIL QT RETURNS EXIT CODE 21 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /_ui2py.pyw: -------------------------------------------------------------------------------- 1 | # IMPORTANT: USE FILE NAMING "ui_about.ui" 2 | # USE HIGHESET LEVEL OBJECT NAMING "ui_about" (IDENTICAL) 3 | 4 | import glob 5 | import os 6 | 7 | out=""" 8 | import sys 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | if __name__ == "__main__": 13 | app = QtGui.QApplication(sys.argv) 14 | 15 | ### SET-UP WINDOWS 16 | """ 17 | showit="" 18 | for pyc in glob.glob("*.pyc"): 19 | os.system("del %s"%pyc) 20 | for ui in glob.glob("*.ui"): 21 | os.system ("pyuic4 -x %s -o %s"%(ui,ui.replace(".ui",".py"))) 22 | out="import %s\n"%(ui.split(".")[0])+out 23 | uiname=ui.split("_")[1].split(".")[0] 24 | out+=""" 25 | # WINDOW ~ 26 | win_~ = ui_~.QtGui.QMainWindow() 27 | ui~ = ui_~.Ui_win_~() 28 | ui~.setupUi(win_~)\n""".replace("~",uiname) 29 | showit+=" win_~.show()\n".replace("~",uiname) 30 | 31 | out+="\n ### DISPLAY WINDOWS\n"+showit 32 | out+=""" 33 | #WAIT UNTIL QT RETURNS EXIT CODE 34 | sys.exit(app.exec_())""" 35 | 36 | f=open("ui.py",'w') 37 | f.write(out) 38 | f.close() 39 | print "DONE" -------------------------------------------------------------------------------- /ui_plot.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | win_plot 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 6 25 | 26 | 27 | 0 28 | 29 | 30 | 6 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | A 39 | 40 | 41 | 42 | 43 | 44 | 45 | B 46 | 47 | 48 | 49 | 50 | 51 | 52 | C 53 | 54 | 55 | 56 | 57 | 58 | 59 | D 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | QwtPlot 71 | QFrame 72 |
qwt_plot.h
73 |
74 |
75 | 76 | 77 |
78 | -------------------------------------------------------------------------------- /ui_plot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'ui_plot.ui' 4 | # 5 | # Created: Wed May 08 10:02:53 2013 6 | # by: PyQt4 UI code generator 4.9.5 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | try: 13 | _fromUtf8 = QtCore.QString.fromUtf8 14 | except AttributeError: 15 | _fromUtf8 = lambda s: s 16 | 17 | class Ui_win_plot(object): 18 | def setupUi(self, win_plot): 19 | win_plot.setObjectName(_fromUtf8("win_plot")) 20 | win_plot.resize(800, 600) 21 | self.centralwidget = QtGui.QWidget(win_plot) 22 | self.centralwidget.setObjectName(_fromUtf8("centralwidget")) 23 | self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget) 24 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 25 | self.qwtPlot = Qwt5.QwtPlot(self.centralwidget) 26 | self.qwtPlot.setObjectName(_fromUtf8("qwtPlot")) 27 | self.verticalLayout.addWidget(self.qwtPlot) 28 | self.horizontalLayout = QtGui.QHBoxLayout() 29 | self.horizontalLayout.setContentsMargins(6, 0, 6, 0) 30 | self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) 31 | self.btnA = QtGui.QPushButton(self.centralwidget) 32 | self.btnA.setObjectName(_fromUtf8("btnA")) 33 | self.horizontalLayout.addWidget(self.btnA) 34 | self.btnB = QtGui.QPushButton(self.centralwidget) 35 | self.btnB.setObjectName(_fromUtf8("btnB")) 36 | self.horizontalLayout.addWidget(self.btnB) 37 | self.btnC = QtGui.QPushButton(self.centralwidget) 38 | self.btnC.setObjectName(_fromUtf8("btnC")) 39 | self.horizontalLayout.addWidget(self.btnC) 40 | self.btnD = QtGui.QPushButton(self.centralwidget) 41 | self.btnD.setObjectName(_fromUtf8("btnD")) 42 | self.btnD.setStyleSheet("background-color: red") 43 | self.horizontalLayout.addWidget(self.btnD) 44 | self.verticalLayout.addLayout(self.horizontalLayout) 45 | win_plot.setCentralWidget(self.centralwidget) 46 | 47 | self.retranslateUi(win_plot) 48 | QtCore.QMetaObject.connectSlotsByName(win_plot) 49 | 50 | def retranslateUi(self, win_plot): 51 | win_plot.setWindowTitle(QtGui.QApplication.translate("win_plot", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) 52 | self.btnA.setText(QtGui.QApplication.translate("win_plot", "A", None, QtGui.QApplication.UnicodeUTF8)) 53 | self.btnB.setText(QtGui.QApplication.translate("win_plot", "B", None, QtGui.QApplication.UnicodeUTF8)) 54 | self.btnC.setText(QtGui.QApplication.translate("win_plot", "C", None, QtGui.QApplication.UnicodeUTF8)) 55 | self.btnD.setText(QtGui.QApplication.translate("win_plot", "BPM", None, QtGui.QApplication.UnicodeUTF8)) 56 | 57 | from PyQt4 import Qwt5 58 | 59 | if __name__ == "__main__": 60 | import sys 61 | app = QtGui.QApplication(sys.argv) 62 | win_plot = QtGui.QMainWindow() 63 | ui = Ui_win_plot() 64 | ui.setupUi(win_plot) 65 | win_plot.show() 66 | sys.exit(app.exec_()) 67 | 68 | -------------------------------------------------------------------------------- /recorder.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use('TkAgg') # <-- THIS MAKES IT FAST! 3 | import numpy 4 | import scipy 5 | import pyaudio 6 | import threading 7 | import pylab 8 | 9 | class InputRecorder: 10 | """Simple, cross-platform class to record from the default input device.""" 11 | 12 | def __init__(self): 13 | self.RATE = 44100 14 | self.BUFFERSIZE = 2**12 15 | self.secToRecord = .1 16 | self.kill_threads = False 17 | self.has_new_audio = False 18 | self.setup() 19 | 20 | def setup(self): 21 | self.buffers_to_record = int(self.RATE * self.secToRecord / self.BUFFERSIZE) 22 | if self.buffers_to_record == 0: 23 | self.buffers_to_record = 1 24 | self.samples_to_record = int(self.BUFFERSIZE * self.buffers_to_record) 25 | self.chunks_to_record = int(self.samples_to_record / self.BUFFERSIZE) 26 | self.sec_per_point = 1. / self.RATE 27 | 28 | self.p = pyaudio.PyAudio() 29 | # make sure the default input device is broadcasting the speaker output 30 | # there are a few ways to do this 31 | # e.g., stereo mix, VB audio cable for windows, soundflower for mac 32 | print("Using default input device: {:s}".format(self.p.get_default_input_device_info()['name'])) 33 | self.in_stream = self.p.open(format=pyaudio.paInt16, 34 | channels=1, 35 | rate=self.RATE, 36 | input=True, 37 | frames_per_buffer=self.BUFFERSIZE) 38 | 39 | self.audio = numpy.empty((self.chunks_to_record * self.BUFFERSIZE), dtype=numpy.int16) 40 | 41 | def close(self): 42 | self.kill_threads = True 43 | self.p.close(self.in_stream) 44 | 45 | ### RECORDING AUDIO ### 46 | 47 | def get_audio(self): 48 | """get a single buffer size worth of audio.""" 49 | audio_string = self.in_stream.read(self.BUFFERSIZE) 50 | return numpy.fromstring(audio_string, dtype=numpy.int16) 51 | 52 | def record(self): 53 | while not self.kill_threads: 54 | for i in range(self.chunks_to_record): 55 | self.audio[i*self.BUFFERSIZE:(i+1)*self.BUFFERSIZE] = self.get_audio() 56 | self.has_new_audio = True 57 | 58 | def start(self): 59 | self.t = threading.Thread(target=self.record) 60 | self.t.start() 61 | 62 | ### MATH ### 63 | 64 | def downsample(self, data, mult): 65 | """Given 1D data, return the binned average.""" 66 | overhang = len(data) % mult 67 | if overhang: 68 | data = data[:-overhang] 69 | data = numpy.reshape(data, (len(data) / mult, mult)) 70 | data = numpy.average(data, 1) 71 | return data 72 | 73 | def fft(self, data=None, trim_by=10, log_scale=False, div_by=100): 74 | if not data: 75 | data = self.audio.flatten() 76 | left, right = numpy.split(numpy.abs(numpy.fft.fft(data)), 2) 77 | ys = numpy.add(left, right[::-1]) 78 | if log_scale: 79 | ys = numpy.multiply(20, numpy.log10(ys)) 80 | xs = numpy.arange(self.BUFFERSIZE/2, dtype=float) 81 | if trim_by: 82 | i = int((self.BUFFERSIZE/2) / trim_by) 83 | ys = ys[:i] 84 | xs = xs[:i] * self.RATE / self.BUFFERSIZE 85 | if div_by: 86 | ys = ys / float(div_by) 87 | return xs, ys 88 | 89 | ### VISUALIZATION ### 90 | 91 | def plot_sound_wave(self): 92 | """open a matplotlib popup window showing audio data.""" 93 | pylab.plot(self.audio.flatten()) 94 | pylab.show() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /beatDetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sources 3 | 4 | http://www.swharden.com/blog/2013-05-09-realtime-fft-audio-visualization-with-python/ 5 | http://julip.co/2012/05/arduino-python-soundlight-spectrum/ 6 | """ 7 | 8 | import ui_plot 9 | import sys 10 | import numpy 11 | from PyQt4 import QtCore, QtGui 12 | import PyQt4.Qwt5 as Qwt 13 | from recorder import * 14 | from time import perf_counter 15 | 16 | try: 17 | _fromUtf8 = QtCore.QString.fromUtf8 18 | except AttributeError: 19 | _fromUtf8 = lambda s: s 20 | 21 | colors_list = ["red", "blue", "green"] 22 | colors_idx = 0; 23 | 24 | bpm_list = [] 25 | prev_beat = perf_counter() 26 | low_freq_avg_list = [] 27 | 28 | def plot_audio_and_detect_beats(): 29 | if not input_recorder.has_new_audio: 30 | return 31 | 32 | # get x and y values from FFT 33 | xs, ys = input_recorder.fft() 34 | 35 | # calculate average for all frequency ranges 36 | y_avg = numpy.mean(ys) 37 | 38 | # calculate low frequency average 39 | low_freq = [ys[i] for i in range(len(xs)) if xs[i] < 1000] 40 | low_freq_avg = numpy.mean(low_freq) 41 | 42 | global low_freq_avg_list 43 | low_freq_avg_list.append(low_freq_avg) 44 | cumulative_avg = numpy.mean(low_freq_avg_list) 45 | 46 | bass = low_freq[:int(len(low_freq)/2)] 47 | bass_avg = numpy.mean(bass) 48 | # print("bass: {:.2f} vs cumulative: {:.2f}".format(bass_avg, cumulative_avg)) 49 | 50 | # check if there is a beat 51 | # song is pretty uniform across all frequencies 52 | if (y_avg > 10 and (bass_avg > cumulative_avg * 1.5 or 53 | (low_freq_avg < y_avg * 1.2 and bass_avg > cumulative_avg))): 54 | global prev_beat 55 | curr_time = perf_counter() 56 | # print(curr_time - prev_beat) 57 | if curr_time - prev_beat > 60/180: # 180 BPM max 58 | # print("beat") 59 | # change the button color 60 | global colors_idx 61 | colors_idx += 1; 62 | uiplot.btnD.setStyleSheet("background-color: {:s}".format(colors_list[colors_idx % len(colors_list)])) 63 | 64 | # change the button text 65 | global bpm_list 66 | bpm = int(60 / (curr_time - prev_beat)) 67 | if len(bpm_list) < 4: 68 | if bpm > 60: 69 | bpm_list.append(bpm) 70 | else: 71 | bpm_avg = int(numpy.mean(bpm_list)) 72 | if abs(bpm_avg - bpm) < 35: 73 | bpm_list.append(bpm) 74 | uiplot.btnD.setText(_fromUtf8("BPM: {:d}".format(bpm_avg))) 75 | 76 | # reset the timer 77 | prev_beat = curr_time 78 | 79 | # shorten the cumulative list to account for changes in dynamics 80 | if len(low_freq_avg_list) > 50: 81 | low_freq_avg_list = low_freq_avg_list[25:] 82 | # print("REFRESH!!") 83 | 84 | # keep two 8-counts of BPMs so we can maybe catch tempo changes 85 | if len(bpm_list) > 24: 86 | bpm_list = bpm_list[8:] 87 | 88 | # reset song data if the song has stopped 89 | if y_avg < 10: 90 | bpm_list = [] 91 | low_freq_avg_list = [] 92 | uiplot.btnD.setText(_fromUtf8("BPM")) 93 | # print("new song") 94 | 95 | # plot the data 96 | c.setData(xs, ys) 97 | uiplot.qwtPlot.replot() 98 | input_recorder.newAudio = False 99 | 100 | if __name__ == "__main__": 101 | app = QtGui.QApplication(sys.argv) 102 | 103 | win_plot = ui_plot.QtGui.QMainWindow() 104 | uiplot = ui_plot.Ui_win_plot() 105 | uiplot.setupUi(win_plot) 106 | # uiplot.btnA.clicked.connect(plot_audio_and_detect_beats) 107 | # uiplot.btnB.clicked.connect(lambda: uiplot.timer.setInterval(100.0)) 108 | # uiplot.btnC.clicked.connect(lambda: uiplot.timer.setInterval(10.0)) 109 | # uiplot.btnD.clicked.connect(lambda: uiplot.timer.setInterval(1.0)) 110 | c = Qwt.QwtPlotCurve() 111 | c.attach(uiplot.qwtPlot) 112 | 113 | uiplot.qwtPlot.setAxisScale(uiplot.qwtPlot.yLeft, 0, 100000) 114 | 115 | uiplot.timer = QtCore.QTimer() 116 | uiplot.timer.start(1.0) 117 | 118 | win_plot.connect(uiplot.timer, QtCore.SIGNAL('timeout()'), plot_audio_and_detect_beats) 119 | 120 | input_recorder = InputRecorder() 121 | input_recorder.start() 122 | 123 | ### DISPLAY WINDOWS 124 | win_plot.show() 125 | code = app.exec_() 126 | 127 | # clean up 128 | input_recorder.close() 129 | sys.exit(code) --------------------------------------------------------------------------------