"""
Module: libfmp.c3.c3s1_transposition_tuning
Author: Meinard Müller
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 scipy.interpolate import interp1d
from scipy import signal
import libfmp.b
import libfmp.c2
[docs]def cyclic_shift(C, shift=1):
"""Cyclically shift a chromagram
Notebook: C3/C3S1_TranspositionTuning.ipynb
Args:
C (np.ndarray): Chromagram
shift (int): Tranposition shift (Default value = 1)
Returns:
C_shift (np.ndarray): Cyclically shifted chromagram
"""
C_shift = np.roll(C, shift=shift, axis=0)
return C_shift
[docs]def compute_freq_distribution(x, Fs, N=16384, gamma=100.0, local=True, filt=True, filt_len=101):
"""Compute an overall frequency distribution
Notebook: C3/C3S1_TranspositionTuning.ipynb
Args:
x (np.ndarray): Signal
Fs (scalar): Sampling rate
N (int): Window size (Default value = 16384)
gamma (float): Constant for logarithmic compression (Default value = 100.0)
local (bool): Computes STFT and averages; otherwise computes global DFT (Default value = True)
filt (bool): Applies local frequency averaging and by rectification (Default value = True)
filt_len (int): Filter length for local frequency averaging (length given in cents) (Default value = 101)
Returns:
v (np.ndarray): Vector representing an overall frequency distribution
F_coef_cents (np.ndarray): Frequency axis (given in cents)
"""
if local:
# Compute an STFT and sum over time
if N > len(x)//2:
raise Exception('The signal length (%d) should be twice as long as the window length (%d)' % (len(x), N))
Y, T_coef, F_coef = libfmp.c2.stft_convention_fmp(x=x, Fs=Fs, N=N, H=N//2, mag=True, gamma=gamma)
# Error "range() arg 3 must not be zero" occurs when N is too large. Why?
Y = np.sum(Y, axis=1)
else:
# Compute a single DFT for the entire signal
N = len(x)
Y = np.abs(np.fft.fft(x)) / Fs
Y = Y[:N//2+1]
Y = np.log(1 + gamma * Y)
# Y = libfmp.c3.log_compression(Y, gamma=100)
F_coef = np.arange(N // 2 + 1).astype(float) * Fs / N
# Convert linearly spaced frequency axis in logarithmic axis (given in cents)
# The minimum frequency F_min corresponds 0 cents.
f_pitch = lambda p: 440 * 2 ** ((p - 69) / 12)
p_min = 24 # C1, MIDI pitch 24
F_min = f_pitch(p_min) # 32.70 Hz
p_max = 108 # C8, MIDI pitch 108
F_max = f_pitch(p_max) # 4186.01 Hz
F_coef_log, F_coef_cents = libfmp.c2.compute_f_coef_log(R=1, F_min=F_min, F_max=F_max)
Y_int = interp1d(F_coef, Y, kind='cubic', fill_value='extrapolate')(F_coef_log)
v = Y_int / np.max(Y_int)
if filt:
# Subtract local average and rectify
filt_kernel = np.ones(filt_len)
Y_smooth = signal.convolve(Y_int, filt_kernel, mode='same') / filt_len
# Y_smooth = signal.medfilt(Y_int, filt_len)
Y_rectified = Y_int - Y_smooth
Y_rectified[Y_rectified < 0] = 0
v = Y_rectified / np.max(Y_rectified)
return v, F_coef_cents
[docs]def template_comb(M, theta=0):
"""Compute a comb template on a pitch axis
Notebook: C3/C3S1_TranspositionTuning.ipynb
Args:
M (int): Length template (given in cents)
theta (int): Shift parameter (given in cents); -50 <= theta < 50 (Default value = 0)
Returns:
template (np.ndarray): Comb template shifted by theta
"""
template = np.zeros(M)
peak_positions = (np.arange(0, M, 100) + theta)
peak_positions = np.intersect1d(peak_positions, np.arange(M)).astype(int)
template[peak_positions] = 1
return template
[docs]def tuning_similarity(v):
"""Compute tuning similarity
Notebook: C3/C3S1_TranspositionTuning.ipynb
Args:
v (np.ndarray): Vector representing an overall frequency distribution
Returns:
theta_axis (np.ndarray): Axis consisting of all tuning parameters -50 <= theta < 50
sim (np.ndarray): Similarity values for all tuning parameters
ind_max (int): Maximizing index
theta_max (int): Maximizing tuning parameter
template_max (np.ndarray): Similiarty-maximizing comb template
"""
theta_axis = np.arange(-50, 50) # Axis (given in cents)
num_theta = len(theta_axis)
sim = np.zeros(num_theta)
M = len(v)
for i in range(num_theta):
theta = theta_axis[i]
template = template_comb(M=M, theta=theta)
sim[i] = np.inner(template, v)
sim = sim / np.max(sim)
ind_max = np.argmax(sim)
theta_max = theta_axis[ind_max]
template_max = template_comb(M=M, theta=theta_max)
return theta_axis, sim, ind_max, theta_max, template_max
[docs]def plot_tuning_similarity(sim, theta_axis, theta_max, ax=None, title=None, figsize=(4, 3)):
"""Plots tuning similarity
Notebook: C3/C3S1_TranspositionTuning.ipynb
Args:
sim: Similarity values
theta_axis: Axis consisting of cent values [-50:49]
theta_max: Maximizing tuning parameter
ax: Axis (in case of ax=None, figure is generated) (Default value = None)
title: Title of figure (or subplot) (Default value = None)
figsize: Size of figure (only used when ax=None) (Default value = (4, 3))
Returns:
fig: Handle for figure
ax: Handle for axes
line: handle for line plot
"""
fig = None
if ax is None:
fig = plt.figure(figsize=figsize)
ax = plt.subplot(1, 1, 1)
if title is None:
title = 'Estimated tuning: %d cents' % theta_max
line = ax.plot(theta_axis, sim, 'k')
ax.set_xlim([theta_axis[0], theta_axis[-1]])
ax.set_ylim([0, 1.1])
ax.plot([theta_max, theta_max], [0, 1.1], 'r')
ax.set_xlabel('Tuning parameter (cents)')
ax.set_ylabel('Similarity')
ax.set_title(title)
if fig is not None:
plt.tight_layout()
return fig, ax, line
[docs]def plot_freq_vector_template(v, F_coef_cents, template_max, theta_max, ax=None, title=None, figsize=(8, 3)):
"""Plots frequency distribution and similarity-maximizing template
Notebook: C3/C3S1_TranspositionTuning.ipynb
Args:
v: Vector representing an overall frequency distribution
F_coef_cents: Frequency axis
template_max: Similarity-maximizing template
theta_max: Maximizing tuning parameter
ax: Axis (in case of ax=None, figure is generated) (Default value = None)
title: Title of figure (or subplot) (Default value = None)
figsize: Size of figure (only used when ax=None) (Default value = (8, 3))
Returns:
fig: Handle for figure
ax: Handle for axes
line: handle for line plot
"""
fig = None
if ax is None:
fig = plt.figure(figsize=figsize)
ax = plt.subplot(1, 1, 1)
if title is None:
title = r'Frequency distribution with maximizing comb template ($\theta$ = %d cents)' % theta_max
line = ax.plot(F_coef_cents, v, c='k', linewidth=1)
ax.set_xlim([F_coef_cents[0], F_coef_cents[-1]])
ax.set_ylim([0, 1.1])
x_ticks_freq = np.array([0, 1200, 2400, 3600, 4800, 6000, 7200, 8000])
ax.plot(F_coef_cents, template_max * 1.1, 'r:', linewidth=0.5)
ax.set_xticks(x_ticks_freq)
ax.set_xlabel('Frequency (cents)')
plt.title(title)
if fig is not None:
plt.tight_layout()
return fig, ax, line