class BeatTrackingProcessor(Processor): """ Class for tracking beats with a simple tempo estimation and beat aligning. """ LOOK_ASIDE = 0.2 LOOK_AHEAD = 10 # tempo defaults TEMPO_METHOD = 'comb' MIN_BPM = 40 MAX_BPM = 240 ACT_SMOOTH = 0.09 HIST_SMOOTH = 7 ALPHA = 0.79 def __init__(self, look_aside=LOOK_ASIDE, look_ahead=LOOK_AHEAD, fps=None, **kwargs): """ Track the beats according to the previously determined (local) tempo by simply aligning them around the estimated position. :param look_aside: look this fraction of a beat interval to each side of the assumed next beat position to look for the most likely position of the next beat :param look_ahead: look N seconds in both directions to determine the local tempo and align the beats accordingly If `look_ahead` is not set, a constant tempo throughout the whole piece is assumed. If `look_ahead` is set, the local tempo (in a range +/- look_ahead seconds around the actual position) is estimated and then the next beat is tracked accordingly. This procedure is repeated from the new position to the end of the piece. "Enhanced Beat Tracking with Context-Aware Neural Networks" Sebastian Böck and Markus Schedl Proceedings of the 14th International Conference on Digital Audio Effects (DAFx), 2011 Instead of the auto-correlation based method for tempo estimation, it uses a comb filter per default. The behaviour can be controlled with the `tempo_method` parameter. """ # import the TempoEstimation here otherwise we have a loop from madmom.features.tempo import TempoEstimationProcessor # save variables self.look_aside = look_aside self.look_ahead = look_ahead self.fps = fps # tempo estimator self.tempo_estimator = TempoEstimationProcessor(fps=fps, **kwargs) def process(self, activations): """ Detect the beats in the given activation function. :param activations: beat activation function :return: detected beat positions [seconds] """ # smooth activations act_smooth = int(self.fps * self.tempo_estimator.act_smooth) activations = smooth_signal(activations, act_smooth) # TODO: refactor interval stuff to use TempoEstimation # if look_ahead is not defined, assume a global tempo if self.look_ahead is None: # create a interval histogram histogram = self.tempo_estimator.interval_histogram(activations) # get the dominant interval interval = self.tempo_estimator.dominant_interval(histogram) # detect beats based on this interval detections = detect_beats(activations, interval, self.look_aside) else: # allow varying tempo look_ahead_frames = int(self.look_ahead * self.fps) # detect the beats detections = [] pos = 0 # TODO: make this _much_ faster! while pos < len(activations): # look N frames around the actual position start = pos - look_ahead_frames end = pos + look_ahead_frames if start < 0: # pad with zeros act = np.append(np.zeros(-start), activations[0:end]) elif end > len(activations): # append zeros accordingly zeros = np.zeros(end - len(activations)) act = np.append(activations[start:], zeros) else: act = activations[start:end] # create a interval histogram histogram = self.tempo_estimator.interval_histogram(act) # get the dominant interval interval = self.tempo_estimator.dominant_interval(histogram) # add the offset (i.e. the new detected start position) positions = detect_beats(act, interval, self.look_aside) # correct the beat positions positions += start # search the closest beat to the predicted beat position pos = positions[(np.abs(positions - pos)).argmin()] # append to the beats detections.append(pos) pos += interval # convert detected beats to a list of timestamps detections = np.array(detections) / float(self.fps) # remove beats with negative times and return them return detections[np.searchsorted(detections, 0):] # only return beats with a bigger inter beat interval than that of the # maximum allowed tempo # return np.append(detections[0], detections[1:][np.diff(detections) > # (60. / max_bpm)]) @classmethod def add_arguments(cls, parser, look_aside=LOOK_ASIDE, look_ahead=LOOK_AHEAD): """ Add beat tracking related arguments to an existing parser. :param parser: existing argparse parser :param look_aside: look this fraction of a beat interval to each side of the assumed next beat position to look for the most likely position of the next beat :param look_ahead: look N seconds in both directions to determine the local tempo and align the beats accordingly :return: beat argument parser group Parameters are included in the group only if they are not 'None'. """ # add beat detection related options to the existing parser g = parser.add_argument_group('beat detection arguments') # TODO: unify look_aside with CRFBeatDetection's interval_sigma if look_aside is not None: g.add_argument('--look_aside', action='store', type=float, default=look_aside, help='look this fraction of a beat interval to ' 'each side of the assumed next beat position ' 'to look for the most likely position of the ' 'next beat [default=%(default).2f]') if look_ahead is not None: g.add_argument('--look_ahead', action='store', type=float, default=look_ahead, help='look this many seconds in both directions ' 'to determine the local tempo and align the ' 'beats accordingly [default=%(default).2f]') # return the argument group so it can be modified if needed return g @classmethod def add_tempo_arguments(cls, parser, method=TEMPO_METHOD, min_bpm=MIN_BPM, max_bpm=MAX_BPM, act_smooth=ACT_SMOOTH, hist_smooth=HIST_SMOOTH, alpha=ALPHA): """ Add tempo arguments to an existing parser. :param parser: existing argparse parser :param method: tempo estimation method ['comb', 'acf'] :param min_bpm: minimum tempo [bpm] :param max_bpm: maximum tempo [bpm] :param act_smooth: smooth the activations over N seconds :param hist_smooth: smooth the tempo histogram over N bins :param alpha: scaling factor of the comb filter :return: tempo argument parser group """ # TODO: import the TempoEstimation here otherwise we have a # loop. This is super ugly, but right now I can't think of a # better solution... from madmom.features.tempo import TempoEstimationProcessor as Tempo return Tempo.add_arguments(parser, method=method, min_bpm=min_bpm, max_bpm=max_bpm, act_smooth=act_smooth, hist_smooth=hist_smooth, alpha=alpha)