From c8bd8d41ca5ee78a92810bc5ce5a2ebcdcf0d9c0 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 20:50:22 -0500 Subject: [PATCH 01/20] Bare minimum modifications to work with Python 3 --- bpm_detection/bpm_detection.py | 122 ++++++++++++++------------------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/bpm_detection/bpm_detection.py b/bpm_detection/bpm_detection.py index 8419d06..49a52c5 100644 --- a/bpm_detection/bpm_detection.py +++ b/bpm_detection/bpm_detection.py @@ -1,50 +1,36 @@ +from __future__ import division, print_function import wave, array, math, time, argparse, sys import numpy, pywt +import wavio from scipy import signal import pdb import matplotlib.pyplot as plt + def read_wav(filename): + wf = wavio.read(filename) + samps = wf.data + fs = wf.rate + return samps.flatten(), fs + - #open file, get metadata for audio - try: - wf = wave.open(filename,'rb') - except IOError, e: - print e - return - - # typ = choose_type( wf.getsampwidth() ) #TODO: implement choose_type - nsamps = wf.getnframes(); - assert(nsamps > 0); - - fs = wf.getframerate() - assert(fs > 0) - - # read entire file and make into an array - samps = list(array.array('i',wf.readframes(nsamps))) - #print 'Read', nsamps,'samples from', filename - try: - assert(nsamps == len(samps)) - except AssertionError, e: - print nsamps, "not equal to", len(samps) - - return samps, fs - # print an error when no data can be found def no_audio_data(): - print "No audio data for sample, skipping..." + print("No audio data for sample, skipping...") return None, None - + + # simple peak detection def peak_detect(data): - max_val = numpy.amax(abs(data)) + max_val = numpy.amax(abs(data)) peak_ndx = numpy.where(data==max_val) if len(peak_ndx[0]) == 0: #if nothing found then the max must be negative peak_ndx = numpy.where(data==-max_val) return peak_ndx - -def bpm_detector(data,fs): - cA = [] + + +def bpm_detector(data, fs): + cA = [] cD = [] correl = [] cD_sum = [] @@ -52,53 +38,53 @@ def bpm_detector(data,fs): max_decimation = 2**(levels-1); min_ndx = 60./ 220 * (fs/max_decimation) max_ndx = 60./ 40 * (fs/max_decimation) - - for loop in range(0,levels): + + for loop in range(0, levels): cD = [] # 1) DWT if loop == 0: - [cA,cD] = pywt.dwt(data,'db4'); - cD_minlen = len(cD)/max_decimation+1; - cD_sum = numpy.zeros(cD_minlen); + [cA,cD] = pywt.dwt(data,'db4') + cD_minlen = len(cD) // max_decimation + 1 + cD_sum = numpy.zeros(cD_minlen) else: - [cA,cD] = pywt.dwt(cA,'db4'); + [cA,cD] = pywt.dwt(cA, 'db4') # 2) Filter - cD = signal.lfilter([0.01],[1 -0.99],cD); + cD = signal.lfilter([0.01],[1 -0.99], cD) # 4) Subtractargs.filename out the mean. # 5) Decimate for reconstruction later. - cD = abs(cD[::(2**(levels-loop-1))]); - cD = cD - numpy.mean(cD); + cD = abs(cD[::(2**(levels-loop-1))]) + cD = cD - numpy.mean(cD) # 6) Recombine the signal before ACF - # essentially, each level I concatenate + # essentially, each level I concatenate # the detail coefs (i.e. the HPF values) # to the beginning of the array cD_sum = cD[0:cD_minlen] + cD_sum; if [b for b in cA if b != 0.0] == []: return no_audio_data() - # adding in the approximate data as well... - cA = signal.lfilter([0.01],[1 -0.99],cA); + # adding in the approximate data as well... + cA = signal.lfilter([0.01], [1 -0.99], cA) cA = abs(cA); - cA = cA - numpy.mean(cA); - cD_sum = cA[0:cD_minlen] + cD_sum; - + cA = cA - numpy.mean(cA) + cD_sum = cA[0:cD_minlen] + cD_sum + # ACF - correl = numpy.correlate(cD_sum,cD_sum,'full') - - midpoint = len(correl) / 2 + correl = numpy.correlate(cD_sum,cD_sum,'full') + + midpoint = len(correl) // 2 correl_midpoint_tmp = correl[midpoint:] peak_ndx = peak_detect(correl_midpoint_tmp[min_ndx:max_ndx]); if len(peak_ndx) > 1: return no_audio_data() - + peak_ndx_adjusted = peak_ndx[0]+min_ndx; bpm = 60./ peak_ndx_adjusted * (fs/max_decimation) - print bpm - return bpm,correl - - + print(bpm) + return bpm, correl + + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Process .wav file to determine the Beats Per Minute.') parser.add_argument('--filename', required=True, @@ -107,42 +93,40 @@ def bpm_detector(data,fs): help='size of the the window (seconds) that will be scanned to determine the bpm. Typically less than 10 seconds. [3]') args = parser.parse_args() - samps,fs = read_wav(args.filename) - + samps, fs = read_wav(args.filename) + data = [] correl=[] bpm = 0 n=0; nsamps = len(samps) - window_samps = int(args.window*fs) - samps_ndx = 0; #first sample in window_ndx - max_window_ndx = nsamps / window_samps; + window_samps = int(args.window*fs) + samps_ndx = 0 #first sample in window_ndx + max_window_ndx = nsamps // window_samps bpms = numpy.zeros(max_window_ndx) #iterate through all windows - for window_ndx in xrange(0,max_window_ndx): + for window_ndx in range(0, max_window_ndx): #get a new set of samples #print n,":",len(bpms),":",max_window_ndx,":",fs,":",nsamps,":",samps_ndx data = samps[samps_ndx:samps_ndx+window_samps] if not ((len(data) % window_samps) == 0): - raise AssertionError( str(len(data) ) ) - + raise AssertionError( str(len(data) ) ) + bpm, correl_temp = bpm_detector(data,fs) if bpm == None: continue bpms[window_ndx] = bpm correl = correl_temp - + #iterate at the end of the loop samps_ndx = samps_ndx+window_samps; n=n+1; #counter for debug... bpm = numpy.median(bpms) - print 'Completed. Estimated Beats Per Minute:', bpm - - n = range(0,len(correl)) - plt.plot(n,abs(correl)); - plt.show(False); #plot non-blocking - time.sleep(10); - plt.close(); + print('Completed. Estimated Beats Per Minute:', bpm) + + n = range(0, len(correl)) + plt.plot(n, abs(correl)) + plt.show() From 24927bf6e566cc2b368978f2eafe81454af9a650 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 20:54:00 -0500 Subject: [PATCH 02/20] Remove print call --- bpm_detection/bpm_detection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpm_detection/bpm_detection.py b/bpm_detection/bpm_detection.py index 49a52c5..a694eb0 100644 --- a/bpm_detection/bpm_detection.py +++ b/bpm_detection/bpm_detection.py @@ -81,7 +81,6 @@ def bpm_detector(data, fs): peak_ndx_adjusted = peak_ndx[0]+min_ndx; bpm = 60./ peak_ndx_adjusted * (fs/max_decimation) - print(bpm) return bpm, correl From e1f31f10398428fb618fa1a65766fb9144f9917d Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 20:56:18 -0500 Subject: [PATCH 03/20] Move script out of subdir --- bpm_detection/bpm_detection.py => bpm_detect.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bpm_detection/bpm_detection.py => bpm_detect.py (100%) diff --git a/bpm_detection/bpm_detection.py b/bpm_detect.py similarity index 100% rename from bpm_detection/bpm_detection.py rename to bpm_detect.py From ac880119893f98dd6ba5296feb66242fc82a195c Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:02:00 -0500 Subject: [PATCH 04/20] Make filename a positional arg --- bpm_detect.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index a694eb0..88b1c0d 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -86,8 +86,7 @@ def bpm_detector(data, fs): if __name__ == '__main__': parser = argparse.ArgumentParser(description='Process .wav file to determine the Beats Per Minute.') - parser.add_argument('--filename', required=True, - help='.wav file for processing') + parser.add_argument('filename', help='.wav file for processing') parser.add_argument('--window', type=float, default=3, help='size of the the window (seconds) that will be scanned to determine the bpm. Typically less than 10 seconds. [3]') From 26a6aceb4c733f716372920f9cb92df83fda7582 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:03:00 -0500 Subject: [PATCH 05/20] Wrap lines --- bpm_detect.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index 88b1c0d..0b86845 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -85,10 +85,13 @@ def bpm_detector(data, fs): if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Process .wav file to determine the Beats Per Minute.') + parser = argparse.ArgumentParser(description='Process .wav file to ' + 'determine the Beats Per Minute.') parser.add_argument('filename', help='.wav file for processing') parser.add_argument('--window', type=float, default=3, - help='size of the the window (seconds) that will be scanned to determine the bpm. Typically less than 10 seconds. [3]') + help='size of the the window (seconds) that will be ' + 'scanned to determine the bpm. Typically less than 10 ' + 'seconds. [3]') args = parser.parse_args() samps, fs = read_wav(args.filename) From d6518194824e1b02765b1d461e423cf160bae888 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:05:31 -0500 Subject: [PATCH 06/20] Whitespace fixes --- bpm_detect.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index 0b86845..1bf38ad 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -25,7 +25,7 @@ def peak_detect(data): max_val = numpy.amax(abs(data)) peak_ndx = numpy.where(data==max_val) if len(peak_ndx[0]) == 0: #if nothing found then the max must be negative - peak_ndx = numpy.where(data==-max_val) + peak_ndx = numpy.where(data == -max_val) return peak_ndx @@ -35,7 +35,7 @@ def bpm_detector(data, fs): correl = [] cD_sum = [] levels = 4 - max_decimation = 2**(levels-1); + max_decimation = 2**(levels - 1) min_ndx = 60./ 220 * (fs/max_decimation) max_ndx = 60./ 40 * (fs/max_decimation) @@ -49,29 +49,29 @@ def bpm_detector(data, fs): else: [cA,cD] = pywt.dwt(cA, 'db4') # 2) Filter - cD = signal.lfilter([0.01],[1 -0.99], cD) + cD = signal.lfilter([0.01], [1 -0.99], cD) # 4) Subtractargs.filename out the mean. # 5) Decimate for reconstruction later. - cD = abs(cD[::(2**(levels-loop-1))]) + cD = abs(cD[::(2**(levels - loop - 1))]) cD = cD - numpy.mean(cD) # 6) Recombine the signal before ACF # essentially, each level I concatenate # the detail coefs (i.e. the HPF values) # to the beginning of the array - cD_sum = cD[0:cD_minlen] + cD_sum; + cD_sum = cD[0:cD_minlen] + cD_sum if [b for b in cA if b != 0.0] == []: return no_audio_data() # adding in the approximate data as well... cA = signal.lfilter([0.01], [1 -0.99], cA) - cA = abs(cA); + cA = abs(cA) cA = cA - numpy.mean(cA) cD_sum = cA[0:cD_minlen] + cD_sum # ACF - correl = numpy.correlate(cD_sum,cD_sum,'full') + correl = numpy.correlate(cD_sum, cD_sum, 'full') midpoint = len(correl) // 2 correl_midpoint_tmp = correl[midpoint:] @@ -111,19 +111,19 @@ def bpm_detector(data, fs): #get a new set of samples #print n,":",len(bpms),":",max_window_ndx,":",fs,":",nsamps,":",samps_ndx - data = samps[samps_ndx:samps_ndx+window_samps] + data = samps[samps_ndx:samps_ndx + window_samps] if not ((len(data) % window_samps) == 0): - raise AssertionError( str(len(data) ) ) + raise AssertionError(str(len(data))) - bpm, correl_temp = bpm_detector(data,fs) + bpm, correl_temp = bpm_detector(data, fs) if bpm == None: continue bpms[window_ndx] = bpm correl = correl_temp #iterate at the end of the loop - samps_ndx = samps_ndx+window_samps; - n=n+1; #counter for debug... + samps_ndx = samps_ndx + window_samps + n = n + 1 #counter for debug... bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) From b8599872e3bad12385043887da0b025e237ee455 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:06:09 -0500 Subject: [PATCH 07/20] Remove equality operator with None --- bpm_detect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index 1bf38ad..dd6a7b1 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -116,7 +116,7 @@ def bpm_detector(data, fs): raise AssertionError(str(len(data))) bpm, correl_temp = bpm_detector(data, fs) - if bpm == None: + if bpm is None: continue bpms[window_ndx] = bpm correl = correl_temp From 984d0e59b5050b11fcd36e03e6f91a91bfa1f939 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:07:30 -0500 Subject: [PATCH 08/20] Convert to ints --- bpm_detect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index dd6a7b1..4367edb 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -75,7 +75,7 @@ def bpm_detector(data, fs): midpoint = len(correl) // 2 correl_midpoint_tmp = correl[midpoint:] - peak_ndx = peak_detect(correl_midpoint_tmp[min_ndx:max_ndx]); + peak_ndx = peak_detect(correl_midpoint_tmp[int(min_ndx):int(max_ndx)]) if len(peak_ndx) > 1: return no_audio_data() From a16d80e4e7294ddd41c9b4d127eb1f029502ad4a Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:11:52 -0500 Subject: [PATCH 09/20] More whitespace fixes --- bpm_detect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index 4367edb..3320b95 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -79,7 +79,7 @@ def bpm_detector(data, fs): if len(peak_ndx) > 1: return no_audio_data() - peak_ndx_adjusted = peak_ndx[0]+min_ndx; + peak_ndx_adjusted = peak_ndx[0] + min_ndx bpm = 60./ peak_ndx_adjusted * (fs/max_decimation) return bpm, correl From 990d6e9f1a70c457907a89a1a0651098fa477962 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:12:11 -0500 Subject: [PATCH 10/20] More whitespace fixes --- bpm_detect.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index 3320b95..a4e2ac4 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -128,6 +128,5 @@ def bpm_detector(data, fs): bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) - n = range(0, len(correl)) - plt.plot(n, abs(correl)) + plt.plot(bpm) plt.show() From 85a79a3c533db8ae0d6d6cadaf2f4d0b631c93fd Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:13:02 -0500 Subject: [PATCH 11/20] Plot bpms --- bpm_detect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index a4e2ac4..2736619 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -128,5 +128,5 @@ def bpm_detector(data, fs): bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) - plt.plot(bpm) + plt.plot(bpms) plt.show() From 2f81ecf4afb6f61947868a817fbc1f1638c9d548 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:17:02 -0500 Subject: [PATCH 12/20] More whitespace fixes --- bpm_detect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index 2736619..6bc88fe 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -97,9 +97,9 @@ def bpm_detector(data, fs): samps, fs = read_wav(args.filename) data = [] - correl=[] + correl = [] bpm = 0 - n=0; + n = 0 nsamps = len(samps) window_samps = int(args.window*fs) samps_ndx = 0 #first sample in window_ndx From ac099eb488bc6fcd7bd479e2b1b6b3a76569ef6e Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:35:59 -0500 Subject: [PATCH 13/20] Calculate window midpoint in seconds --- bpm_detect.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index 6bc88fe..12ae1a4 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -101,10 +101,12 @@ def bpm_detector(data, fs): bpm = 0 n = 0 nsamps = len(samps) + seconds = numpy.arange(len(samps)) / fs window_samps = int(args.window*fs) samps_ndx = 0 #first sample in window_ndx max_window_ndx = nsamps // window_samps bpms = numpy.zeros(max_window_ndx) + seconds_mid = numpy.zeros(max_window_ndx) #iterate through all windows for window_ndx in range(0, max_window_ndx): @@ -112,6 +114,8 @@ def bpm_detector(data, fs): #get a new set of samples #print n,":",len(bpms),":",max_window_ndx,":",fs,":",nsamps,":",samps_ndx data = samps[samps_ndx:samps_ndx + window_samps] + seconds_mid[window_ndx] = \ + seconds[samps_ndx:samps_ndx + window_samps].mean() if not ((len(data) % window_samps) == 0): raise AssertionError(str(len(data))) @@ -128,5 +132,7 @@ def bpm_detector(data, fs): bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) - plt.plot(bpms) + plt.plot(seconds_mid, bpms) + plt.xlabel("Time (s)") + plt.ylabel("BPM") plt.show() From c910d584c89adac85cfe66bbbac8c4bbd6019350 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:38:00 -0500 Subject: [PATCH 14/20] Use Seaborn for plot styling --- bpm_detect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bpm_detect.py b/bpm_detect.py index 12ae1a4..47323c9 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -5,6 +5,7 @@ from scipy import signal import pdb import matplotlib.pyplot as plt +import seaborn def read_wav(filename): From d965383985b7b621accbe0a081d96c16935e21ef Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Mon, 5 Dec 2016 21:48:55 -0500 Subject: [PATCH 15/20] Add filtering to tempo --- bpm_detect.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bpm_detect.py b/bpm_detect.py index 47323c9..88abe26 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -6,6 +6,7 @@ import pdb import matplotlib.pyplot as plt import seaborn +from statsmodels.nonparametric.smoothers_lowess import lowess def read_wav(filename): @@ -133,7 +134,12 @@ def bpm_detector(data, fs): bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) + # Smoothing from http://stackoverflow.com/questions/28536191\ + # /how-to-filter-smooth-with-scipy-numpy + filtered = lowess(bpms, seconds_mid, is_sorted=True, frac=0.3, it=0) + plt.plot(seconds_mid, bpms) + plt.plot(filtered[:, 0], filtered[:, 1]) plt.xlabel("Time (s)") plt.ylabel("BPM") plt.show() From e71246fb9a86ff79c929328ab80134d820f97260 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Sun, 11 Dec 2016 10:42:46 -0500 Subject: [PATCH 16/20] Add smoothing and calculate beats as x-values --- bpm_detect.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index 88abe26..29d5441 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -7,6 +7,14 @@ import matplotlib.pyplot as plt import seaborn from statsmodels.nonparametric.smoothers_lowess import lowess +from scipy.integrate import cumtrapz + + +def smooth(data): + """Smooth data with 3rd order Butterworth filter.""" + b, a = signal.butter(3, 7 / len(data)) + data = signal.filtfilt(b, a, data) + return data def read_wav(filename): @@ -136,10 +144,15 @@ def bpm_detector(data, fs): # Smoothing from http://stackoverflow.com/questions/28536191\ # /how-to-filter-smooth-with-scipy-numpy - filtered = lowess(bpms, seconds_mid, is_sorted=True, frac=0.3, it=0) + filtered = lowess(bpms, seconds_mid, is_sorted=True, frac=0.3, it=0)[:, 1] + # filtered = smooth(bpms) + + # Create beats array + bps = filtered / 60 + beats = cumtrapz(bps, seconds_mid, initial=0) - plt.plot(seconds_mid, bpms) - plt.plot(filtered[:, 0], filtered[:, 1]) - plt.xlabel("Time (s)") + plt.plot(beats, bpms) + plt.plot(beats, filtered) + plt.xlabel("Beat") plt.ylabel("BPM") plt.show() From 908bcef04d98d43c61befa181465d784d63426fa Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Sun, 11 Dec 2016 10:51:56 -0500 Subject: [PATCH 17/20] Add argument for plotting --- bpm_detect.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index 29d5441..d8ce5be 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -102,6 +102,8 @@ def bpm_detector(data, fs): help='size of the the window (seconds) that will be ' 'scanned to determine the bpm. Typically less than 10 ' 'seconds. [3]') + parser.add_argument('--plot', '-p', action='store_true', default=False, + help='Plot tempo with matplotlib') args = parser.parse_args() samps, fs = read_wav(args.filename) @@ -142,17 +144,19 @@ def bpm_detector(data, fs): bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) - # Smoothing from http://stackoverflow.com/questions/28536191\ - # /how-to-filter-smooth-with-scipy-numpy - filtered = lowess(bpms, seconds_mid, is_sorted=True, frac=0.3, it=0)[:, 1] - # filtered = smooth(bpms) - - # Create beats array - bps = filtered / 60 - beats = cumtrapz(bps, seconds_mid, initial=0) - - plt.plot(beats, bpms) - plt.plot(beats, filtered) - plt.xlabel("Beat") - plt.ylabel("BPM") - plt.show() + if args.plot: + # Smoothing from http://stackoverflow.com/questions/28536191\ + # /how-to-filter-smooth-with-scipy-numpy + filtered = lowess( + bpms, seconds_mid, is_sorted=True, frac=0.3, it=0 + )[:, 1] + + # Create beats array + bps = filtered / 60 + beats = cumtrapz(bps, seconds_mid, initial=0) + + plt.plot(beats, bpms) + plt.plot(beats, filtered) + plt.xlabel("Beat") + plt.ylabel("BPM") + plt.show() From bd51381fa57fa35f849bfc0a7684189294f68986 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Sun, 11 Dec 2016 11:33:44 -0500 Subject: [PATCH 18/20] Add ability to write MIDI file for click track --- bpm_detect.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/bpm_detect.py b/bpm_detect.py index d8ce5be..7a3de75 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -104,6 +104,7 @@ def bpm_detector(data, fs): 'seconds. [3]') parser.add_argument('--plot', '-p', action='store_true', default=False, help='Plot tempo with matplotlib') + parser.add_argument('--write-midi', '-w', help='Write MIDI to file') args = parser.parse_args() samps, fs = read_wav(args.filename) @@ -141,20 +142,37 @@ def bpm_detector(data, fs): samps_ndx = samps_ndx + window_samps n = n + 1 #counter for debug... + # Smoothing from http://stackoverflow.com/questions/28536191\ + # /how-to-filter-smooth-with-scipy-numpy + filtered = lowess(bpms, seconds_mid, is_sorted=True, frac=0.3, it=0)[:, 1] + # Create beats array + bps = filtered / 60 + beats = cumtrapz(bps, seconds_mid, initial=0) + bpm = numpy.median(bpms) print('Completed. Estimated Beats Per Minute:', bpm) - if args.plot: - # Smoothing from http://stackoverflow.com/questions/28536191\ - # /how-to-filter-smooth-with-scipy-numpy - filtered = lowess( - bpms, seconds_mid, is_sorted=True, frac=0.3, it=0 - )[:, 1] - - # Create beats array - bps = filtered / 60 - beats = cumtrapz(bps, seconds_mid, initial=0) + if args.write_midi is not None: + from midiutil.MidiFile import MIDIFile + pitch = 70 # MIDI note number for A# + track = 0 + channel = 0 + duration = 0.125 # In beats + volume = 127 # 0-127, as per the MIDI standard + mf = MIDIFile(1) # One track, defaults to format 1 (tempo track + # automatically created) + # Set time signature to 1/4 + # mf.addTimeSignature(track, 0, 1, 2, 24, notes_per_quarter=8) + # Create new beats array that's evenly spaced and map to this + beats_even = numpy.arange(0, beats.max()*1.1, 0.5) + bpm_mapped = numpy.interp(beats_even, beats, filtered) + for tempo, beat in zip(bpm_mapped, beats_even): + mf.addTempo(track, beat, tempo) + mf.addNote(track, channel, pitch, beat, duration, volume) + with open(args.write_midi, "wb") as output_file: + mf.writeFile(output_file) + if args.plot: plt.plot(beats, bpms) plt.plot(beats, filtered) plt.xlabel("Beat") From 305a6a6d281f0ceb3e1bc53a11b5221643937426 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Tue, 13 Dec 2016 07:51:07 -0500 Subject: [PATCH 19/20] Add MIDI note option --- bpm_detect.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index 7a3de75..c95be88 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -105,6 +105,8 @@ def bpm_detector(data, fs): parser.add_argument('--plot', '-p', action='store_true', default=False, help='Plot tempo with matplotlib') parser.add_argument('--write-midi', '-w', help='Write MIDI to file') + parser.add_argument('--midi-note', '-n', help='MIDI note number for click ' + 'track output', type=int, default=67) args = parser.parse_args() samps, fs = read_wav(args.filename) @@ -154,7 +156,7 @@ def bpm_detector(data, fs): if args.write_midi is not None: from midiutil.MidiFile import MIDIFile - pitch = 70 # MIDI note number for A# + pitch = ags.midi_note track = 0 channel = 0 duration = 0.125 # In beats From 8610935dae46d493a4b44f5cd7c35002e8f28287 Mon Sep 17 00:00:00 2001 From: Pete Peterson Date: Tue, 13 Dec 2016 07:55:41 -0500 Subject: [PATCH 20/20] Fix typo --- bpm_detect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpm_detect.py b/bpm_detect.py index c95be88..5de6208 100644 --- a/bpm_detect.py +++ b/bpm_detect.py @@ -156,7 +156,7 @@ def bpm_detector(data, fs): if args.write_midi is not None: from midiutil.MidiFile import MIDIFile - pitch = ags.midi_note + pitch = args.midi_note track = 0 channel = 0 duration = 0.125 # In beats