|
| 1 | +import numpy as np |
| 2 | +import math |
| 3 | + |
| 4 | +############################################################################### |
| 5 | +# Obtains the rhythm calculation for each note in the map by counting the |
| 6 | +# amount of notes around the note in a timing window of size bin_size |
| 7 | +############################################################################### |
| 8 | +bin_size = 1000 |
| 9 | + |
| 10 | +# TO-DO: Make the window non-rigid by weighting over a bell curve over (-2,2) instead |
| 11 | + |
| 12 | + |
| 13 | +def gaussian(x): |
| 14 | + return math.exp(-(x**2)/(2)) |
| 15 | + |
| 16 | + |
| 17 | +def bind_500(x): return max(min(x, 500), -500) |
| 18 | + |
| 19 | + |
| 20 | +def chord_suppression(x): |
| 21 | + grace_threshold = 30 |
| 22 | + slope = +0.2 |
| 23 | + x = bind_500(x) |
| 24 | + return 1 / (1 + math.exp(grace_threshold*slope-slope*x)) |
| 25 | + |
| 26 | + |
| 27 | +def weighted_avg_and_std(values, weights): |
| 28 | + """ |
| 29 | + Return the weighted average and standard deviation. |
| 30 | + values, weights -- Numpy ndarrays with the same shape. |
| 31 | + """ |
| 32 | + average = np.average(values, weights=weights) |
| 33 | + # Fast and numerically precise: |
| 34 | + variance = np.average((values-average)**2, weights=weights) |
| 35 | + return (average, math.sqrt(variance)) |
| 36 | + |
| 37 | + |
| 38 | +def obtainRhythmCalculation(ho): |
| 39 | + rhythm = np.ones(len(ho)) |
| 40 | + wl = 0 # Current index of the note that first enters in the window |
| 41 | + wr = 0 # Same but for last |
| 42 | + |
| 43 | + for i in range(len(ho)): |
| 44 | + r = 0 |
| 45 | + # Find the new first note that is inside the window |
| 46 | + while ho[wl].timestamp < (ho[i].timestamp-bin_size/2): |
| 47 | + wl += 1 |
| 48 | + |
| 49 | + # Fin the new last note that is inside the window |
| 50 | + while ho[wr].timestamp < (ho[i].timestamp+bin_size/2) and wr < len(ho)-1: |
| 51 | + wr += 1 |
| 52 | + |
| 53 | + if wr-wl > 0: |
| 54 | + |
| 55 | + # Raw distances between consecutive notes |
| 56 | + distances = np.zeros(wr-wl) |
| 57 | + # Gaussian weights assigned depending on distance to i, the center of the window (basically to smooth the calculations like in Density and Manip models) |
| 58 | + weights = np.zeros(wr-wl) |
| 59 | + |
| 60 | + # We always use the closest note to i as the note to calculate the weight with (gaussian(((ho[j+1]... vs. gaussian(((ho[j]...) |
| 61 | + for j in range(wl, i): |
| 62 | + distances[j-wl] = ho[j+1].timestamp-ho[j].timestamp |
| 63 | + weights[j-wl] = gaussian(((ho[j+1].timestamp-ho[i].timestamp)/( |
| 64 | + bin_size/2))*3)*chord_suppression(ho[j+1].timestamp-ho[j].timestamp) |
| 65 | + for j in range(i, wr): |
| 66 | + distances[j-wl] = ho[j+1].timestamp-ho[j].timestamp |
| 67 | + weights[j-wl] = gaussian(((ho[j].timestamp-ho[i].timestamp)/( |
| 68 | + bin_size/2))*3)*chord_suppression(ho[j+1].timestamp-ho[j].timestamp) |
| 69 | + |
| 70 | + # Compute the weighted avg and std deviation, rhythm difficulty for that note is stdev/avg. Perhaps this metric is not correct and favors higher BPM /lower BPM/faster maps? |
| 71 | + norm_avg, norm_stdev = weighted_avg_and_std(distances, weights) |
| 72 | + rhythm[i] += norm_stdev/(1+norm_avg) |
| 73 | + |
| 74 | + return rhythm |
0 commit comments