Source code for libfmp.c3.c3s3_tempo_curve

"""
Module: libfmp.c3.c3s3_tempo_curve
Author: Meinard Müller, Frank Zalkow
License: The MIT license, https://opensource.org/licenses/MIT

This file is part of the FMP Notebooks (https://www.audiolabs-erlangen.de/FMP)
"""
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.ticker import ScalarFormatter
import librosa
from scipy import signal
from scipy.interpolate import interp1d
import scipy.ndimage.filters

import libfmp.c3


[docs]def compute_score_chromagram(score, Fs_beat): """Compute chromagram from score representation Notebook: C3/C3S3_MusicAppTempoCurve.ipynb Args: score (list): Score representation Fs_beat (scalar): Sampling rate for beat axis Returns: X_score (np.ndarray): Chromagram representation X_score t_beat (np.ndarray): Time axis t_beat (given in beats) """ score_beat_min = min(n[0] for n in score) score_beat_max = max(n[0] + n[1] for n in score) beat_res = 1.0 / Fs_beat t_beat = np.arange(score_beat_min, score_beat_max, beat_res) X_score = np.zeros((12, len(t_beat))) for start, duration, pitch, velocity, label in score: start_idx = int(round(start / beat_res)) end_idx = int(round((start + duration) / beat_res)) cur_chroma = int(round(pitch)) % 12 X_score[cur_chroma, start_idx:end_idx] += velocity X_score = librosa.util.normalize(X_score, norm=2) return X_score, t_beat
[docs]def plot_measure(ax, measure_pos): """Plot measure positions Notebook: C3/C3S3_MusicAppTempoCurve.ipynb Args: ax (mpl.axes.Axes): Figure axis measure_pos (list or np.ndarray): Array containing measure positions """ y_min, y_max = ax.get_ylim() ax.vlines(measure_pos, y_min, y_max, color='r') for m in range(len(measure_pos)): ax.text(measure_pos[m], y_max, '%s' % (m + 1), color='r', backgroundcolor='mistyrose', verticalalignment='top', horizontalalignment='left')
[docs]def compute_strict_alignment_path(P): """Compute strict alignment path from a warping path Notebook: C3/C3S3_MusicAppTempoCurve.ipynb Args: P (list or np.ndarray): Warping path Returns: P_mod (list or np.ndarray): Strict alignment path """ # Initialize P_mod and enforce start boundary condition P_mod = np.zeros(P.shape) P_mod[0] = P[0] N, M = P[-1] # Go through all cells of P until reaching last row or column assert N > 1 and M > 1, 'Length of sequences must be longer than one.' i, j = 0, 0 n1, m1 = P[i] while True: i += 1 n2, m2 = P[i] if n2 == N or m2 == M: # If last row or column is reached, quit loop break if n2 > n1 and m2 > m1: # Strict monotonicity condition is fulfuilled j += 1 P_mod[j] = n2, m2 n1, m1 = n2, m2 j += 1 # Enforce end boundary condition P_mod[j] = P[-1] P_mod = P_mod[:j+1] return P_mod
[docs]def compute_strict_alignment_path_mask(P): """Compute strict alignment path from a warping path Notebook: C3/C3S3_MusicAppTempoCurve.ipynb Args: P (list or np.ndarray): Wapring path Returns: P_mod (list or np.ndarray): Strict alignment path """ P = np.array(P, copy=True) N, M = P[-1] # Get indices for strict monotonicity keep_mask = (P[1:, 0] > P[:-1, 0]) & (P[1:, 1] > P[:-1, 1]) # Add first index to enforce start boundary condition keep_mask = np.concatenate(([True], keep_mask)) # Remove all indices for of last row or column keep_mask[(P[:, 0] == N) | (P[:, 1] == M)] = False # Add last index to enforce end boundary condition keep_mask[-1] = True P_mod = P[keep_mask, :] return P_mod
[docs]def plot_tempo_curve(f_tempo, t_beat, ax=None, figsize=(8, 2), color='k', logscale=False, xlabel='Time (beats)', ylabel='Temp (BPM)', xlim=None, ylim=None, label='', measure_pos=[]): """Plot a tempo curve Notebook: C3/C3S3_MusicAppTempoCurve.ipynb Args: f_tempo: Tempo curve t_beat: Time axis of tempo curve (given as sampled beat axis) ax: Plot either as figure (ax==None) or into axis (ax==True) (Default value = None) figsize: Size of figure (Default value = (8, 2)) color: Color of tempo curve (Default value = 'k') logscale: Use linear (logscale==False) or logartihmic (logscale==True) tempo axis (Default value = False) xlabel: Label for x-axis (Default value = 'Time (beats)') ylabel: Label for y-axis (Default value = 'Temp (BPM)') xlim: Limits for x-axis (Default value = None) ylim: Limits for x-axis (Default value = None) label: Figure labels when plotting into axis (ax==True) (Default value = '') measure_pos: Plot measure positions as spefified (Default value = []) Returns: fig: figure handle ax: axes handle """ fig = None if ax is None: fig = plt.figure(figsize=figsize) ax = plt.subplot(1, 1, 1) ax.plot(t_beat, f_tempo, color=color, label=label) ax.set_title('Tempo curve') if xlim is None: xlim = [t_beat[0], t_beat[-1]] if ylim is None: ylim = [np.min(f_tempo) * 0.9, np.max(f_tempo) * 1.1] ax.set_xlim(xlim) ax.set_ylim(ylim) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) ax.grid(True, which='both') if logscale: ax.set_yscale('log') ax.yaxis.set_major_formatter(ScalarFormatter()) ax.yaxis.set_minor_formatter(ScalarFormatter()) # ax.set_yticks([], minor=True) # yticks = np.arange(ylim[0], ylim[1]+1, 10) # ax.set_yticks(yticks) plot_measure(ax, measure_pos) return fig, ax
[docs]def compute_tempo_curve(score, x, Fs=22050, Fs_beat=10, N=4410, H=2205, shift=0, sigma=np.array([[1, 0], [0, 1], [2, 1], [1, 2], [1, 1]]), win_len_beat=4): """Compute a tempo curve Notebook: C3/C3S3_MusicAppTempoCurve.ipynb Args: score (list): Score representation x (np.ndarray): Audio signal Fs (scalar): Samping rate of audio signal (Default value = 22050) Fs_beat (scalar): Sampling rate for beat axis (Default value = 10) N (int): Window size for computing audio chromagram (Default value = 4410) H (int): Hop size for computing audio chromagram (Default value = 2205) shift (int): Cyclic chroma shift applied to audio chromagram (Default value = 0) sigma (np.ndarray): Step size set used for DTW (Default value = np.array([[1, 0], [0, 1], [2, 1], [1, 2], [1, 1]])) win_len_beat (float): Window length (given in beats) used for smoothing tempo curve (Default value = 4) Returns: f_tempo (np.ndarray): Tempo curve t_beat (np.ndarray): Time axis (given in beats) """ # Compute score an audio chromagram X_score, t_beat = compute_score_chromagram(score, Fs_beat) Fs_X = Fs / H X = librosa.feature.chroma_stft(y=x, sr=Fs, norm=2, tuning=0, hop_length=H, n_fft=N) X = np.roll(X, shift, axis=0) # Apply DTW to compte C, D, P C = libfmp.c3.compute_cost_matrix(X, X_score, metric='euclidean') D, P = librosa.sequence.dtw(C=C, step_sizes_sigma=sigma) P = P[::-1, :] # reverse P P_mod = compute_strict_alignment_path(P) # Convert path into beat-time function and interpolte t_path_beat = P_mod[:, 1] / Fs_beat f_path_sec = P_mod[:, 0] / Fs_X f_sec = interp1d(t_path_beat, f_path_sec, kind='linear', fill_value='extrapolate')(t_beat) # Compute difference and smooth with Hann window f_diff_sec = np.diff(f_sec) * Fs_beat pad = np.array([f_diff_sec[-1]]) f_diff_sec = np.concatenate((f_diff_sec, pad)) # f_diff_sec = np.concatenate((f_diff_sec, np.array([0]) )) filt_len = int(win_len_beat * Fs_beat) filt_win = signal.hann(filt_len) filt_win = filt_win / np.sum(filt_win) f_diff_smooth_sec = scipy.ndimage.filters.convolve(f_diff_sec, filt_win, mode='reflect') # Compute tempo curve f_tempo = 1. / f_diff_smooth_sec * 60 return f_tempo, t_beat