Module guitarsounds.analysis
Expand source code
import librosa
from soundfile import write
import IPython.display as ipd
import matplotlib.ticker as ticker
import matplotlib.pyplot as plt
import matplotlib.cm
import numpy as np
import os
from noisereduce.noisereducev1 import reduce_noise
import scipy
import scipy.optimize
import scipy.integrate
import scipy.interpolate
from scipy import signal as sig
from guitarsounds.parameters import sound_parameters
import guitarsounds.utils as utils
from tabulate import tabulate
from timbral_models import timbral_extractor
"""
Getting the sound parameters from the guitarsounds_parameters.py file
"""
SP = sound_parameters()
"""
Classes
"""
class SoundPack(object):
"""
A class to store and analyse multiple sounds
Some methods are only available for the case with two sounds
"""
def __init__(self, *sounds, names=None, fundamentals=None, SoundParams=None, equalize_time=True):
"""
The SoundPack can be instantiated from existing Sound class instances, either in a list or as
multiple arguments
The class can also handle the creation of Sound class instances if the arguments are filenames,
either a list or multiple arguments.
If the number of Sound contained is equal to two, the SoundPack will be 'dual' and the associated methods
will be available
If it contains multiple sounds the SoundPack will be multiple and a reduced number of methods will work
A list of names as strings and fundamental frequencies can be specified when creating the SoundPack
If equalize_time is set to False, the contained sounds will not be trimmed to the same length.
Examples :
```
Sound_Test = SoundPack('sounds/test1.wav', 'sounds/test2.wav', names=['A', 'B'], fundamentals = [134, 134])
sounds = [sound1, sound2, sound3, sound4, sound5] # instances of the Sound class
large_Test = SoundPack(sounds, names=['1', '2', '3', '4', '5'])
```
"""
# create a copy of the sound parameters
if SoundParams is None:
self.SP = SP
else:
self.SP = SoundParams
# Check if the sounds argument is a list
if type(sounds[0]) is list:
sounds = sounds[0] # unpack the list
# Check for special case
if len(sounds) == 2:
# special case to compare two sounds
self.kind = 'dual'
elif len(sounds) > 1:
# general case for multiple sounds
self.kind = 'multiple'
if type(sounds[0]) is str:
self.sounds_from_files(sounds, names=names, fundamentals=fundamentals)
else:
self.sounds = sounds
# Assign a default value to names
if names is None:
names = [str(n) for n in np.arange(1, len(sounds) + 1)]
for sound, n in zip(self.sounds, names):
sound.name = n
# sound name defined in constructor
elif names and (len(names) == len(self.sounds)):
for sound, n in zip(self.sounds, names):
sound.name = n
if equalize_time:
self.equalize_time()
# Define bin strings
self.bin_strings = [*list(self.SP.bins.__dict__.keys())[1:], 'brillance']
# Sort according to fundamental
key = np.argsort([sound.fundamental for sound in self.sounds])
self.sounds = np.array(self.sounds)[key]
def sounds_from_files(self, sound_files, names=None, fundamentals=None):
"""
Create Sound class instances and assign them to the SoundPack from a list of files
:param sound_files: sound filenames
:param names: sound names
:param fundamentals: user specified fundamental frequencies
:return: None
"""
# Make the default name list from sound filenames if none is supplied
if (names is None) or (len(names) != len(sound_files)):
names = [file[:-4] for file in sound_files] # remove the .wav
# If the fundamentals are not supplied or mismatch in number None is used
if (fundamentals is None) or (len(fundamentals) != len(sound_files)):
fundamentals = len(sound_files) * [None]
# Create Sound instances from files
self.sounds = []
for file, name, fundamental in zip(sound_files, names, fundamentals):
self.sounds.append(Sound(file, name=name, fundamental=fundamental,
SoundParams=self.SP).condition(return_self=True))
def equalize_time(self):
"""
Trim the sounds so that they all have the length of the shortest sound, trimming is done at the end.
:return: None
"""
trim_index = np.min([len(sound.signal.signal) for sound in self.sounds])
trimmed_sounds = []
for sound in self.sounds:
new_sound = sound
new_sound.signal = new_sound.signal.trim_time(trim_index / sound.signal.sr)
new_sound.bin_divide()
trimmed_sounds.append(new_sound)
self.sounds = trimmed_sounds
def normalize(self):
"""
Normalize all the signals in the SoundPack and returns a normaized
instance of itself
:return: SoundPack with normalized signals
"""
new_sounds = []
names = [sound.name for sound in self.sounds]
fundamentals = [sound.fundamental for sound in self.sounds]
for sound in self.sounds:
sound.signal = sound.signal.normalize()
new_sounds.append(sound)
return SoundPack(new_sounds, names=names, fundamentals=fundamentals, SoundParams=self.SP, equalize_time=False)
"""
Methods for all SoundPacks
"""
def plot(self, kind, **kwargs):
"""
Superimposed plot of all the sounds on one figure for a specific kind
__ Multiple SoundPack Method __
Plots a specific signal.plot for all sounds on the same figure
Ex : compare_plot('fft') plots the fft of all sounds on a single figure
The color argument is set to none so that the plots have different colors
:param kind: Attribute passed to the `signal.plot()` method
:param kwargs: key words arguments to pass to the `signal.plot()` method
:return: None
"""
plt.figure(figsize=(8, 6))
for sound in self.sounds:
kwargs['label'] = sound.name
kwargs['color'] = None
sound.signal.old_plot(kind, **kwargs)
plt.title(kind + ' plot')
if kind == 'timbre':
plt.legend(bbox_to_anchor=(1.3, 0.9))
else:
plt.legend()
def compare_plot(self, kind, **kwargs):
"""
Plots all the sounds on different figures to compare them for a specific kind
__ Multiple SoundPack Method __
Draws the same kind of plot on a different axis for each sound
Example : `SoundPack.compare_plot('peaks')` with 4 Sounds will plot a figure with 4 axes, with each
a different 'peak' plot.
:param kind: kind argument passed to `Signal.plot()`
:param kwargs: key word arguments passed to Signal.plot()
:return: None
"""
# if a dual SoundPack : only plot two big plots
if self.kind == 'dual':
if kind == 'timbre':
fig, axs = plt.subplots(1, 2, figsize=(8, 4), subplot_kw={'projection': 'polar'})
for sound, ax in zip(self.sounds, axs):
plt.sca(ax)
sound.signal.old_plot(kind, **kwargs)
ax.set_title(kind + ' ' + sound.name)
else:
fig, axs = plt.subplots(1, 2, figsize=(12, 4))
for sound, ax in zip(self.sounds, axs):
plt.sca(ax)
sound.signal.old_plot(kind, **kwargs)
ax.set_title(kind + ' ' + sound.name)
plt.tight_layout()
# If a multiple SoundPack : plot on a grid of axes
elif self.kind == 'multiple':
# find the n, m values for the subplots line and columns
n = len(self.sounds)
if n // 4 >= 10:
# a lot of sounds
cols = 4
elif n // 3 >= 10:
# many sounds
cols = 3
elif n // 2 <= 4:
# a few sounds
cols = 2
remainder = n % cols
if remainder == 0:
rows = n // cols
else:
rows = n // cols + 1
fig, axs = plt.subplots(rows, cols, figsize=(12, 4 * rows))
axs = axs.reshape(-1)
for sound, ax in zip(self.sounds, axs):
plt.sca(ax)
sound.signal.old_plot(kind, **kwargs)
title = ax.get_title()
title = sound.name + ' ' + title
ax.set_title(title)
if remainder != 0:
for ax in axs[-(cols - remainder):]:
ax.set_axis_off()
plt.tight_layout()
def freq_bin_plot(self, f_bin='all'):
"""
Plots the log envelop of specified frequency bins
__ Multiple SoundPack Method __
A function to compare signals decomposed frequency wise in the time domain on a logarithm scale.
The methods plots all the sounds and plots their frequency bins according to the frequency bin argument f_bin.
Example : SoundPack.freq_bin_plot(f_bin='mid') will plot the log-scale envelop of the 'mid' signal of every
sound in the SoundPack
f_bin: frequency bins to compare, Supported arguments are :
'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
"""
if f_bin == 'all':
# Create one plot per bin
fig, axs = plt.subplots(3, 2, figsize=(12, 12))
axs = axs.reshape(-1)
for key, ax in zip([*list(self.SP.bins.__dict__.keys())[1:], 'brillance'], axs):
plt.sca(ax)
# plot every sound for a frequency bin
norm_factors = np.array([son.bins[key].normalize().norm_factor for son in self.sounds])
for i, son in enumerate(self.sounds):
son.bins[key].normalize().old_plot('log envelop', label=son.name)
plt.xscale('log')
plt.legend()
title0 = ' ' + key + ' : ' + str(int(son.bins[key].range[0])) + ' - ' + str(
int(son.bins[key].range[1])) + ' Hz, '
title1 = 'Norm. Factors : '
title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors)
plt.title(title0 + title1 + title2)
plt.tight_layout()
elif f_bin in [*list(SP.bins.__dict__.keys())[1:], 'brillance']:
plt.figure(figsize=(10, 4))
# Plot every envelop for a single frequency bin
norm_factors = np.array([son.bins[f_bin].normalize().norm_factor for son in self.sounds])
for i, son in enumerate(self.sounds):
son.bins[f_bin].normalize().old_plot('log envelop', label=(str(i + 1) + '. ' + son.name))
plt.xscale('log')
plt.legend()
title0 = ' ' + f_bin + ' : ' + str(int(son.bins[f_bin].range[0])) + ' - ' + str(
int(son.bins[f_bin].range[1])) + ' Hz, '
title1 = 'Norm. Factors : '
title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors)
plt.title(title0 + title1 + title2)
else:
print('invalid frequency bin')
def combine_envelop(self, kind='signal', difference_factor=1, show_sounds=True, show_rejects=True, **kwargs):
"""
__ Multiple SoundPack Method __
Combines the envelops of the Sounds contained in the SoundPack, Sounds having a too large difference factor
from the average are rejected.
:param kind: wich signal to use from :
'signal', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
:param difference_factor: threshold to reject a sound from the combinaison, can be adjusted to reject
or include more sounds.
:param show_sounds: If True all the included Sounds are shown on the plot
:param show_rejects: If True all the rejected Sounds are shown on the plot
:param kwargs: Key word arguments to pass to the envelop plot.
:return: None
"""
sounds = self.sounds
sample_number = np.min([len(s1.signal.log_envelop()[0]) for s1 in sounds])
if kind == 'signal':
log_envelops = np.stack([s1.signal.normalize().log_envelop()[0][:sample_number] for s1 in sounds])
elif kind in SP.bins.__dict__.keys():
log_envelops = np.stack([s1.bins[kind].normalize().log_envelop()[0][:sample_number] for s1 in sounds])
else:
print('Wrong kind')
average_log_envelop = np.mean(log_envelops, axis=0)
means = np.tile(average_log_envelop, (len(sounds), 1))
diffs = np.sum(np.abs(means - log_envelops), axis=1)
diff = np.mean(diffs) * difference_factor
good_sounds = np.array(sounds)[diffs < diff]
rejected_sounds = np.array(sounds)[diffs > diff]
average_log_envelop = np.mean(log_envelops[diffs < diff], axis=0)
norm_factors = np.array([s1.signal.normalize().norm_factor for s1 in good_sounds])
if kind == 'signal':
if show_sounds:
for s1 in good_sounds[:-1]:
s1.signal.normalize().old_plot(kind='log envelop', alpha=0.2, color='k')
sounds[-1].signal.normalize().old_plot(kind='log envelop', alpha=0.2, color='k', label='sounds')
if show_rejects:
if len(rejected_sounds) > 1:
for s1 in rejected_sounds[:-1]:
s1.signal.normalize().old_plot(kind='log envelop', alpha=0.3, color='r')
rejected_sounds[-1].signal.normalize().old_plot(kind='log envelop', alpha=0.3, color='r',
label='rejected sounds')
if len(rejected_sounds) == 1:
rejected_sounds[0].signal.normalize().plot(kind='log envelop', alpha=0.3, color='r',
label='rejected sounds')
if len(good_sounds) > 0:
if 'label' in kwargs.keys():
plt.plot(good_sounds[0].signal.log_envelop()[1][:len(average_log_envelop)], average_log_envelop,
**kwargs)
else:
plt.plot(good_sounds[0].signal.log_envelop()[1][:len(average_log_envelop)], average_log_envelop,
label='average', color='k', **kwargs)
else:
if show_sounds:
for s1 in good_sounds[:-1]:
s1.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.2, color='k')
sounds[-1].bins[kind].normalize().old_plot(kind='log envelop', alpha=0.2, color='k', label='sounds')
if show_rejects:
if len(rejected_sounds) > 1:
for s2 in rejected_sounds[:-1]:
s2.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r')
rejected_sounds[-1].bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r',
label='rejected sounds')
if len(rejected_sounds) == 1:
rejected_sounds.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r',
label='rejected sounds')
plt.plot(good_sounds[0].signal.log_envelop()[1][:sample_number], average_log_envelop, color='k', **kwargs)
plt.xlabel('time (s)')
plt.ylabel('Amplitude')
plt.legend()
plt.xscale('log')
print('Number of rejected sounds : ' + str(len(rejected_sounds)))
print('Number of sounds included : ' + str(len(good_sounds)))
print('Maximum normalisation factor : ' + str(np.around(np.max(norm_factors), 0)) + 'x')
print('Minimum normalisation factor : ' + str(np.around(np.min(norm_factors), 0)) + 'x')
def fundamentals(self):
"""
__ Multiple Soundpack Method __
Displays the fundamentals of every sound in the SoundPack
:return: None
"""
names = np.array([sound.name for sound in self.sounds])
fundamentals = np.array([np.around(sound.fundamental, 1) for sound in self.sounds])
key = np.argsort(fundamentals)
table_data = [names[key], fundamentals[key]]
table_data = np.array(table_data).transpose()
print(tabulate(table_data, headers=['Name', 'Fundamental (Hz)']))
def integral_plot(self, f_bin='all'):
"""
Normalized cumulative bin power plot for the frequency bins
__ Multiple SoundPack Method __
Plots the cumulative integral plot of specified frequency bins
see help(Plot.integral)
f_bin: frequency bins to compare, Supported arguments are :
'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
"""
if f_bin == 'all':
# create a figure with 6 axes
fig, axs = plt.subplots(3, 2, figsize=(12, 12))
axs = axs.reshape(-1)
for key, ax in zip(self.bin_strings, axs):
plt.sca(ax)
norm_factors = np.array([son.bins[key].normalize().norm_factor for son in self.sounds])
for sound in self.sounds:
sound.bins[key].plot.integral(label=sound.name)
plt.legend()
title0 = ' ' + key + ' : ' + str(int(sound.bins[key].range[0])) + ' - ' + str(
int(sound.bins[key].range[1])) + ' Hz, '
title1 = 'Norm. Factors : '
title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors)
plt.title(title0 + title1 + title2)
plt.title(title0 + title1 + title2)
plt.tight_layout()
elif f_bin in self.bin_strings:
fig, ax = plt.subplots(figsize=(6, 4))
plt.sca(ax)
norm_factors = np.array([son.bins[f_bin].normalize().norm_factor for son in self.sounds])
for sound in self.sounds:
sound.bins[f_bin].plot.integral(label=sound.name)
plt.legend()
title0 = ' ' + f_bin + ' : ' + str(int(sound.bins[f_bin].range[0])) + ' - ' + str(
int(sound.bins[f_bin].range[1])) + ' Hz, '
title1 = 'Norm. Factors : '
title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors)
plt.title(title0 + title1 + title2)
else:
print('invalid frequency bin')
def bin_power_table(self):
"""
Displays a table with the signal power contained in every frequency bin
The power is computed as the time integral of the signal
"""
# Bin power distribution table
bin_strings = self.bin_strings
integrals = []
# for every sound in the SoundPack
for sound in self.sounds:
integral = []
# for every frequency bin in the sound
for f_bin in bin_strings:
log_envelop, log_time = sound.bins[f_bin].normalize().log_envelop()
integral.append(scipy.integrate.trapezoid(log_envelop, log_time))
# a list of dict for every sound
integrals.append(integral)
# make the table
table_data = np.array([list(bin_strings), *integrals]).transpose()
sound_names = [sound.name for sound in self.sounds]
print('___ Signal Power Frequency Bin Distribution ___ \n')
print(tabulate(table_data, headers=['bin', *sound_names]))
def bin_power_hist(self):
"""
Histogram of the frequency bin power for multiple sounds
frequency bin power is computed as the integral of the bin envelop
"""
# Compute the bin powers
bin_strings = self.bin_strings
integrals = []
# for every sound in the SoundPack
for sound in self.sounds:
integral = []
# for every frequency bin in the sound
for f_bin in bin_strings:
log_envelop, log_time = sound.bins[f_bin].normalize().log_envelop()
integral.append(scipy.integrate.trapezoid(log_envelop, log_time))
# a list of dict for every sound
integrals.append(integral)
# create the bar plotting vectors
fig, ax = plt.subplots(figsize=(6, 6))
# make the bar plot
n = len(self.sounds)
width = 0.8 / n
# get nice colors
cmap = matplotlib.cm.get_cmap('Set2')
for i, sound in enumerate(self.sounds):
x = np.arange(i * width, len(bin_strings) + i * width)
y = integrals[i]
if n < 8:
color = cmap(i)
else:
color = None
if i == n // 2:
ax.bar(x, y, width=width, tick_label=list(bin_strings), label=sound.name, color=color)
else:
ax.bar(x, y, width=width, label=sound.name, color=color)
plt.legend()
"""
Methods for dual SoundPacks
"""
def compare_peaks(self):
"""
Plot to compare the FFT peaks values of two sounds
__ Dual SoundPack Method __
Compares the peaks in the Fourier Transform of two Sounds,
the peak with the highest difference is highlighted
"""
if self.kind == 'dual':
son1 = self.sounds[0]
son2 = self.sounds[1]
index1 = np.where(son1.signal.fft_frequencies() > self.SP.general.fft_range.value)[0][0]
index2 = np.where(son2.signal.fft_frequencies() > self.SP.general.fft_range.value)[0][0]
# Get the peak data from the sounds
peaks1 = son1.signal.peaks()
peaks2 = son2.signal.peaks()
freq1 = son1.signal.fft_frequencies()[:index1]
freq2 = son2.signal.fft_frequencies()[:index2]
fft1 = son1.signal.fft()[:index1]
fft2 = son2.signal.fft()[:index2]
peak_distance1 = np.mean([freq1[peaks1[i]] - freq1[peaks1[i + 1]] for i in range(len(peaks1) - 1)]) / 4
peak_distance2 = np.mean([freq2[peaks2[i]] - freq2[peaks2[i + 1]] for i in range(len(peaks2) - 1)]) / 4
peak_distance = np.abs(np.mean([peak_distance1, peak_distance2]))
# Align the two peak vectors
new_peaks1 = []
new_peaks2 = []
for peak1 in peaks1:
for peak2 in peaks2:
if np.abs(freq1[peak1] - freq2[peak2]) < peak_distance:
new_peaks1.append(peak1)
new_peaks2.append(peak2)
new_peaks1 = np.unique(np.array(new_peaks1))
new_peaks2 = np.unique(np.array(new_peaks2))
different_peaks1 = []
different_peaks2 = []
difference_threshold = 0.5
while len(different_peaks1) < 1:
for peak1, peak2 in zip(new_peaks1, new_peaks2):
if np.abs(fft1[peak1] - fft2[peak2]) > difference_threshold:
different_peaks1.append(peak1)
different_peaks2.append(peak2)
difference_threshold -= 0.01
# Plot the output
plt.figure(figsize=(10, 6))
plt.yscale('symlog', linthresh=10e-1)
# Sound 1
plt.plot(freq1, fft1, color='#919191', label=son1.name)
plt.scatter(freq1[new_peaks1], fft1[new_peaks1], color='b', label='peaks')
plt.scatter(freq1[different_peaks1], fft1[different_peaks1], color='g', label='diff peaks')
annotation_string = 'Peaks with ' + str(np.around(difference_threshold, 2)) + ' difference'
plt.annotate(annotation_string, (freq1[different_peaks1[0]] + peak_distance / 2, fft1[different_peaks1[0]]))
# Sound2
plt.plot(freq2, -fft2, color='#3d3d3d', label=son2.name)
plt.scatter(freq2[new_peaks2], -fft2[new_peaks2], color='b')
plt.scatter(freq2[different_peaks2], -fft2[different_peaks2], color='g')
plt.title('Fourier Transform Peak Analysis for ' + son1.name + ' and ' + son2.name)
plt.grid('on')
plt.legend()
else:
print('Unsupported for multiple sounds SoundPacks')
def fft_mirror(self):
"""
Plot the Fourier Transforms of two sounds on opposed axis to compare the spectras
__ Dual SoundPack Method __
The fourier transforms are normalized between 0 and [-1, 1], the y scale is logarithmic
:return: None
"""
if self.kind == 'dual':
son1 = self.sounds[0]
son2 = self.sounds[1]
index = np.where(son1.signal.fft_frequencies() > SP.general.fft_range.value)[0][0]
plt.figure(figsize=(10, 6))
plt.yscale('symlog')
plt.grid('on')
plt.plot(son1.signal.fft_frequencies()[:index], son1.signal.fft()[:index], label=son1.name)
plt.plot(son2.signal.fft_frequencies()[:index], -son2.signal.fft()[:index], label=son2.name)
plt.xlabel('Fréquence (Hz)')
plt.ylabel('Amplitude')
plt.title('Mirror Fourier Transform for ' + son1.name + ' and ' + son2.name)
plt.legend()
else:
print('Unsupported for multiple sounds SoundPacks')
def fft_diff(self, fraction=3, ticks=None):
"""
Plot the difference between the spectral distribution in the two sounds
__ Dual SoundPack Method __
Compare the Fourier Transform of two sounds by computing the differences of the octave bins heights.
The two FTs are superimposed on the first plot to show differences
The difference between the two FTs is plotted on the second plot
:param fraction: octave fraction value used to compute the frequency bins A higher number will show
a more precise comparison, but conclusions may be harder to draw.
:param ticks: If True the frequency bins intervals are used as X axis ticks
:return: None
"""
if self.kind == 'dual':
# Separate the sounds
son1 = self.sounds[0]
son2 = self.sounds[1]
# Compute plotting bins
x_values = utils.octave_values(fraction)
hist_bins = utils.octave_histogram(fraction)
bar_widths = np.array([hist_bins[i + 1] - hist_bins[i] for i in range(0, len(hist_bins) - 1)])
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
plot1 = ax1.hist(son1.signal.fft_bins(), utils.octave_histogram(fraction), color='blue', alpha=0.6,
label=son1.name)
plot2 = ax1.hist(son2.signal.fft_bins(), utils.octave_histogram(fraction), color='orange', alpha=0.6,
label=son2.name)
ax1.set_title('FT Histogram for ' + son1.name + ' and ' + son2.name)
ax1.set_xscale('log')
ax1.set_xlabel('Fréquence (Hz)')
ax1.set_ylabel('Amplitude')
ax1.grid('on')
ax1.legend()
diff = plot1[0] - plot2[0]
n_index = np.where(diff <= 0)[0]
p_index = np.where(diff >= 0)[0]
# Negative difference corresponding to sound 2
ax2.bar(x_values[n_index], diff[n_index], width=bar_widths[n_index], color='orange', alpha=0.6)
# Positive difference corresponding to sound1
ax2.bar(x_values[p_index], diff[p_index], width=bar_widths[p_index], color='blue', alpha=0.6)
ax2.set_title('Difference ' + son1.name + ' - ' + son2.name)
ax2.set_xscale('log')
ax2.set_xlabel('Fréquence (Hz)')
ax2.set_ylabel('<- Son 2 : Son 1 ->')
ax2.grid('on')
if ticks == 'bins':
labels = [label for label in self.SP.bins.__dict__ if label != 'name']
labels.append('brillance')
x = [param.value for param in self.SP.bins.__dict__.values() if param != 'bins']
x.append(11250)
x_formatter = ticker.FixedFormatter(labels)
x_locator = ticker.FixedLocator(x)
ax1.xaxis.set_major_locator(x_locator)
ax1.xaxis.set_major_formatter(x_formatter)
ax1.tick_params(axis="x", labelrotation=90)
ax2.xaxis.set_major_locator(x_locator)
ax2.xaxis.set_major_formatter(x_formatter)
ax2.tick_params(axis="x", labelrotation=90)
else:
print('Unsupported for multiple sounds SoundPacks')
def integral_compare(self, f_bin='all'):
"""
Cumulative bin envelop integral comparison for two signals
__ Dual SoundPack Method __
Plots the cumulative integral plot of specified frequency bins
and their difference as surfaces
f_bin: frequency bins to compare, Supported arguments are :
'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
"""
# Case when plotting all the frequency bins
if f_bin == 'all':
fig, axs = plt.subplots(3, 2, figsize=(16, 16))
axs = axs.reshape(-1)
self.bin_strings = self.sounds[0].bins.keys()
bins1 = self.sounds[0].bins.values()
bins2 = self.sounds[1].bins.values()
for signal1, signal2, bin_string, ax in zip(bins1, bins2, self.bin_strings, axs):
log_envelop1, log_time1 = signal1.normalize().log_envelop()
log_envelop2, log_time2 = signal2.normalize().log_envelop()
integ = scipy.integrate.trapezoid
integral1 = np.array([integ(log_envelop1[:i], log_time1[:i]) for i in np.arange(2, len(log_envelop1), 1)])
integral2 = np.array([integ(log_envelop2[:i], log_time2[:i]) for i in np.arange(2, len(log_envelop2), 1)])
time1 = log_time1[2:len(log_time1):1]
time2 = log_time2[2:len(log_time2):1]
int_index = np.min([integral1.shape[0], integral2.shape[0]])
ax.fill_between(time1, integral1, label=self.sounds[0].name, alpha=0.4)
ax.fill_between(time2, -integral2, label=self.sounds[1].name, alpha=0.4)
ax.fill_between(time2, integral1 - integral2, color='g', label='int diff', alpha=0.6)
ax.set_xlabel('time (s)')
ax.set_ylabel('cummulative power')
ax.set_xscale('log')
ax.set_title(bin_string)
ax.legend()
ax.grid('on')
plt.tight_layout()
elif f_bin in self.bin_strings:
fig, ax = plt.subplots(figsize=(8, 6))
signal1 = self.sounds[0].bins[f_bin]
signal2 = self.sounds[1].bins[f_bin]
log_envelop1, log_time1 = signal1.normalize().log_envelop()
log_envelop2, log_time2 = signal2.normalize().log_envelop()
integ = scipy.integrate.trapezoid
integral1 = np.array([integ(log_envelop1[:i], log_time1[:i]) for i in np.arange(2, len(log_envelop1), 1)])
integral2 = np.array([integ(log_envelop2[:i], log_time2[:i]) for i in np.arange(2, len(log_envelop2), 1)])
time1 = log_time1[2:len(log_time1):1]
time2 = log_time2[2:len(log_time2):1]
int_index = np.min([integral1.shape[0], integral2.shape[0]])
ax.fill_between(time1, integral1, label=self.sounds[0].name, alpha=0.4)
ax.fill_between(time2, -integral2, label=self.sounds[1].name, alpha=0.4)
ax.fill_between(time2, integral1 - integral2, color='g', label='int diff', alpha=0.6)
ax.set_xlabel('time (s)')
ax.set_ylabel('cummulative power')
ax.set_xscale('log')
ax.set_title(f_bin)
ax.legend(loc='upper left')
ax.grid('on')
else:
print('invalid frequency bin')
def coherence_plot(self):
"""
__ Dual SoundPack Method __
computes and plots the coherence between the time signal of two Sounds
:return: None
"""
if self.kind == 'dual':
f, C = sig.coherence(self.sounds[0].signal.signal, self.sounds[1].signal.signal, self.sounds[0].signal.sr)
plt.plot(f, C, color='b')
plt.yscale('log')
plt.xlabel('Fréquence (Hz)')
plt.ylabel('Coherence [0, 1]')
title = 'Cohérence entre les sons ' + self.sounds[0].name + ' et ' + self.sounds[1].name
plt.title(title)
else:
print('Unsupported for multiple sounds SoundPacks')
class Sound(object):
"""
A class to store audio signals obtained from a sound and compare them
"""
def __init__(self, file, name='', fundamental=None, SoundParams=None):
"""
Creates a Sound instance from a .wav file, name as a string and fundamental frequency
value can be user specified.
:param file: file path to the .wav file
:param name: Sound instance name to use in plot legend and titles
:param fundamental: Fundamental frequency value if None the value is estimated
from the FFT (see `Signal.fundamental`).
:param SoundParams: SoundParameters to use in the Sound instance
"""
# create a reference of the parameters
if SoundParams is None:
self.SP = SP
else:
self.SP = SoundParams
if type(file) == str:
# Load the soundfile using librosa
signal, sr = librosa.load(file)
self.file = file
elif type(file) == tuple:
signal, sr = file
# create a Signal class from the signal and sample rate
self.raw_signal = Signal(signal, sr, self.SP)
# Allow user specified fundamental
self.fundamental = fundamental
self.name = name
def condition(self, verbose=True, return_self=False):
"""
A method conditioning the Sound instance.
- Trimming to just before the onset
- Filtering the noise
:param verbose: if True problem with trimming and filtering are reported
:param return_self: If True the method returns the conditioned Sound instance
:return: a conditioned Sound instance if `return_self = True`
"""
self.trim_signal(verbose=verbose)
self.filter_noise(verbose=verbose)
self.bin_divide()
if self.fundamental is None:
self.fundamental = self.signal.fundamental()
self.plot = self.signal.plot
if return_self:
return self
def use_raw_signal(self, normalized=False):
"""
Assigns the raw signal to the `signal` attribute of the Sound instance to
analyze it
:param normalized: if True, the raw signal is first normalized
:return: None
"""
if normalized:
self.signal = self.raw_signal.normalize()
else:
self.signal = self.raw_signal
def bin_divide(self):
"""
Calls the `.make_freq_bins` method of the signal to create the signals associated
to the frequency bins. The bins are all stored in the `.bin` attribute and also as
their names (Ex: `Sound.mid` contains the mid signal).
:return: None
"""
""" a method to divide the main signal into frequency bins"""
# divide in frequency bins
self.bins = self.signal.make_freq_bins()
# unpack the bins
self.bass, self.mid, self.highmid, self.uppermid, self.presence, self.brillance = self.bins.values()
def filter_noise(self, verbose=True):
"""
Filters the noise in the signal attribute
:param verbose: if True problem are printed to the terminal
:return: None
"""
# filter the noise in the Signal class
self.signal = self.trimmed_signal.filter_noise(verbose=verbose)
def trim_signal(self, verbose=True):
"""
A method to trim the signal to a specific time before the onset. The time value
can be changed in the SoundParameters.
:param verbose: if True problems encountered are printed to the terminal
:return: None
"""
# Trim the signal in the signal class
self.trimmed_signal = self.raw_signal.trim_onset(verbose=verbose)
def listen_freq_bins(self):
"""
Method to listen to all the frequency bins of a sound
The bins signals are obtained by filtering the sound signal
with band pass filters.
See guitarsounds.parameters.sound_parameters().bins.info() for the
frequency bin intervals.
"""
for key in self.bins.keys():
print(key)
self.bins[key].normalize().listen()
def plot_freq_bins(self, bins=None):
"""
Method to plot all the frequency bins logarithmic envelops of a sound
The parameter `bins` allows choosing specific frequency bins to plot
By default the function plots all the bins
Supported bins arguments are :
'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
Example :
`Sound.plot_freq_bins(bins=['all])` plots all the frequency bins
`Sound.plot_freq_bins(bins=['bass', 'mid'])` plots the bass and mid bins
"""
try:
value = bins[0]
if value == 'all':
bins = self.bins.keys()
except TypeError:
if bins is None:
bins = self.bins.keys()
for key in bins:
lab = key + ' : ' + str(int(self.bins[key].range[0])) + ' - ' + str(int(self.bins[key].range[1])) + ' Hz'
self.bins[key].old_plot('log envelop', label=lab)
plt.xscale('log')
plt.yscale('log')
plt.legend(fontsize="x-small") # using a named size
def peak_damping(self):
"""
Prints a table with peak damping values and peak frequency values
The peaks are found with the `signal.peaks()` function and the damping
values are computed with the half power bandwith method.
"""
peak_indexes = self.signal.peaks()
frequencies = self.signal.fft_frequencies()[peak_indexes]
damping = self.signal.peak_damping()
table_data = np.array([frequencies, np.array(damping) * 100]).transpose()
print(tabulate(table_data, headers=['Frequency (Hz)', 'Damping ratio (%)']))
def bin_hist(self):
"""
Histogram of the frequency bin power
frequency bin power is computed as the integral of the bin envelop.
See guitarsounds.parameters.sound_parameters().bins.info() for the
frequency bin intervals.
"""
# Compute the bin powers
bin_strings = list(self.bins.keys())
integral = []
for f_bin in bin_strings:
log_envelop, log_time = self.bins[f_bin].normalize().log_envelop()
integral.append(scipy.integrate.trapezoid(log_envelop, log_time))
# create the bar plotting vectors
fig, ax = plt.subplots(figsize=(6, 6))
x = np.arange(0, len(bin_strings))
y = integral
ax.bar(x, y, tick_label=list(bin_strings))
class Signal(object):
"""
A Class to do computations on an audio signal.
The signal is never changed in the class, when transformations are made, a new instance is returned.
"""
def __init__(self, signal, sr, SoundParam, freq_range=None):
""" Create a Signal class from a vector of samples and a sample rate"""
self.SP = SoundParam
self.onset = None
self.signal = signal
self.sr = sr
self.range = freq_range
self.trimmed = None
self.noise = None
self.plot = Plot()
self.plot.parent = self
def time(self):
"""
Returns the time vector associated to the signal
:return: numpy array corresponding to the time values of the signal samples in seconds
"""
return np.linspace(0, len(self.signal) * (1 / self.sr), len(self.signal))
def listen(self):
"""
Method to listen the sound signal in a Jupyter Notebook
Listening to the sounds imported in the analysis tool allows the
user to validate if the sound was well trimmed and filtered
A temporary file is created, the IPython display Audio function is
called on it and then the file is removed
"""
file = 'temp.wav'
write(file, self.signal, self.sr)
ipd.display(ipd.Audio(file))
os.remove(file)
def old_plot(self, kind, **kwargs):
"""
Convenience function for the different signal plots
Calls the function corresponding to Plot.kind()
See help(guitarsounds.analysis.Plot) for info on the different plots
"""
self.plot.method_dict[kind](**kwargs)
def fft(self):
"""
Computes the Fast Fourier Transform of the signal and returns the vector.
:return: Fast Fourier Transform amplitude values in a numpy array
"""
fft = np.fft.fft(self.signal)
fft = np.abs(fft[:int(len(fft) // 2)]) # Only the symmetric of the absolute value
return fft / np.max(fft)
def peaks(self, max_freq=None, height=False, result=False):
"""
Computes the harmonic peaks indexes from the FFT of the signal
:param max_freq: Supply a max frequency value overiding the one in guitarsounds_parameters
:param height: if True the height threshold is returned to be used in the 'peaks' plot
:param result: if True the Scipy peak finding results dictionary is returned
:return: peak indexes
"""
# Replace None by the default value
if max_freq is None:
max_freq = self.SP.general.fft_range.value
# Get the fft and fft frequencies from the signal
fft, fft_freq = self.fft(), self.fft_frequencies()
# Find the max index
max_index = np.where(fft_freq >= max_freq)[0][0]
# Find an approximation of the distance between peaks, this only works for harmonic signals
peak_distance = np.argmax(fft) // 2
# Maximum of the signal in a small region on both ends
fft_max_start = np.max(fft[:peak_distance])
fft_max_end = np.max(fft[max_index - peak_distance:max_index])
# Build the curve below the peaks but above the noise
exponents = np.linspace(np.log10(fft_max_start), np.log10(fft_max_end), max_index)
intersect = 10 ** exponents[peak_distance]
diff_start = fft_max_start - intersect # offset by a small distance so that the first max is not a peak
min_height = 10 ** np.linspace(np.log10(fft_max_start + diff_start), np.log10(fft_max_end), max_index)
first_peak_indexes, _ = sig.find_peaks(fft[:max_index], height=min_height, distance=peak_distance)
number_of_peaks = len(first_peak_indexes)
if number_of_peaks > 0:
average_len = int(max_index / number_of_peaks) * 3
else:
average_len = int(max_index / 3)
if average_len % 2 == 0:
average_len += 1
average_fft = sig.savgol_filter(fft[:max_index], average_len, 1, mode='mirror') * 1.9
min_freq_index = np.where(fft_freq >= 70)[0][0]
average_fft[:min_freq_index] = 1
peak_indexes, res = sig.find_peaks(fft[:max_index], height=average_fft, distance=min_freq_index)
# Remove noisy peaks at the low frequencies
while fft[peak_indexes[0]] < 5e-2:
peak_indexes = np.delete(peak_indexes, 0)
while fft[peak_indexes[-1]] < 1e-4:
peak_indexes = np.delete(peak_indexes, -1)
if not height and not result:
return peak_indexes
elif height:
return peak_indexes, average_fft
elif result:
return peak_indexes, res
elif height and result:
return peak_indexes, height, res
def time_damping(self):
"""
Computes the time wise damping ratio of the signal by fitting a negative exponential curve
to the Signal envelop and computing the ratio with the Signal fundamental frequency.
:return: The damping ratio, a scalar.
"""
# Get the envelop data
envelop_time = self.normalize().envelop_time()
envelop = self.normalize().envelop()
# First point is the maximum because e^-kt is stricly decreasing
first_index = np.argmax(envelop)
# The second point is the first point where the signal crosses the lower_threshold line
second_point_thresh = self.SP.damping.lower_threshold.value
try:
second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh)[0]
except IndexError:
second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh * 2)[0]
# Function to compute the residual for the exponential curve fit
def residual_function(zeta_w, t, s):
"""
Function computing the residual to curve fit a negative exponential to the signal envelop
:param zeta_w: zeta*omega constant
:param t: time vector
:param s: signal
:return: residual
"""
return np.exp(zeta_w[0] * t) - s
zeta_guess = [-0.5]
result = scipy.optimize.least_squares(residual_function, zeta_guess,
args=(envelop_time[first_index:second_index],
envelop[first_index:second_index]))
# Get the zeta*omega constant
zeta_omega = result.x[0]
# Compute the fundamental frequency in radiants of the signal
wd = 2 * np.pi * self.fundamental()
return -zeta_omega / wd
def peak_damping(self):
"""
Computes the frequency wise damping with the half bandwidth method on the Fourier Transform peaks
:return: an array containing the peak damping values
"""
zetas = []
fft_freqs = self.fft_frequencies()
fft = self.fft()[:len(fft_freqs)]
for peak in self.peaks():
peak_frequency = fft_freqs[peak]
peak_height = fft[peak]
root_height = peak_height / np.sqrt(2)
frequency_roots = scipy.interpolate.InterpolatedUnivariateSpline(fft_freqs, fft - root_height).roots()
sorted_roots_indexes = np.argsort(np.abs(frequency_roots - peak_frequency))
w2, w1 = frequency_roots[sorted_roots_indexes[:2]]
w1, w2 = np.sort([w1, w2])
zeta = (w2 - w1) / (2 * peak_frequency)
zetas.append(zeta)
return np.array(zetas)
def fundamental(self):
"""
Returns the fundamental approximated by the first peak of the fft
:return: fundamental value (Hz)
"""
index = self.peaks()[0]
fundamental = self.fft_frequencies()[index]
return fundamental
def cavity_peak(self):
"""
Finds the Hemlotz cavity frequency index from the Fourier Transform by searching for a peak in the expected
range (80 - 100 Hz), if the fundamental is too close to the expected Hemlotz frequency a comment
is printed and None is returned.
:return: If successful the cavity peak index
"""
first_index = np.where(self.fft_frequencies() >= 80)[0][0]
second_index = np.where(self.fft_frequencies() >= 110)[0][0]
cavity_peak = np.argmax(self.fft()[first_index:second_index]) + first_index
if self.fundamental() == self.fft_frequencies()[cavity_peak]:
print('Cavity peak is obscured by the fundamental')
else:
return cavity_peak
def cavity_frequency(self):
"""
Finds the hemlotz cavity frequency from the Fourier Transform by searching for a peak in the expected
range (80 - 100 Hz), if the fundamental is too close to the expected hemlotz frequency a comment
is printed and None is returned.
:return: If successful, the cavity peak frequency
"""
first_index = np.where(self.fft_frequencies() >= 80)[0][0]
second_index = np.where(self.fft_frequencies() >= 110)[0][0]
cavity_peak = np.argmax(self.fft()[first_index:second_index]) + first_index
if self.fundamental() == self.fft_frequencies()[cavity_peak]:
print('Cavity peak is obscured by the fundamental')
return 0
else:
return self.fft_frequencies()[cavity_peak]
def fft_frequencies(self):
"""
Computes the frequency vector associated to the Signal Fourier Transform
:return: an array containing the frequency values.
"""
fft = self.fft()
fft_frequencies = np.fft.fftfreq(len(fft) * 2, 1 / self.sr) # Frequencies corresponding to the bins
return fft_frequencies[:len(fft)]
def fft_bins(self):
"""
Transforms the Fourier Transform signal into a statistic distribution.
Occurences of each frequency varies from 0 to 100 according to their
amplitude.
:return : a list containing the frequency occurences.
"""
# Make the FT values integers
fft_integers = [int(np.around(sample * 100, 0)) for sample in self.fft()]
# Create a list of the frequency occurrences in the signal
occurrences = []
for freq, count in zip(self.fft_frequencies(), fft_integers):
occurrences.append([freq] * count)
# flatten the list
return [item for sublist in occurrences for item in sublist]
def envelop(self):
"""
Method calculating the amplitude envelope of a signal as a
maximum of the absolute value of the signal.
:return: Amplitude envelop of the signal
"""
# Get the hop length
hop_length = self.SP.envelop.hop_length.value
# Compute the envelop
envelop = np.array(
[np.max(np.abs(self.signal[i:i + self.SP.envelop.frame_size.value])) for i in
range(0, len(self.signal), hop_length)])
envelop = np.insert(envelop, 0, 0)
return envelop
def envelop_time(self):
"""
Method calculating the time vector associated to a signal envelop
:return: Time vector associated to the signal envelop
"""
# Get the number of frames from the signal envelop
frames = range(len(self.envelop()))
# Return the envelop frames computed with Librosa
return librosa.frames_to_time(frames, hop_length=self.SP.envelop.hop_length.value)
def log_envelop(self):
"""
Computes the logarithmic scale envelop of the signal.
The width of the samples increases exponentially so that
the envelop appears having a constant window width on
an X axis logarithmic scale.
:return: The log envelop and the time vector associated in a tuple
"""
if self.onset is None:
onset = np.argmax(self.signal)
else:
onset = self.onset
start_time = self.SP.log_envelop.start_time.value
while start_time > (onset / self.sr):
start_time /= 10.
start_exponent = int(np.log10(start_time)) # closest 10^x value for smooth graph
if self.SP.log_envelop.min_window.value is None:
min_window = 15 ** (start_exponent + 4)
if min_window < 15: # Value should at least be 10
min_window = 15
else:
min_window = self.SP.log_envelop.min_window.value
# initial values
current_exponent = start_exponent
current_time = 10 ** current_exponent # start time on log scale
index = int(current_time * self.sr) # Start at the specified time
window = min_window # number of samples per window
overlap = window // 2
log_envelop = []
log_envelop_time = [0] # First value for comparison
while index + window <= len(self.signal):
while log_envelop_time[-1] < 10 ** (current_exponent + 1):
if (index + window) < len(self.signal):
log_envelop.append(np.max(self.signal[index:index + window]))
log_envelop_time.append(self.time()[index])
index += overlap
else:
break
if window * 10 < self.SP.log_envelop.max_window.value:
window = window * 10
else:
window = self.SP.log_envelop.max_window.value
overlap = window // 2
current_exponent += 1
# remove the value where t=0 so the log scale does not break
log_envelop_time.remove(0)
return np.array(log_envelop), np.array(log_envelop_time)
def find_onset(self, verbose=True):
"""
Finds the onset as an increase in more of 50% with the maximum normalized value above 0.5
:param verbose: Prints a warning if the algorithm does not converge
:return: the index of the onset in the signal
"""
# Index corresponding to the onset time interval
window_index = np.ceil(self.SP.onset.onset_time.value * self.sr).astype(int)
# Use the normalized signal to compare against a fixed value
onset_signal = self.normalize()
overlap = window_index // 2 # overlap for algorithm progression
# Initial values
increase = 0
i = 0
broke = False
while increase <= 0.5:
signal_min = np.min(np.abs(onset_signal.signal[i:i + window_index]))
signal_max = np.max(np.abs(onset_signal.signal[i:i + window_index]))
if (signal_max > 0.5) and (signal_min != 0):
increase = signal_max / signal_min
else:
increase = 0
i += overlap
if i + window_index > len(self.signal):
if verbose:
print('Onset detection did not converge \n')
print('Approximating onset with signal max value \n')
broke = True
break
if broke:
return np.argmax(self.signal)
else:
return np.argmax(np.abs(self.signal[i:i + window_index])) + i
def trim_onset(self, verbose=True):
"""
Trim the signal at the onset (max) minus the delay in milliseconds as
Specified in the SoundParameters
:param : verbose if False the warning comments are not displayed
:return : a trimmed signal with a noise attribute
"""
# nb of samples to keep before the onset
delay_samples = int((self.SP.onset.onset_delay.value / 1000) * self.sr)
onset = self.find_onset(verbose=verbose) # find the onset
if onset > delay_samples: # To make sure the index is positive
trimmed_signal = Signal(self.signal[onset - delay_samples:], self.sr, self.SP)
trimmed_signal.noise = self.signal[:onset - delay_samples]
trimmed_signal.trimmed = True
trimmed_signal.onset = np.argmax(trimmed_signal.signal)
return trimmed_signal
else:
if verbose:
print('Signal is too short to be trimmed before onset.')
print('')
self.trimmed = False
return self
def trim_time(self, time_length):
"""
Trims the signal to the specified length and returns a new Signal instance.
:param time_length: desired length of the new signal in seconds.
:return: A trimmed Signal
"""
max_index = int(time_length * self.sr)
time_trimmed_signal = Signal(self.signal[:max_index], self.sr, self.SP)
time_trimmed_signal.time_length = time_length
return time_trimmed_signal
def filter_noise(self, verbose=True):
"""
Method filtering the noise from the recorded signal and returning a filtered signal.
If the signal was not trimmed it is trimmed in place then filtered.
If the signal can not be trimmed it can't be filtered and the original signal is returned
:return : A Signal instance, filtered if possible.
"""
try:
return Signal(reduce_noise(audio_clip=self.signal, noise_clip=self.noise), self.sr, self.SP)
except AttributeError:
if self.trimmed is False:
if verbose:
print('Not sufficient noise in the raw signal, unable to filter.')
print('')
return self
def normalize(self):
"""
Normalizes the signal to [-1, 1] and returns the normalised instance.
:return : A normalized signal
"""
factor = np.max(np.abs(self.signal))
normalised_signal = Signal((self.signal / factor), self.sr, self.SP)
normalised_signal.norm_factor = (1 / factor)
return normalised_signal
def make_freq_bins(self):
"""
Method to divide a signal in frequency bins using butterworth filters
bins are passed as a dict, default values are :
- bass < 100 Hz
- mid = 100 - 700 Hz
- highmid = 700 - 2000 Hz
- uppermid = 2000 - 4000 Hz
- presence = 4000 - 6000 Hz
- brillance > 6000 Hz
:return : A dictionary with the divided signal as values and bin names as keys
"""
bins = self.SP.bins.__dict__
bass_filter = sig.butter(12, bins["bass"].value, 'lp', fs=self.sr, output='sos')
mid_filter = sig.butter(12, [bins["bass"].value, bins['mid'].value], 'bp', fs=self.sr, output='sos')
himid_filter = sig.butter(12, [bins["mid"].value, bins['highmid'].value], 'bp', fs=self.sr, output='sos')
upmid_filter = sig.butter(12, [bins["highmid"].value, bins['uppermid'].value], 'bp', fs=self.sr, output='sos')
pres_filter = sig.butter(12, [bins["uppermid"].value, bins['presence'].value], 'bp', fs=self.sr, output='sos')
bril_filter = sig.butter(12, bins['presence'].value, 'hp', fs=self.sr, output='sos')
return {
"bass": Signal(sig.sosfilt(bass_filter, self.signal), self.sr, self.SP,
freq_range=[0, bins["bass"].value]),
"mid": Signal(sig.sosfilt(mid_filter, self.signal), self.sr, self.SP,
freq_range=[bins["bass"].value, bins["mid"].value]),
"highmid": Signal(sig.sosfilt(himid_filter, self.signal), self.sr, self.SP,
freq_range=[bins["mid"].value, bins["highmid"].value]),
"uppermid": Signal(sig.sosfilt(upmid_filter, self.signal), self.sr, self.SP,
freq_range=[bins["highmid"].value, bins["uppermid"].value]),
"presence": Signal(sig.sosfilt(pres_filter, self.signal), self.sr, self.SP,
freq_range=[bins['uppermid'].value, bins["presence"].value]),
"brillance": Signal(sig.sosfilt(bril_filter, self.signal), self.sr, self.SP,
freq_range=[bins["presence"].value, max(self.fft_frequencies())])}
def timbre(self):
"""
A method computing the timbral attributes of the signal
This method returns timbral attributes "Brightness", "Depth", "Boominess", "Sharpness" and "Warmth".
They are obtained trough linear regression with a model trained with regular sounds.
More information :
Andy Pearce, Mark Plumbley, Saeid, S., Brookes, T., Mason, R., & Wang, W. (2019).
Release of timbral characterisation tools for semantically annotating non-musical content.pdf
(Rapport No. AC-WP5-SURREY-D5.8). AudioCommons. Repéré à :
https://www.audiocommons.org/assets/files/AC-WP5-SURREY-D5.8%20Release%20of%20timbral
%20characterisation%20tools%20for%20semantically%20annotating%20non-musical%20content.pdf
:return: A dictionary with timbral attributes and their values
"""
# Save the signal in a temporary file
self.save_wav('temp')
# Compute the timbre dict from the temp file
timbre = timbral_extractor('temp.wav', verbose=False)
# remove reverb and roughness and hardness attributes
timbre = {key: timbre[key] for key in timbre if key not in ['reverb', 'roughness', 'hardness']}
# Remove the temp file
os.remove('temp.wav')
return timbre
def save_wav(self, name, path=''):
"""
Create a soundfile from a signal
:param name: the name of the saved file
:param path: the path were the '.wav' file is saved
"""
write(path + name + ".wav", self.signal, self.sr)
class Plot(object):
"""
A class to handle all the plotting functions of the Signal and to allow a nice call signature :
Signal.plot.envelop()
Supported plots are :
'signal', 'envelop', 'log envelop', 'fft', 'fft hist', 'peaks', 'peak damping', 'time damping',
'timbre', 'integral
"""
# Illegal plot key word arguments
illegal_kwargs = ['max_time', 'n', 'ticks', 'normalize', 'inverse', 'peak_height', 'fill']
def __init__(self):
# define the parent attribute
self.parent = None
# dictonary with methods and key words
self.method_dict = {'signal': self.signal,
'envelop': self.envelop,
'log envelop': self.log_envelop,
'fft': self.fft,
'fft hist': self.fft_hist,
'peaks': self.peaks,
'peak damping': self.peak_damping,
'time damping': self.time_damping,
'timbre': self.timbre,
'integral': self.integral, }
def sanitize_kwargs(self, kwargs):
"""
Remove illegal key words to supply the key word arguments to matplotlib
:param kwargs:
:return: sanitized kwargs
"""
return {i: kwargs[i] for i in kwargs if i not in self.illegal_kwargs}
def set_bin_ticks(self):
"""
Applies the frequency bin ticks to the current plot
:param kwargs:
:return:
"""
labels = [label for label in self.parent.SP.bins.__dict__ if label != 'name']
labels.append('brillance')
x = [param.value for param in self.parent.SP.bins.__dict__.values() if param != 'bins']
x.append(11250)
x_formatter = ticker.FixedFormatter(labels)
x_locator = ticker.FixedLocator(x)
ax = plt.gca()
ax.xaxis.set_major_locator(x_locator)
ax.xaxis.set_major_formatter(x_formatter)
ax.tick_params(axis="x", labelrotation=90)
def signal(self, **kwargs):
"""
Plots the time varying real signal as amplitude vs time.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
plt.plot(self.parent.time(), self.parent.signal, alpha=0.6, **plot_kwargs)
plt.xlabel('time (s)')
plt.ylabel('amplitude')
plt.grid('on')
def envelop(self, **kwargs):
"""
Plots the envelop of the signal as amplitude vs time.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
plt.plot(self.parent.envelop_time(), self.parent.envelop(), **plot_kwargs)
plt.xlabel("time (s)")
plt.ylabel("amplitude")
plt.grid('on')
def log_envelop(self, **kwargs):
"""
Plots the signal envelop with logarithmic window widths on a logarithmic x axis scale.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
log_envelop, log_envelop_time = self.parent.log_envelop()
if ('max_time' in kwargs.keys()) and (kwargs['max_time'] < log_envelop_time[-1]):
max_index = np.nonzero(log_envelop_time >= kwargs['max_time'])[0][0]
else:
max_index = len(log_envelop_time)
plt.plot(log_envelop_time[:max_index], log_envelop[:max_index], **plot_kwargs)
plt.xlabel("time (s)")
plt.ylabel("amplitude")
plt.xscale('log')
plt.grid('on')
def fft(self, **kwargs):
"""
Plots the Fourier Transform of the Signal.
If `ticks = 'bins'` is supplied in the keyword arguments, the frequency ticks are replaced
with the frequency bin values.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
# find the index corresponding to the fft range
result = np.where(self.parent.fft_frequencies() >= self.parent.SP.general.fft_range.value)[0]
if len(result) == 0:
last_index = -1
else:
last_index = result[0]
plt.plot(self.parent.fft_frequencies()[:last_index], self.parent.fft()[:last_index], **plot_kwargs)
plt.xlabel("frequency"),
plt.ylabel("amplitude"),
plt.yscale('log')
plt.grid('on')
if 'ticks' in kwargs and kwargs['ticks'] == 'bins':
self.set_bin_ticks()
def fft_hist(self, **kwargs):
"""
Plots the octave based Fourier Transform Histogram.
Both axes are on a log scale.
If `ticks = 'bins'` is supplied in the keyword arguments, the frequency ticks are replaced
with the frequency bin values
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
# Histogram of frequency values occurences in octave bins
plt.hist(self.parent.fft_bins(), utils.octave_histogram(self.parent.SP.general.octave_fraction.value),
alpha=0.7, **plot_kwargs)
plt.xlabel('Fréquence (Hz)')
plt.ylabel('Amplitude')
plt.xscale('log')
plt.yscale('log')
plt.grid('on')
if 'ticks' in kwargs and kwargs['ticks'] == 'bins':
self.set_bin_ticks()
def peaks(self, **kwargs):
"""
Plots the Fourier Transform of the Signal, with the peaks detected with the `Signal.peaks()` method.
If `peak_height = True` is supplied in the keyword arguments the computed height threshold is
shown on the plot.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
fft_freqs = self.parent.fft_frequencies()
fft = self.parent.fft()
max_index = np.where(fft_freqs >= self.parent.SP.general.fft_range.value)[0][0]
peak_indexes, height = self.parent.peaks(height=True)
plt.xlabel('Fréquence (Hz)')
plt.ylabel('Amplitude')
plt.yscale('log')
plt.grid('on')
if 'color' not in plot_kwargs.keys():
plot_kwargs['color'] = 'k'
plt.plot(fft_freqs[:max_index], fft[:max_index], **plot_kwargs)
plt.scatter(fft_freqs[peak_indexes], fft[peak_indexes], color='r')
if ('peak_height' in kwargs.keys()) and (kwargs['peak_height']):
plt.plot(fft_freqs[:max_index], height, color='r')
def peak_damping(self, **kwargs):
"""
Plots the frequency vs damping scatter of the damping ratio computed from the
Fourier Transform peak shapes. A polynomial curve fit is added to help visualisation.
Supported key word arguments are :
`n=5` : The order of the fitted polynomial curve, default is 5,
if the supplied value is too high, it will be reduced until the number of peaks
is sufficient to fit the polynomial.
`inverse=True` : Default value is True, if False, the damping ratio is shown instead
of its inverse.
`normalize=False` : Default value is False, if True the damping values are normalized
from 0 to 1, to help analyze results and compare Sounds.
`ticks=None` : Default value is None, if `ticks='bins'` the x axis ticks are replaced with
frequency bin values.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
# Get the damping ratio and peak frequencies
if 'inverse' in kwargs.keys() and kwargs['inverse'] is False:
zetas = np.array(self.parent.peak_damping())
ylabel = r'Damping $\zeta$'
else:
zetas = 1 / np.array(self.parent.peak_damping())
ylabel = r'Inverse Damping $1/\zeta$'
peak_freqs = self.parent.fft_frequencies()[self.parent.peaks()]
# If a polynomial order is supplied assign it, if not default is 5
if 'n' in kwargs.keys():
n = kwargs['n']
else:
n = 5
# If labels are supplied the default color are used
if 'label' in plot_kwargs:
plot_kwargs['color'] = None
plot2_kwargs = plot_kwargs.copy()
plot2_kwargs['label'] = None
# If not black and red are used
else:
plot_kwargs['color'] = 'r'
plot2_kwargs = plot_kwargs.copy()
plot2_kwargs['color'] = 'k'
if 'normalize' in kwargs.keys() and kwargs['normalize']:
zetas = np.array(zetas) / np.array(zetas).max()
plt.scatter(peak_freqs, zetas, **plot_kwargs)
fun = utils.nth_order_polynomial_fit(n, peak_freqs, zetas)
freq = np.linspace(peak_freqs[0], peak_freqs[-1], 100)
plt.plot(freq, fun(freq), **plot2_kwargs)
plt.grid('on')
plt.title('Frequency vs Damping Factor with Order ' + str(n))
plt.xlabel('Frequency (Hz)')
plt.ylabel(ylabel)
if 'ticks' in kwargs and kwargs['ticks'] == 'bins':
self.set_bin_ticks()
def time_damping(self, **kwargs):
"""
Shows the signal envelop with the fitted negative exponential curve used to determine the
time damping ratio of the signal.
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
# Get the envelop data
envelop_time = self.parent.normalize().envelop_time()
envelop = self.parent.normalize().envelop()
# First point is the maximum because e^-kt is stricly decreasing
first_index = np.argmax(envelop)
# The second point is the first point where the signal crosses the lower_threshold line
second_point_thresh = self.parent.SP.damping.lower_threshold.value
try:
second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh)[0]
except IndexError:
second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh * 2)[0]
# Function to compute the residual for the exponential curve fit
def residual_function(zeta_w, t, s):
return np.exp(zeta_w[0] * t) - s
zeta_guess = [-0.5]
result = scipy.optimize.least_squares(residual_function, zeta_guess,
args=(envelop_time[first_index:second_index],
envelop[first_index:second_index]))
# Get the zeta*omega constant
zeta_omega = result.x[0]
# Compute the fundamental frequency in radiants of the signal
wd = 2 * np.pi * self.parent.fundamental()
# Plot the two points used for the regression
plt.scatter(envelop_time[[first_index, second_index]], envelop[[first_index, second_index]], color='r')
# get the current ax
ax = plt.gca()
# Plot the damping curve
ax.plot(envelop_time[first_index:second_index],
np.exp(zeta_omega * envelop_time[first_index:second_index]), c='k')
plt.sca(ax)
self.parent.normalize().plot.envelop(**plot_kwargs)
if 'label' not in plot_kwargs.keys():
ax.legend(['damping curve', 'signal envelop'])
title = 'Zeta : ' + str(np.around(-zeta_omega / wd, 5)) + ' Fundamental ' + \
str(np.around(self.parent.fundamental(), 0)) + 'Hz'
plt.title(title)
def timbre(self, **kwargs):
"""
A polar plot of the timbral attributes of the signal
See help(guitarsounds.analysis.Signal.timbre) for more info about the timbral attributes
"""
plot_kwargs = self.sanitize_kwargs(kwargs)
fig = plt.gcf()
if not fig.axes: # case when the figure is empty
ax = fig.add_subplot(projection='polar')
elif plt.gca().name == 'polar': # if the current ax is polar
ax = plt.gca()
timbre = self.parent.timbre() # compute timbral attributes
categories = list(timbre.keys()) # get the timbral categories
values = list(timbre.values()) # get the timbral values
N = len(values) # Number of values
values += values[:1] # append the first value to the end to close the loop
angles = [n / float(N) * 2 * np.pi for n in range(N)] # N equidistant angles
angles += angles[:1] # append the first value at the end
ax.plot(angles, values, lw=2, **plot_kwargs)
if 'fill' in kwargs and kwargs['fill']:
ax.fill(angles, values)
ax.set_yticks([])
ax.set_yticklabels([])
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories)
ax.xaxis.set_tick_params(pad=18)
ax.set_theta_zero_location('S')
def integral(self, **kwargs):
"""
Cumulative integral plot of the normalized signal log envelop
Represents the power distribution variation in time for the signal.
This is a plot of the function $F(x)$ such as :
$ F(x) = \int_0^x e(x) dx $
Where e(x) is the signal envelop.
"""
# sanitize the kwargs
plot_kwargs = self.sanitize_kwargs(kwargs)
# Compute log envelop and log time
log_envelop, log_time = self.parent.normalize().log_envelop()
# define integrating function
integ = scipy.integrate.trapezoid
# compute the cumulative integral
integral = [integ(log_envelop[:i], log_time[:i]) for i in np.arange(2, len(log_envelop), 1)]
# plot the integral
plt.plot(log_time[2:], integral, **plot_kwargs)
# Add labels and scale
plt.xlabel('time (s)')
plt.ylabel('cummulative power')
plt.xscale('log')
plt.grid('on')
Global variables
var SP
-
Classes
Classes
class Plot
-
A class to handle all the plotting functions of the Signal and to allow a nice call signature : Signal.plot.envelop()
Supported plots are : 'signal', 'envelop', 'log envelop', 'fft', 'fft hist', 'peaks', 'peak damping', 'time damping', 'timbre', 'integral
Expand source code
class Plot(object): """ A class to handle all the plotting functions of the Signal and to allow a nice call signature : Signal.plot.envelop() Supported plots are : 'signal', 'envelop', 'log envelop', 'fft', 'fft hist', 'peaks', 'peak damping', 'time damping', 'timbre', 'integral """ # Illegal plot key word arguments illegal_kwargs = ['max_time', 'n', 'ticks', 'normalize', 'inverse', 'peak_height', 'fill'] def __init__(self): # define the parent attribute self.parent = None # dictonary with methods and key words self.method_dict = {'signal': self.signal, 'envelop': self.envelop, 'log envelop': self.log_envelop, 'fft': self.fft, 'fft hist': self.fft_hist, 'peaks': self.peaks, 'peak damping': self.peak_damping, 'time damping': self.time_damping, 'timbre': self.timbre, 'integral': self.integral, } def sanitize_kwargs(self, kwargs): """ Remove illegal key words to supply the key word arguments to matplotlib :param kwargs: :return: sanitized kwargs """ return {i: kwargs[i] for i in kwargs if i not in self.illegal_kwargs} def set_bin_ticks(self): """ Applies the frequency bin ticks to the current plot :param kwargs: :return: """ labels = [label for label in self.parent.SP.bins.__dict__ if label != 'name'] labels.append('brillance') x = [param.value for param in self.parent.SP.bins.__dict__.values() if param != 'bins'] x.append(11250) x_formatter = ticker.FixedFormatter(labels) x_locator = ticker.FixedLocator(x) ax = plt.gca() ax.xaxis.set_major_locator(x_locator) ax.xaxis.set_major_formatter(x_formatter) ax.tick_params(axis="x", labelrotation=90) def signal(self, **kwargs): """ Plots the time varying real signal as amplitude vs time. """ plot_kwargs = self.sanitize_kwargs(kwargs) plt.plot(self.parent.time(), self.parent.signal, alpha=0.6, **plot_kwargs) plt.xlabel('time (s)') plt.ylabel('amplitude') plt.grid('on') def envelop(self, **kwargs): """ Plots the envelop of the signal as amplitude vs time. """ plot_kwargs = self.sanitize_kwargs(kwargs) plt.plot(self.parent.envelop_time(), self.parent.envelop(), **plot_kwargs) plt.xlabel("time (s)") plt.ylabel("amplitude") plt.grid('on') def log_envelop(self, **kwargs): """ Plots the signal envelop with logarithmic window widths on a logarithmic x axis scale. """ plot_kwargs = self.sanitize_kwargs(kwargs) log_envelop, log_envelop_time = self.parent.log_envelop() if ('max_time' in kwargs.keys()) and (kwargs['max_time'] < log_envelop_time[-1]): max_index = np.nonzero(log_envelop_time >= kwargs['max_time'])[0][0] else: max_index = len(log_envelop_time) plt.plot(log_envelop_time[:max_index], log_envelop[:max_index], **plot_kwargs) plt.xlabel("time (s)") plt.ylabel("amplitude") plt.xscale('log') plt.grid('on') def fft(self, **kwargs): """ Plots the Fourier Transform of the Signal. If `ticks = 'bins'` is supplied in the keyword arguments, the frequency ticks are replaced with the frequency bin values. """ plot_kwargs = self.sanitize_kwargs(kwargs) # find the index corresponding to the fft range result = np.where(self.parent.fft_frequencies() >= self.parent.SP.general.fft_range.value)[0] if len(result) == 0: last_index = -1 else: last_index = result[0] plt.plot(self.parent.fft_frequencies()[:last_index], self.parent.fft()[:last_index], **plot_kwargs) plt.xlabel("frequency"), plt.ylabel("amplitude"), plt.yscale('log') plt.grid('on') if 'ticks' in kwargs and kwargs['ticks'] == 'bins': self.set_bin_ticks() def fft_hist(self, **kwargs): """ Plots the octave based Fourier Transform Histogram. Both axes are on a log scale. If `ticks = 'bins'` is supplied in the keyword arguments, the frequency ticks are replaced with the frequency bin values """ plot_kwargs = self.sanitize_kwargs(kwargs) # Histogram of frequency values occurences in octave bins plt.hist(self.parent.fft_bins(), utils.octave_histogram(self.parent.SP.general.octave_fraction.value), alpha=0.7, **plot_kwargs) plt.xlabel('Fréquence (Hz)') plt.ylabel('Amplitude') plt.xscale('log') plt.yscale('log') plt.grid('on') if 'ticks' in kwargs and kwargs['ticks'] == 'bins': self.set_bin_ticks() def peaks(self, **kwargs): """ Plots the Fourier Transform of the Signal, with the peaks detected with the `Signal.peaks()` method. If `peak_height = True` is supplied in the keyword arguments the computed height threshold is shown on the plot. """ plot_kwargs = self.sanitize_kwargs(kwargs) fft_freqs = self.parent.fft_frequencies() fft = self.parent.fft() max_index = np.where(fft_freqs >= self.parent.SP.general.fft_range.value)[0][0] peak_indexes, height = self.parent.peaks(height=True) plt.xlabel('Fréquence (Hz)') plt.ylabel('Amplitude') plt.yscale('log') plt.grid('on') if 'color' not in plot_kwargs.keys(): plot_kwargs['color'] = 'k' plt.plot(fft_freqs[:max_index], fft[:max_index], **plot_kwargs) plt.scatter(fft_freqs[peak_indexes], fft[peak_indexes], color='r') if ('peak_height' in kwargs.keys()) and (kwargs['peak_height']): plt.plot(fft_freqs[:max_index], height, color='r') def peak_damping(self, **kwargs): """ Plots the frequency vs damping scatter of the damping ratio computed from the Fourier Transform peak shapes. A polynomial curve fit is added to help visualisation. Supported key word arguments are : `n=5` : The order of the fitted polynomial curve, default is 5, if the supplied value is too high, it will be reduced until the number of peaks is sufficient to fit the polynomial. `inverse=True` : Default value is True, if False, the damping ratio is shown instead of its inverse. `normalize=False` : Default value is False, if True the damping values are normalized from 0 to 1, to help analyze results and compare Sounds. `ticks=None` : Default value is None, if `ticks='bins'` the x axis ticks are replaced with frequency bin values. """ plot_kwargs = self.sanitize_kwargs(kwargs) # Get the damping ratio and peak frequencies if 'inverse' in kwargs.keys() and kwargs['inverse'] is False: zetas = np.array(self.parent.peak_damping()) ylabel = r'Damping $\zeta$' else: zetas = 1 / np.array(self.parent.peak_damping()) ylabel = r'Inverse Damping $1/\zeta$' peak_freqs = self.parent.fft_frequencies()[self.parent.peaks()] # If a polynomial order is supplied assign it, if not default is 5 if 'n' in kwargs.keys(): n = kwargs['n'] else: n = 5 # If labels are supplied the default color are used if 'label' in plot_kwargs: plot_kwargs['color'] = None plot2_kwargs = plot_kwargs.copy() plot2_kwargs['label'] = None # If not black and red are used else: plot_kwargs['color'] = 'r' plot2_kwargs = plot_kwargs.copy() plot2_kwargs['color'] = 'k' if 'normalize' in kwargs.keys() and kwargs['normalize']: zetas = np.array(zetas) / np.array(zetas).max() plt.scatter(peak_freqs, zetas, **plot_kwargs) fun = utils.nth_order_polynomial_fit(n, peak_freqs, zetas) freq = np.linspace(peak_freqs[0], peak_freqs[-1], 100) plt.plot(freq, fun(freq), **plot2_kwargs) plt.grid('on') plt.title('Frequency vs Damping Factor with Order ' + str(n)) plt.xlabel('Frequency (Hz)') plt.ylabel(ylabel) if 'ticks' in kwargs and kwargs['ticks'] == 'bins': self.set_bin_ticks() def time_damping(self, **kwargs): """ Shows the signal envelop with the fitted negative exponential curve used to determine the time damping ratio of the signal. """ plot_kwargs = self.sanitize_kwargs(kwargs) # Get the envelop data envelop_time = self.parent.normalize().envelop_time() envelop = self.parent.normalize().envelop() # First point is the maximum because e^-kt is stricly decreasing first_index = np.argmax(envelop) # The second point is the first point where the signal crosses the lower_threshold line second_point_thresh = self.parent.SP.damping.lower_threshold.value try: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh)[0] except IndexError: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh * 2)[0] # Function to compute the residual for the exponential curve fit def residual_function(zeta_w, t, s): return np.exp(zeta_w[0] * t) - s zeta_guess = [-0.5] result = scipy.optimize.least_squares(residual_function, zeta_guess, args=(envelop_time[first_index:second_index], envelop[first_index:second_index])) # Get the zeta*omega constant zeta_omega = result.x[0] # Compute the fundamental frequency in radiants of the signal wd = 2 * np.pi * self.parent.fundamental() # Plot the two points used for the regression plt.scatter(envelop_time[[first_index, second_index]], envelop[[first_index, second_index]], color='r') # get the current ax ax = plt.gca() # Plot the damping curve ax.plot(envelop_time[first_index:second_index], np.exp(zeta_omega * envelop_time[first_index:second_index]), c='k') plt.sca(ax) self.parent.normalize().plot.envelop(**plot_kwargs) if 'label' not in plot_kwargs.keys(): ax.legend(['damping curve', 'signal envelop']) title = 'Zeta : ' + str(np.around(-zeta_omega / wd, 5)) + ' Fundamental ' + \ str(np.around(self.parent.fundamental(), 0)) + 'Hz' plt.title(title) def timbre(self, **kwargs): """ A polar plot of the timbral attributes of the signal See help(guitarsounds.analysis.Signal.timbre) for more info about the timbral attributes """ plot_kwargs = self.sanitize_kwargs(kwargs) fig = plt.gcf() if not fig.axes: # case when the figure is empty ax = fig.add_subplot(projection='polar') elif plt.gca().name == 'polar': # if the current ax is polar ax = plt.gca() timbre = self.parent.timbre() # compute timbral attributes categories = list(timbre.keys()) # get the timbral categories values = list(timbre.values()) # get the timbral values N = len(values) # Number of values values += values[:1] # append the first value to the end to close the loop angles = [n / float(N) * 2 * np.pi for n in range(N)] # N equidistant angles angles += angles[:1] # append the first value at the end ax.plot(angles, values, lw=2, **plot_kwargs) if 'fill' in kwargs and kwargs['fill']: ax.fill(angles, values) ax.set_yticks([]) ax.set_yticklabels([]) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) ax.xaxis.set_tick_params(pad=18) ax.set_theta_zero_location('S') def integral(self, **kwargs): """ Cumulative integral plot of the normalized signal log envelop Represents the power distribution variation in time for the signal. This is a plot of the function $F(x)$ such as : $ F(x) = \int_0^x e(x) dx $ Where e(x) is the signal envelop. """ # sanitize the kwargs plot_kwargs = self.sanitize_kwargs(kwargs) # Compute log envelop and log time log_envelop, log_time = self.parent.normalize().log_envelop() # define integrating function integ = scipy.integrate.trapezoid # compute the cumulative integral integral = [integ(log_envelop[:i], log_time[:i]) for i in np.arange(2, len(log_envelop), 1)] # plot the integral plt.plot(log_time[2:], integral, **plot_kwargs) # Add labels and scale plt.xlabel('time (s)') plt.ylabel('cummulative power') plt.xscale('log') plt.grid('on')
Class variables
var illegal_kwargs
Methods
def envelop(self, **kwargs)
-
Plots the envelop of the signal as amplitude vs time.
Expand source code
def envelop(self, **kwargs): """ Plots the envelop of the signal as amplitude vs time. """ plot_kwargs = self.sanitize_kwargs(kwargs) plt.plot(self.parent.envelop_time(), self.parent.envelop(), **plot_kwargs) plt.xlabel("time (s)") plt.ylabel("amplitude") plt.grid('on')
def fft(self, **kwargs)
-
Plots the Fourier Transform of the Signal.
If
ticks = 'bins'
is supplied in the keyword arguments, the frequency ticks are replaced with the frequency bin values.Expand source code
def fft(self, **kwargs): """ Plots the Fourier Transform of the Signal. If `ticks = 'bins'` is supplied in the keyword arguments, the frequency ticks are replaced with the frequency bin values. """ plot_kwargs = self.sanitize_kwargs(kwargs) # find the index corresponding to the fft range result = np.where(self.parent.fft_frequencies() >= self.parent.SP.general.fft_range.value)[0] if len(result) == 0: last_index = -1 else: last_index = result[0] plt.plot(self.parent.fft_frequencies()[:last_index], self.parent.fft()[:last_index], **plot_kwargs) plt.xlabel("frequency"), plt.ylabel("amplitude"), plt.yscale('log') plt.grid('on') if 'ticks' in kwargs and kwargs['ticks'] == 'bins': self.set_bin_ticks()
def fft_hist(self, **kwargs)
-
Plots the octave based Fourier Transform Histogram. Both axes are on a log scale.
If
ticks = 'bins'
is supplied in the keyword arguments, the frequency ticks are replaced with the frequency bin valuesExpand source code
def fft_hist(self, **kwargs): """ Plots the octave based Fourier Transform Histogram. Both axes are on a log scale. If `ticks = 'bins'` is supplied in the keyword arguments, the frequency ticks are replaced with the frequency bin values """ plot_kwargs = self.sanitize_kwargs(kwargs) # Histogram of frequency values occurences in octave bins plt.hist(self.parent.fft_bins(), utils.octave_histogram(self.parent.SP.general.octave_fraction.value), alpha=0.7, **plot_kwargs) plt.xlabel('Fréquence (Hz)') plt.ylabel('Amplitude') plt.xscale('log') plt.yscale('log') plt.grid('on') if 'ticks' in kwargs and kwargs['ticks'] == 'bins': self.set_bin_ticks()
def integral(self, **kwargs)
-
Cumulative integral plot of the normalized signal log envelop
Represents the power distribution variation in time for the signal. This is a plot of the function $F(x)$ such as :
$ F(x) = \int_0^x e(x) dx $
Where e(x) is the signal envelop.
Expand source code
def integral(self, **kwargs): """ Cumulative integral plot of the normalized signal log envelop Represents the power distribution variation in time for the signal. This is a plot of the function $F(x)$ such as : $ F(x) = \int_0^x e(x) dx $ Where e(x) is the signal envelop. """ # sanitize the kwargs plot_kwargs = self.sanitize_kwargs(kwargs) # Compute log envelop and log time log_envelop, log_time = self.parent.normalize().log_envelop() # define integrating function integ = scipy.integrate.trapezoid # compute the cumulative integral integral = [integ(log_envelop[:i], log_time[:i]) for i in np.arange(2, len(log_envelop), 1)] # plot the integral plt.plot(log_time[2:], integral, **plot_kwargs) # Add labels and scale plt.xlabel('time (s)') plt.ylabel('cummulative power') plt.xscale('log') plt.grid('on')
def log_envelop(self, **kwargs)
-
Plots the signal envelop with logarithmic window widths on a logarithmic x axis scale.
Expand source code
def log_envelop(self, **kwargs): """ Plots the signal envelop with logarithmic window widths on a logarithmic x axis scale. """ plot_kwargs = self.sanitize_kwargs(kwargs) log_envelop, log_envelop_time = self.parent.log_envelop() if ('max_time' in kwargs.keys()) and (kwargs['max_time'] < log_envelop_time[-1]): max_index = np.nonzero(log_envelop_time >= kwargs['max_time'])[0][0] else: max_index = len(log_envelop_time) plt.plot(log_envelop_time[:max_index], log_envelop[:max_index], **plot_kwargs) plt.xlabel("time (s)") plt.ylabel("amplitude") plt.xscale('log') plt.grid('on')
def peak_damping(self, **kwargs)
-
Plots the frequency vs damping scatter of the damping ratio computed from the Fourier Transform peak shapes. A polynomial curve fit is added to help visualisation.
Supported key word arguments are :
n=5
: The order of the fitted polynomial curve, default is 5, if the supplied value is too high, it will be reduced until the number of peaks is sufficient to fit the polynomial.inverse=True
: Default value is True, if False, the damping ratio is shown instead of its inverse.normalize=False
: Default value is False, if True the damping values are normalized from 0 to 1, to help analyze results and compare Sounds.ticks=None
: Default value is None, ifticks='bins'
the x axis ticks are replaced with frequency bin values.Expand source code
def peak_damping(self, **kwargs): """ Plots the frequency vs damping scatter of the damping ratio computed from the Fourier Transform peak shapes. A polynomial curve fit is added to help visualisation. Supported key word arguments are : `n=5` : The order of the fitted polynomial curve, default is 5, if the supplied value is too high, it will be reduced until the number of peaks is sufficient to fit the polynomial. `inverse=True` : Default value is True, if False, the damping ratio is shown instead of its inverse. `normalize=False` : Default value is False, if True the damping values are normalized from 0 to 1, to help analyze results and compare Sounds. `ticks=None` : Default value is None, if `ticks='bins'` the x axis ticks are replaced with frequency bin values. """ plot_kwargs = self.sanitize_kwargs(kwargs) # Get the damping ratio and peak frequencies if 'inverse' in kwargs.keys() and kwargs['inverse'] is False: zetas = np.array(self.parent.peak_damping()) ylabel = r'Damping $\zeta$' else: zetas = 1 / np.array(self.parent.peak_damping()) ylabel = r'Inverse Damping $1/\zeta$' peak_freqs = self.parent.fft_frequencies()[self.parent.peaks()] # If a polynomial order is supplied assign it, if not default is 5 if 'n' in kwargs.keys(): n = kwargs['n'] else: n = 5 # If labels are supplied the default color are used if 'label' in plot_kwargs: plot_kwargs['color'] = None plot2_kwargs = plot_kwargs.copy() plot2_kwargs['label'] = None # If not black and red are used else: plot_kwargs['color'] = 'r' plot2_kwargs = plot_kwargs.copy() plot2_kwargs['color'] = 'k' if 'normalize' in kwargs.keys() and kwargs['normalize']: zetas = np.array(zetas) / np.array(zetas).max() plt.scatter(peak_freqs, zetas, **plot_kwargs) fun = utils.nth_order_polynomial_fit(n, peak_freqs, zetas) freq = np.linspace(peak_freqs[0], peak_freqs[-1], 100) plt.plot(freq, fun(freq), **plot2_kwargs) plt.grid('on') plt.title('Frequency vs Damping Factor with Order ' + str(n)) plt.xlabel('Frequency (Hz)') plt.ylabel(ylabel) if 'ticks' in kwargs and kwargs['ticks'] == 'bins': self.set_bin_ticks()
def peaks(self, **kwargs)
-
Plots the Fourier Transform of the Signal, with the peaks detected with the
Signal.peaks()
method.If
peak_height = True
is supplied in the keyword arguments the computed height threshold is shown on the plot.Expand source code
def peaks(self, **kwargs): """ Plots the Fourier Transform of the Signal, with the peaks detected with the `Signal.peaks()` method. If `peak_height = True` is supplied in the keyword arguments the computed height threshold is shown on the plot. """ plot_kwargs = self.sanitize_kwargs(kwargs) fft_freqs = self.parent.fft_frequencies() fft = self.parent.fft() max_index = np.where(fft_freqs >= self.parent.SP.general.fft_range.value)[0][0] peak_indexes, height = self.parent.peaks(height=True) plt.xlabel('Fréquence (Hz)') plt.ylabel('Amplitude') plt.yscale('log') plt.grid('on') if 'color' not in plot_kwargs.keys(): plot_kwargs['color'] = 'k' plt.plot(fft_freqs[:max_index], fft[:max_index], **plot_kwargs) plt.scatter(fft_freqs[peak_indexes], fft[peak_indexes], color='r') if ('peak_height' in kwargs.keys()) and (kwargs['peak_height']): plt.plot(fft_freqs[:max_index], height, color='r')
def sanitize_kwargs(self, kwargs)
-
Remove illegal key words to supply the key word arguments to matplotlib :param kwargs: :return: sanitized kwargs
Expand source code
def sanitize_kwargs(self, kwargs): """ Remove illegal key words to supply the key word arguments to matplotlib :param kwargs: :return: sanitized kwargs """ return {i: kwargs[i] for i in kwargs if i not in self.illegal_kwargs}
def set_bin_ticks(self)
-
Applies the frequency bin ticks to the current plot :param kwargs: :return:
Expand source code
def set_bin_ticks(self): """ Applies the frequency bin ticks to the current plot :param kwargs: :return: """ labels = [label for label in self.parent.SP.bins.__dict__ if label != 'name'] labels.append('brillance') x = [param.value for param in self.parent.SP.bins.__dict__.values() if param != 'bins'] x.append(11250) x_formatter = ticker.FixedFormatter(labels) x_locator = ticker.FixedLocator(x) ax = plt.gca() ax.xaxis.set_major_locator(x_locator) ax.xaxis.set_major_formatter(x_formatter) ax.tick_params(axis="x", labelrotation=90)
def signal(self, **kwargs)
-
Plots the time varying real signal as amplitude vs time.
Expand source code
def signal(self, **kwargs): """ Plots the time varying real signal as amplitude vs time. """ plot_kwargs = self.sanitize_kwargs(kwargs) plt.plot(self.parent.time(), self.parent.signal, alpha=0.6, **plot_kwargs) plt.xlabel('time (s)') plt.ylabel('amplitude') plt.grid('on')
def timbre(self, **kwargs)
-
A polar plot of the timbral attributes of the signal
See help(guitarsounds.analysis.Signal.timbre) for more info about the timbral attributes
Expand source code
def timbre(self, **kwargs): """ A polar plot of the timbral attributes of the signal See help(guitarsounds.analysis.Signal.timbre) for more info about the timbral attributes """ plot_kwargs = self.sanitize_kwargs(kwargs) fig = plt.gcf() if not fig.axes: # case when the figure is empty ax = fig.add_subplot(projection='polar') elif plt.gca().name == 'polar': # if the current ax is polar ax = plt.gca() timbre = self.parent.timbre() # compute timbral attributes categories = list(timbre.keys()) # get the timbral categories values = list(timbre.values()) # get the timbral values N = len(values) # Number of values values += values[:1] # append the first value to the end to close the loop angles = [n / float(N) * 2 * np.pi for n in range(N)] # N equidistant angles angles += angles[:1] # append the first value at the end ax.plot(angles, values, lw=2, **plot_kwargs) if 'fill' in kwargs and kwargs['fill']: ax.fill(angles, values) ax.set_yticks([]) ax.set_yticklabels([]) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) ax.xaxis.set_tick_params(pad=18) ax.set_theta_zero_location('S')
def time_damping(self, **kwargs)
-
Shows the signal envelop with the fitted negative exponential curve used to determine the time damping ratio of the signal.
Expand source code
def time_damping(self, **kwargs): """ Shows the signal envelop with the fitted negative exponential curve used to determine the time damping ratio of the signal. """ plot_kwargs = self.sanitize_kwargs(kwargs) # Get the envelop data envelop_time = self.parent.normalize().envelop_time() envelop = self.parent.normalize().envelop() # First point is the maximum because e^-kt is stricly decreasing first_index = np.argmax(envelop) # The second point is the first point where the signal crosses the lower_threshold line second_point_thresh = self.parent.SP.damping.lower_threshold.value try: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh)[0] except IndexError: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh * 2)[0] # Function to compute the residual for the exponential curve fit def residual_function(zeta_w, t, s): return np.exp(zeta_w[0] * t) - s zeta_guess = [-0.5] result = scipy.optimize.least_squares(residual_function, zeta_guess, args=(envelop_time[first_index:second_index], envelop[first_index:second_index])) # Get the zeta*omega constant zeta_omega = result.x[0] # Compute the fundamental frequency in radiants of the signal wd = 2 * np.pi * self.parent.fundamental() # Plot the two points used for the regression plt.scatter(envelop_time[[first_index, second_index]], envelop[[first_index, second_index]], color='r') # get the current ax ax = plt.gca() # Plot the damping curve ax.plot(envelop_time[first_index:second_index], np.exp(zeta_omega * envelop_time[first_index:second_index]), c='k') plt.sca(ax) self.parent.normalize().plot.envelop(**plot_kwargs) if 'label' not in plot_kwargs.keys(): ax.legend(['damping curve', 'signal envelop']) title = 'Zeta : ' + str(np.around(-zeta_omega / wd, 5)) + ' Fundamental ' + \ str(np.around(self.parent.fundamental(), 0)) + 'Hz' plt.title(title)
class Signal (signal, sr, SoundParam, freq_range=None)
-
A Class to do computations on an audio signal.
The signal is never changed in the class, when transformations are made, a new instance is returned.
Create a Signal class from a vector of samples and a sample rate
Expand source code
class Signal(object): """ A Class to do computations on an audio signal. The signal is never changed in the class, when transformations are made, a new instance is returned. """ def __init__(self, signal, sr, SoundParam, freq_range=None): """ Create a Signal class from a vector of samples and a sample rate""" self.SP = SoundParam self.onset = None self.signal = signal self.sr = sr self.range = freq_range self.trimmed = None self.noise = None self.plot = Plot() self.plot.parent = self def time(self): """ Returns the time vector associated to the signal :return: numpy array corresponding to the time values of the signal samples in seconds """ return np.linspace(0, len(self.signal) * (1 / self.sr), len(self.signal)) def listen(self): """ Method to listen the sound signal in a Jupyter Notebook Listening to the sounds imported in the analysis tool allows the user to validate if the sound was well trimmed and filtered A temporary file is created, the IPython display Audio function is called on it and then the file is removed """ file = 'temp.wav' write(file, self.signal, self.sr) ipd.display(ipd.Audio(file)) os.remove(file) def old_plot(self, kind, **kwargs): """ Convenience function for the different signal plots Calls the function corresponding to Plot.kind() See help(guitarsounds.analysis.Plot) for info on the different plots """ self.plot.method_dict[kind](**kwargs) def fft(self): """ Computes the Fast Fourier Transform of the signal and returns the vector. :return: Fast Fourier Transform amplitude values in a numpy array """ fft = np.fft.fft(self.signal) fft = np.abs(fft[:int(len(fft) // 2)]) # Only the symmetric of the absolute value return fft / np.max(fft) def peaks(self, max_freq=None, height=False, result=False): """ Computes the harmonic peaks indexes from the FFT of the signal :param max_freq: Supply a max frequency value overiding the one in guitarsounds_parameters :param height: if True the height threshold is returned to be used in the 'peaks' plot :param result: if True the Scipy peak finding results dictionary is returned :return: peak indexes """ # Replace None by the default value if max_freq is None: max_freq = self.SP.general.fft_range.value # Get the fft and fft frequencies from the signal fft, fft_freq = self.fft(), self.fft_frequencies() # Find the max index max_index = np.where(fft_freq >= max_freq)[0][0] # Find an approximation of the distance between peaks, this only works for harmonic signals peak_distance = np.argmax(fft) // 2 # Maximum of the signal in a small region on both ends fft_max_start = np.max(fft[:peak_distance]) fft_max_end = np.max(fft[max_index - peak_distance:max_index]) # Build the curve below the peaks but above the noise exponents = np.linspace(np.log10(fft_max_start), np.log10(fft_max_end), max_index) intersect = 10 ** exponents[peak_distance] diff_start = fft_max_start - intersect # offset by a small distance so that the first max is not a peak min_height = 10 ** np.linspace(np.log10(fft_max_start + diff_start), np.log10(fft_max_end), max_index) first_peak_indexes, _ = sig.find_peaks(fft[:max_index], height=min_height, distance=peak_distance) number_of_peaks = len(first_peak_indexes) if number_of_peaks > 0: average_len = int(max_index / number_of_peaks) * 3 else: average_len = int(max_index / 3) if average_len % 2 == 0: average_len += 1 average_fft = sig.savgol_filter(fft[:max_index], average_len, 1, mode='mirror') * 1.9 min_freq_index = np.where(fft_freq >= 70)[0][0] average_fft[:min_freq_index] = 1 peak_indexes, res = sig.find_peaks(fft[:max_index], height=average_fft, distance=min_freq_index) # Remove noisy peaks at the low frequencies while fft[peak_indexes[0]] < 5e-2: peak_indexes = np.delete(peak_indexes, 0) while fft[peak_indexes[-1]] < 1e-4: peak_indexes = np.delete(peak_indexes, -1) if not height and not result: return peak_indexes elif height: return peak_indexes, average_fft elif result: return peak_indexes, res elif height and result: return peak_indexes, height, res def time_damping(self): """ Computes the time wise damping ratio of the signal by fitting a negative exponential curve to the Signal envelop and computing the ratio with the Signal fundamental frequency. :return: The damping ratio, a scalar. """ # Get the envelop data envelop_time = self.normalize().envelop_time() envelop = self.normalize().envelop() # First point is the maximum because e^-kt is stricly decreasing first_index = np.argmax(envelop) # The second point is the first point where the signal crosses the lower_threshold line second_point_thresh = self.SP.damping.lower_threshold.value try: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh)[0] except IndexError: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh * 2)[0] # Function to compute the residual for the exponential curve fit def residual_function(zeta_w, t, s): """ Function computing the residual to curve fit a negative exponential to the signal envelop :param zeta_w: zeta*omega constant :param t: time vector :param s: signal :return: residual """ return np.exp(zeta_w[0] * t) - s zeta_guess = [-0.5] result = scipy.optimize.least_squares(residual_function, zeta_guess, args=(envelop_time[first_index:second_index], envelop[first_index:second_index])) # Get the zeta*omega constant zeta_omega = result.x[0] # Compute the fundamental frequency in radiants of the signal wd = 2 * np.pi * self.fundamental() return -zeta_omega / wd def peak_damping(self): """ Computes the frequency wise damping with the half bandwidth method on the Fourier Transform peaks :return: an array containing the peak damping values """ zetas = [] fft_freqs = self.fft_frequencies() fft = self.fft()[:len(fft_freqs)] for peak in self.peaks(): peak_frequency = fft_freqs[peak] peak_height = fft[peak] root_height = peak_height / np.sqrt(2) frequency_roots = scipy.interpolate.InterpolatedUnivariateSpline(fft_freqs, fft - root_height).roots() sorted_roots_indexes = np.argsort(np.abs(frequency_roots - peak_frequency)) w2, w1 = frequency_roots[sorted_roots_indexes[:2]] w1, w2 = np.sort([w1, w2]) zeta = (w2 - w1) / (2 * peak_frequency) zetas.append(zeta) return np.array(zetas) def fundamental(self): """ Returns the fundamental approximated by the first peak of the fft :return: fundamental value (Hz) """ index = self.peaks()[0] fundamental = self.fft_frequencies()[index] return fundamental def cavity_peak(self): """ Finds the Hemlotz cavity frequency index from the Fourier Transform by searching for a peak in the expected range (80 - 100 Hz), if the fundamental is too close to the expected Hemlotz frequency a comment is printed and None is returned. :return: If successful the cavity peak index """ first_index = np.where(self.fft_frequencies() >= 80)[0][0] second_index = np.where(self.fft_frequencies() >= 110)[0][0] cavity_peak = np.argmax(self.fft()[first_index:second_index]) + first_index if self.fundamental() == self.fft_frequencies()[cavity_peak]: print('Cavity peak is obscured by the fundamental') else: return cavity_peak def cavity_frequency(self): """ Finds the hemlotz cavity frequency from the Fourier Transform by searching for a peak in the expected range (80 - 100 Hz), if the fundamental is too close to the expected hemlotz frequency a comment is printed and None is returned. :return: If successful, the cavity peak frequency """ first_index = np.where(self.fft_frequencies() >= 80)[0][0] second_index = np.where(self.fft_frequencies() >= 110)[0][0] cavity_peak = np.argmax(self.fft()[first_index:second_index]) + first_index if self.fundamental() == self.fft_frequencies()[cavity_peak]: print('Cavity peak is obscured by the fundamental') return 0 else: return self.fft_frequencies()[cavity_peak] def fft_frequencies(self): """ Computes the frequency vector associated to the Signal Fourier Transform :return: an array containing the frequency values. """ fft = self.fft() fft_frequencies = np.fft.fftfreq(len(fft) * 2, 1 / self.sr) # Frequencies corresponding to the bins return fft_frequencies[:len(fft)] def fft_bins(self): """ Transforms the Fourier Transform signal into a statistic distribution. Occurences of each frequency varies from 0 to 100 according to their amplitude. :return : a list containing the frequency occurences. """ # Make the FT values integers fft_integers = [int(np.around(sample * 100, 0)) for sample in self.fft()] # Create a list of the frequency occurrences in the signal occurrences = [] for freq, count in zip(self.fft_frequencies(), fft_integers): occurrences.append([freq] * count) # flatten the list return [item for sublist in occurrences for item in sublist] def envelop(self): """ Method calculating the amplitude envelope of a signal as a maximum of the absolute value of the signal. :return: Amplitude envelop of the signal """ # Get the hop length hop_length = self.SP.envelop.hop_length.value # Compute the envelop envelop = np.array( [np.max(np.abs(self.signal[i:i + self.SP.envelop.frame_size.value])) for i in range(0, len(self.signal), hop_length)]) envelop = np.insert(envelop, 0, 0) return envelop def envelop_time(self): """ Method calculating the time vector associated to a signal envelop :return: Time vector associated to the signal envelop """ # Get the number of frames from the signal envelop frames = range(len(self.envelop())) # Return the envelop frames computed with Librosa return librosa.frames_to_time(frames, hop_length=self.SP.envelop.hop_length.value) def log_envelop(self): """ Computes the logarithmic scale envelop of the signal. The width of the samples increases exponentially so that the envelop appears having a constant window width on an X axis logarithmic scale. :return: The log envelop and the time vector associated in a tuple """ if self.onset is None: onset = np.argmax(self.signal) else: onset = self.onset start_time = self.SP.log_envelop.start_time.value while start_time > (onset / self.sr): start_time /= 10. start_exponent = int(np.log10(start_time)) # closest 10^x value for smooth graph if self.SP.log_envelop.min_window.value is None: min_window = 15 ** (start_exponent + 4) if min_window < 15: # Value should at least be 10 min_window = 15 else: min_window = self.SP.log_envelop.min_window.value # initial values current_exponent = start_exponent current_time = 10 ** current_exponent # start time on log scale index = int(current_time * self.sr) # Start at the specified time window = min_window # number of samples per window overlap = window // 2 log_envelop = [] log_envelop_time = [0] # First value for comparison while index + window <= len(self.signal): while log_envelop_time[-1] < 10 ** (current_exponent + 1): if (index + window) < len(self.signal): log_envelop.append(np.max(self.signal[index:index + window])) log_envelop_time.append(self.time()[index]) index += overlap else: break if window * 10 < self.SP.log_envelop.max_window.value: window = window * 10 else: window = self.SP.log_envelop.max_window.value overlap = window // 2 current_exponent += 1 # remove the value where t=0 so the log scale does not break log_envelop_time.remove(0) return np.array(log_envelop), np.array(log_envelop_time) def find_onset(self, verbose=True): """ Finds the onset as an increase in more of 50% with the maximum normalized value above 0.5 :param verbose: Prints a warning if the algorithm does not converge :return: the index of the onset in the signal """ # Index corresponding to the onset time interval window_index = np.ceil(self.SP.onset.onset_time.value * self.sr).astype(int) # Use the normalized signal to compare against a fixed value onset_signal = self.normalize() overlap = window_index // 2 # overlap for algorithm progression # Initial values increase = 0 i = 0 broke = False while increase <= 0.5: signal_min = np.min(np.abs(onset_signal.signal[i:i + window_index])) signal_max = np.max(np.abs(onset_signal.signal[i:i + window_index])) if (signal_max > 0.5) and (signal_min != 0): increase = signal_max / signal_min else: increase = 0 i += overlap if i + window_index > len(self.signal): if verbose: print('Onset detection did not converge \n') print('Approximating onset with signal max value \n') broke = True break if broke: return np.argmax(self.signal) else: return np.argmax(np.abs(self.signal[i:i + window_index])) + i def trim_onset(self, verbose=True): """ Trim the signal at the onset (max) minus the delay in milliseconds as Specified in the SoundParameters :param : verbose if False the warning comments are not displayed :return : a trimmed signal with a noise attribute """ # nb of samples to keep before the onset delay_samples = int((self.SP.onset.onset_delay.value / 1000) * self.sr) onset = self.find_onset(verbose=verbose) # find the onset if onset > delay_samples: # To make sure the index is positive trimmed_signal = Signal(self.signal[onset - delay_samples:], self.sr, self.SP) trimmed_signal.noise = self.signal[:onset - delay_samples] trimmed_signal.trimmed = True trimmed_signal.onset = np.argmax(trimmed_signal.signal) return trimmed_signal else: if verbose: print('Signal is too short to be trimmed before onset.') print('') self.trimmed = False return self def trim_time(self, time_length): """ Trims the signal to the specified length and returns a new Signal instance. :param time_length: desired length of the new signal in seconds. :return: A trimmed Signal """ max_index = int(time_length * self.sr) time_trimmed_signal = Signal(self.signal[:max_index], self.sr, self.SP) time_trimmed_signal.time_length = time_length return time_trimmed_signal def filter_noise(self, verbose=True): """ Method filtering the noise from the recorded signal and returning a filtered signal. If the signal was not trimmed it is trimmed in place then filtered. If the signal can not be trimmed it can't be filtered and the original signal is returned :return : A Signal instance, filtered if possible. """ try: return Signal(reduce_noise(audio_clip=self.signal, noise_clip=self.noise), self.sr, self.SP) except AttributeError: if self.trimmed is False: if verbose: print('Not sufficient noise in the raw signal, unable to filter.') print('') return self def normalize(self): """ Normalizes the signal to [-1, 1] and returns the normalised instance. :return : A normalized signal """ factor = np.max(np.abs(self.signal)) normalised_signal = Signal((self.signal / factor), self.sr, self.SP) normalised_signal.norm_factor = (1 / factor) return normalised_signal def make_freq_bins(self): """ Method to divide a signal in frequency bins using butterworth filters bins are passed as a dict, default values are : - bass < 100 Hz - mid = 100 - 700 Hz - highmid = 700 - 2000 Hz - uppermid = 2000 - 4000 Hz - presence = 4000 - 6000 Hz - brillance > 6000 Hz :return : A dictionary with the divided signal as values and bin names as keys """ bins = self.SP.bins.__dict__ bass_filter = sig.butter(12, bins["bass"].value, 'lp', fs=self.sr, output='sos') mid_filter = sig.butter(12, [bins["bass"].value, bins['mid'].value], 'bp', fs=self.sr, output='sos') himid_filter = sig.butter(12, [bins["mid"].value, bins['highmid'].value], 'bp', fs=self.sr, output='sos') upmid_filter = sig.butter(12, [bins["highmid"].value, bins['uppermid'].value], 'bp', fs=self.sr, output='sos') pres_filter = sig.butter(12, [bins["uppermid"].value, bins['presence'].value], 'bp', fs=self.sr, output='sos') bril_filter = sig.butter(12, bins['presence'].value, 'hp', fs=self.sr, output='sos') return { "bass": Signal(sig.sosfilt(bass_filter, self.signal), self.sr, self.SP, freq_range=[0, bins["bass"].value]), "mid": Signal(sig.sosfilt(mid_filter, self.signal), self.sr, self.SP, freq_range=[bins["bass"].value, bins["mid"].value]), "highmid": Signal(sig.sosfilt(himid_filter, self.signal), self.sr, self.SP, freq_range=[bins["mid"].value, bins["highmid"].value]), "uppermid": Signal(sig.sosfilt(upmid_filter, self.signal), self.sr, self.SP, freq_range=[bins["highmid"].value, bins["uppermid"].value]), "presence": Signal(sig.sosfilt(pres_filter, self.signal), self.sr, self.SP, freq_range=[bins['uppermid'].value, bins["presence"].value]), "brillance": Signal(sig.sosfilt(bril_filter, self.signal), self.sr, self.SP, freq_range=[bins["presence"].value, max(self.fft_frequencies())])} def timbre(self): """ A method computing the timbral attributes of the signal This method returns timbral attributes "Brightness", "Depth", "Boominess", "Sharpness" and "Warmth". They are obtained trough linear regression with a model trained with regular sounds. More information : Andy Pearce, Mark Plumbley, Saeid, S., Brookes, T., Mason, R., & Wang, W. (2019). Release of timbral characterisation tools for semantically annotating non-musical content.pdf (Rapport No. AC-WP5-SURREY-D5.8). AudioCommons. Repéré à : https://www.audiocommons.org/assets/files/AC-WP5-SURREY-D5.8%20Release%20of%20timbral %20characterisation%20tools%20for%20semantically%20annotating%20non-musical%20content.pdf :return: A dictionary with timbral attributes and their values """ # Save the signal in a temporary file self.save_wav('temp') # Compute the timbre dict from the temp file timbre = timbral_extractor('temp.wav', verbose=False) # remove reverb and roughness and hardness attributes timbre = {key: timbre[key] for key in timbre if key not in ['reverb', 'roughness', 'hardness']} # Remove the temp file os.remove('temp.wav') return timbre def save_wav(self, name, path=''): """ Create a soundfile from a signal :param name: the name of the saved file :param path: the path were the '.wav' file is saved """ write(path + name + ".wav", self.signal, self.sr)
Methods
def cavity_frequency(self)
-
Finds the hemlotz cavity frequency from the Fourier Transform by searching for a peak in the expected range (80 - 100 Hz), if the fundamental is too close to the expected hemlotz frequency a comment is printed and None is returned. :return: If successful, the cavity peak frequency
Expand source code
def cavity_frequency(self): """ Finds the hemlotz cavity frequency from the Fourier Transform by searching for a peak in the expected range (80 - 100 Hz), if the fundamental is too close to the expected hemlotz frequency a comment is printed and None is returned. :return: If successful, the cavity peak frequency """ first_index = np.where(self.fft_frequencies() >= 80)[0][0] second_index = np.where(self.fft_frequencies() >= 110)[0][0] cavity_peak = np.argmax(self.fft()[first_index:second_index]) + first_index if self.fundamental() == self.fft_frequencies()[cavity_peak]: print('Cavity peak is obscured by the fundamental') return 0 else: return self.fft_frequencies()[cavity_peak]
def cavity_peak(self)
-
Finds the Hemlotz cavity frequency index from the Fourier Transform by searching for a peak in the expected range (80 - 100 Hz), if the fundamental is too close to the expected Hemlotz frequency a comment is printed and None is returned. :return: If successful the cavity peak index
Expand source code
def cavity_peak(self): """ Finds the Hemlotz cavity frequency index from the Fourier Transform by searching for a peak in the expected range (80 - 100 Hz), if the fundamental is too close to the expected Hemlotz frequency a comment is printed and None is returned. :return: If successful the cavity peak index """ first_index = np.where(self.fft_frequencies() >= 80)[0][0] second_index = np.where(self.fft_frequencies() >= 110)[0][0] cavity_peak = np.argmax(self.fft()[first_index:second_index]) + first_index if self.fundamental() == self.fft_frequencies()[cavity_peak]: print('Cavity peak is obscured by the fundamental') else: return cavity_peak
def envelop(self)
-
Method calculating the amplitude envelope of a signal as a maximum of the absolute value of the signal. :return: Amplitude envelop of the signal
Expand source code
def envelop(self): """ Method calculating the amplitude envelope of a signal as a maximum of the absolute value of the signal. :return: Amplitude envelop of the signal """ # Get the hop length hop_length = self.SP.envelop.hop_length.value # Compute the envelop envelop = np.array( [np.max(np.abs(self.signal[i:i + self.SP.envelop.frame_size.value])) for i in range(0, len(self.signal), hop_length)]) envelop = np.insert(envelop, 0, 0) return envelop
def envelop_time(self)
-
Method calculating the time vector associated to a signal envelop :return: Time vector associated to the signal envelop
Expand source code
def envelop_time(self): """ Method calculating the time vector associated to a signal envelop :return: Time vector associated to the signal envelop """ # Get the number of frames from the signal envelop frames = range(len(self.envelop())) # Return the envelop frames computed with Librosa return librosa.frames_to_time(frames, hop_length=self.SP.envelop.hop_length.value)
def fft(self)
-
Computes the Fast Fourier Transform of the signal and returns the vector. :return: Fast Fourier Transform amplitude values in a numpy array
Expand source code
def fft(self): """ Computes the Fast Fourier Transform of the signal and returns the vector. :return: Fast Fourier Transform amplitude values in a numpy array """ fft = np.fft.fft(self.signal) fft = np.abs(fft[:int(len(fft) // 2)]) # Only the symmetric of the absolute value return fft / np.max(fft)
def fft_bins(self)
-
Transforms the Fourier Transform signal into a statistic distribution. Occurences of each frequency varies from 0 to 100 according to their amplitude. :return : a list containing the frequency occurences.
Expand source code
def fft_bins(self): """ Transforms the Fourier Transform signal into a statistic distribution. Occurences of each frequency varies from 0 to 100 according to their amplitude. :return : a list containing the frequency occurences. """ # Make the FT values integers fft_integers = [int(np.around(sample * 100, 0)) for sample in self.fft()] # Create a list of the frequency occurrences in the signal occurrences = [] for freq, count in zip(self.fft_frequencies(), fft_integers): occurrences.append([freq] * count) # flatten the list return [item for sublist in occurrences for item in sublist]
def fft_frequencies(self)
-
Computes the frequency vector associated to the Signal Fourier Transform :return: an array containing the frequency values.
Expand source code
def fft_frequencies(self): """ Computes the frequency vector associated to the Signal Fourier Transform :return: an array containing the frequency values. """ fft = self.fft() fft_frequencies = np.fft.fftfreq(len(fft) * 2, 1 / self.sr) # Frequencies corresponding to the bins return fft_frequencies[:len(fft)]
def filter_noise(self, verbose=True)
-
Method filtering the noise from the recorded signal and returning a filtered signal. If the signal was not trimmed it is trimmed in place then filtered. If the signal can not be trimmed it can't be filtered and the original signal is returned :return : A Signal instance, filtered if possible.
Expand source code
def filter_noise(self, verbose=True): """ Method filtering the noise from the recorded signal and returning a filtered signal. If the signal was not trimmed it is trimmed in place then filtered. If the signal can not be trimmed it can't be filtered and the original signal is returned :return : A Signal instance, filtered if possible. """ try: return Signal(reduce_noise(audio_clip=self.signal, noise_clip=self.noise), self.sr, self.SP) except AttributeError: if self.trimmed is False: if verbose: print('Not sufficient noise in the raw signal, unable to filter.') print('') return self
def find_onset(self, verbose=True)
-
Finds the onset as an increase in more of 50% with the maximum normalized value above 0.5 :param verbose: Prints a warning if the algorithm does not converge :return: the index of the onset in the signal
Expand source code
def find_onset(self, verbose=True): """ Finds the onset as an increase in more of 50% with the maximum normalized value above 0.5 :param verbose: Prints a warning if the algorithm does not converge :return: the index of the onset in the signal """ # Index corresponding to the onset time interval window_index = np.ceil(self.SP.onset.onset_time.value * self.sr).astype(int) # Use the normalized signal to compare against a fixed value onset_signal = self.normalize() overlap = window_index // 2 # overlap for algorithm progression # Initial values increase = 0 i = 0 broke = False while increase <= 0.5: signal_min = np.min(np.abs(onset_signal.signal[i:i + window_index])) signal_max = np.max(np.abs(onset_signal.signal[i:i + window_index])) if (signal_max > 0.5) and (signal_min != 0): increase = signal_max / signal_min else: increase = 0 i += overlap if i + window_index > len(self.signal): if verbose: print('Onset detection did not converge \n') print('Approximating onset with signal max value \n') broke = True break if broke: return np.argmax(self.signal) else: return np.argmax(np.abs(self.signal[i:i + window_index])) + i
def fundamental(self)
-
Returns the fundamental approximated by the first peak of the fft :return: fundamental value (Hz)
Expand source code
def fundamental(self): """ Returns the fundamental approximated by the first peak of the fft :return: fundamental value (Hz) """ index = self.peaks()[0] fundamental = self.fft_frequencies()[index] return fundamental
def listen(self)
-
Method to listen the sound signal in a Jupyter Notebook
Listening to the sounds imported in the analysis tool allows the user to validate if the sound was well trimmed and filtered
A temporary file is created, the IPython display Audio function is called on it and then the file is removed
Expand source code
def listen(self): """ Method to listen the sound signal in a Jupyter Notebook Listening to the sounds imported in the analysis tool allows the user to validate if the sound was well trimmed and filtered A temporary file is created, the IPython display Audio function is called on it and then the file is removed """ file = 'temp.wav' write(file, self.signal, self.sr) ipd.display(ipd.Audio(file)) os.remove(file)
def log_envelop(self)
-
Computes the logarithmic scale envelop of the signal. The width of the samples increases exponentially so that the envelop appears having a constant window width on an X axis logarithmic scale. :return: The log envelop and the time vector associated in a tuple
Expand source code
def log_envelop(self): """ Computes the logarithmic scale envelop of the signal. The width of the samples increases exponentially so that the envelop appears having a constant window width on an X axis logarithmic scale. :return: The log envelop and the time vector associated in a tuple """ if self.onset is None: onset = np.argmax(self.signal) else: onset = self.onset start_time = self.SP.log_envelop.start_time.value while start_time > (onset / self.sr): start_time /= 10. start_exponent = int(np.log10(start_time)) # closest 10^x value for smooth graph if self.SP.log_envelop.min_window.value is None: min_window = 15 ** (start_exponent + 4) if min_window < 15: # Value should at least be 10 min_window = 15 else: min_window = self.SP.log_envelop.min_window.value # initial values current_exponent = start_exponent current_time = 10 ** current_exponent # start time on log scale index = int(current_time * self.sr) # Start at the specified time window = min_window # number of samples per window overlap = window // 2 log_envelop = [] log_envelop_time = [0] # First value for comparison while index + window <= len(self.signal): while log_envelop_time[-1] < 10 ** (current_exponent + 1): if (index + window) < len(self.signal): log_envelop.append(np.max(self.signal[index:index + window])) log_envelop_time.append(self.time()[index]) index += overlap else: break if window * 10 < self.SP.log_envelop.max_window.value: window = window * 10 else: window = self.SP.log_envelop.max_window.value overlap = window // 2 current_exponent += 1 # remove the value where t=0 so the log scale does not break log_envelop_time.remove(0) return np.array(log_envelop), np.array(log_envelop_time)
def make_freq_bins(self)
-
Method to divide a signal in frequency bins using butterworth filters bins are passed as a dict, default values are : - bass < 100 Hz - mid = 100 - 700 Hz - highmid = 700 - 2000 Hz - uppermid = 2000 - 4000 Hz - presence = 4000 - 6000 Hz - brillance > 6000 Hz :return : A dictionary with the divided signal as values and bin names as keys
Expand source code
def make_freq_bins(self): """ Method to divide a signal in frequency bins using butterworth filters bins are passed as a dict, default values are : - bass < 100 Hz - mid = 100 - 700 Hz - highmid = 700 - 2000 Hz - uppermid = 2000 - 4000 Hz - presence = 4000 - 6000 Hz - brillance > 6000 Hz :return : A dictionary with the divided signal as values and bin names as keys """ bins = self.SP.bins.__dict__ bass_filter = sig.butter(12, bins["bass"].value, 'lp', fs=self.sr, output='sos') mid_filter = sig.butter(12, [bins["bass"].value, bins['mid'].value], 'bp', fs=self.sr, output='sos') himid_filter = sig.butter(12, [bins["mid"].value, bins['highmid'].value], 'bp', fs=self.sr, output='sos') upmid_filter = sig.butter(12, [bins["highmid"].value, bins['uppermid'].value], 'bp', fs=self.sr, output='sos') pres_filter = sig.butter(12, [bins["uppermid"].value, bins['presence'].value], 'bp', fs=self.sr, output='sos') bril_filter = sig.butter(12, bins['presence'].value, 'hp', fs=self.sr, output='sos') return { "bass": Signal(sig.sosfilt(bass_filter, self.signal), self.sr, self.SP, freq_range=[0, bins["bass"].value]), "mid": Signal(sig.sosfilt(mid_filter, self.signal), self.sr, self.SP, freq_range=[bins["bass"].value, bins["mid"].value]), "highmid": Signal(sig.sosfilt(himid_filter, self.signal), self.sr, self.SP, freq_range=[bins["mid"].value, bins["highmid"].value]), "uppermid": Signal(sig.sosfilt(upmid_filter, self.signal), self.sr, self.SP, freq_range=[bins["highmid"].value, bins["uppermid"].value]), "presence": Signal(sig.sosfilt(pres_filter, self.signal), self.sr, self.SP, freq_range=[bins['uppermid'].value, bins["presence"].value]), "brillance": Signal(sig.sosfilt(bril_filter, self.signal), self.sr, self.SP, freq_range=[bins["presence"].value, max(self.fft_frequencies())])}
def normalize(self)
-
Normalizes the signal to [-1, 1] and returns the normalised instance. :return : A normalized signal
Expand source code
def normalize(self): """ Normalizes the signal to [-1, 1] and returns the normalised instance. :return : A normalized signal """ factor = np.max(np.abs(self.signal)) normalised_signal = Signal((self.signal / factor), self.sr, self.SP) normalised_signal.norm_factor = (1 / factor) return normalised_signal
def old_plot(self, kind, **kwargs)
-
Convenience function for the different signal plots
Calls the function corresponding to Plot.kind() See help(guitarsounds.analysis.Plot) for info on the different plots
Expand source code
def old_plot(self, kind, **kwargs): """ Convenience function for the different signal plots Calls the function corresponding to Plot.kind() See help(guitarsounds.analysis.Plot) for info on the different plots """ self.plot.method_dict[kind](**kwargs)
def peak_damping(self)
-
Computes the frequency wise damping with the half bandwidth method on the Fourier Transform peaks :return: an array containing the peak damping values
Expand source code
def peak_damping(self): """ Computes the frequency wise damping with the half bandwidth method on the Fourier Transform peaks :return: an array containing the peak damping values """ zetas = [] fft_freqs = self.fft_frequencies() fft = self.fft()[:len(fft_freqs)] for peak in self.peaks(): peak_frequency = fft_freqs[peak] peak_height = fft[peak] root_height = peak_height / np.sqrt(2) frequency_roots = scipy.interpolate.InterpolatedUnivariateSpline(fft_freqs, fft - root_height).roots() sorted_roots_indexes = np.argsort(np.abs(frequency_roots - peak_frequency)) w2, w1 = frequency_roots[sorted_roots_indexes[:2]] w1, w2 = np.sort([w1, w2]) zeta = (w2 - w1) / (2 * peak_frequency) zetas.append(zeta) return np.array(zetas)
def peaks(self, max_freq=None, height=False, result=False)
-
Computes the harmonic peaks indexes from the FFT of the signal :param max_freq: Supply a max frequency value overiding the one in guitarsounds_parameters :param height: if True the height threshold is returned to be used in the 'peaks' plot :param result: if True the Scipy peak finding results dictionary is returned :return: peak indexes
Expand source code
def peaks(self, max_freq=None, height=False, result=False): """ Computes the harmonic peaks indexes from the FFT of the signal :param max_freq: Supply a max frequency value overiding the one in guitarsounds_parameters :param height: if True the height threshold is returned to be used in the 'peaks' plot :param result: if True the Scipy peak finding results dictionary is returned :return: peak indexes """ # Replace None by the default value if max_freq is None: max_freq = self.SP.general.fft_range.value # Get the fft and fft frequencies from the signal fft, fft_freq = self.fft(), self.fft_frequencies() # Find the max index max_index = np.where(fft_freq >= max_freq)[0][0] # Find an approximation of the distance between peaks, this only works for harmonic signals peak_distance = np.argmax(fft) // 2 # Maximum of the signal in a small region on both ends fft_max_start = np.max(fft[:peak_distance]) fft_max_end = np.max(fft[max_index - peak_distance:max_index]) # Build the curve below the peaks but above the noise exponents = np.linspace(np.log10(fft_max_start), np.log10(fft_max_end), max_index) intersect = 10 ** exponents[peak_distance] diff_start = fft_max_start - intersect # offset by a small distance so that the first max is not a peak min_height = 10 ** np.linspace(np.log10(fft_max_start + diff_start), np.log10(fft_max_end), max_index) first_peak_indexes, _ = sig.find_peaks(fft[:max_index], height=min_height, distance=peak_distance) number_of_peaks = len(first_peak_indexes) if number_of_peaks > 0: average_len = int(max_index / number_of_peaks) * 3 else: average_len = int(max_index / 3) if average_len % 2 == 0: average_len += 1 average_fft = sig.savgol_filter(fft[:max_index], average_len, 1, mode='mirror') * 1.9 min_freq_index = np.where(fft_freq >= 70)[0][0] average_fft[:min_freq_index] = 1 peak_indexes, res = sig.find_peaks(fft[:max_index], height=average_fft, distance=min_freq_index) # Remove noisy peaks at the low frequencies while fft[peak_indexes[0]] < 5e-2: peak_indexes = np.delete(peak_indexes, 0) while fft[peak_indexes[-1]] < 1e-4: peak_indexes = np.delete(peak_indexes, -1) if not height and not result: return peak_indexes elif height: return peak_indexes, average_fft elif result: return peak_indexes, res elif height and result: return peak_indexes, height, res
def save_wav(self, name, path='')
-
Create a soundfile from a signal :param name: the name of the saved file :param path: the path were the '.wav' file is saved
Expand source code
def save_wav(self, name, path=''): """ Create a soundfile from a signal :param name: the name of the saved file :param path: the path were the '.wav' file is saved """ write(path + name + ".wav", self.signal, self.sr)
def timbre(self)
-
A method computing the timbral attributes of the signal
This method returns timbral attributes "Brightness", "Depth", "Boominess", "Sharpness" and "Warmth". They are obtained trough linear regression with a model trained with regular sounds. More information : Andy Pearce, Mark Plumbley, Saeid, S., Brookes, T., Mason, R., & Wang, W. (2019). Release of timbral characterisation tools for semantically annotating non-musical content.pdf (Rapport No. AC-WP5-SURREY-D5.8). AudioCommons. Repéré à : https://www.audiocommons.org/assets/files/AC-WP5-SURREY-D5.8%20Release%20of%20timbral %20characterisation%20tools%20for%20semantically%20annotating%20non-musical%20content.pdf :return: A dictionary with timbral attributes and their values
Expand source code
def timbre(self): """ A method computing the timbral attributes of the signal This method returns timbral attributes "Brightness", "Depth", "Boominess", "Sharpness" and "Warmth". They are obtained trough linear regression with a model trained with regular sounds. More information : Andy Pearce, Mark Plumbley, Saeid, S., Brookes, T., Mason, R., & Wang, W. (2019). Release of timbral characterisation tools for semantically annotating non-musical content.pdf (Rapport No. AC-WP5-SURREY-D5.8). AudioCommons. Repéré à : https://www.audiocommons.org/assets/files/AC-WP5-SURREY-D5.8%20Release%20of%20timbral %20characterisation%20tools%20for%20semantically%20annotating%20non-musical%20content.pdf :return: A dictionary with timbral attributes and their values """ # Save the signal in a temporary file self.save_wav('temp') # Compute the timbre dict from the temp file timbre = timbral_extractor('temp.wav', verbose=False) # remove reverb and roughness and hardness attributes timbre = {key: timbre[key] for key in timbre if key not in ['reverb', 'roughness', 'hardness']} # Remove the temp file os.remove('temp.wav') return timbre
def time(self)
-
Returns the time vector associated to the signal :return: numpy array corresponding to the time values of the signal samples in seconds
Expand source code
def time(self): """ Returns the time vector associated to the signal :return: numpy array corresponding to the time values of the signal samples in seconds """ return np.linspace(0, len(self.signal) * (1 / self.sr), len(self.signal))
def time_damping(self)
-
Computes the time wise damping ratio of the signal by fitting a negative exponential curve to the Signal envelop and computing the ratio with the Signal fundamental frequency. :return: The damping ratio, a scalar.
Expand source code
def time_damping(self): """ Computes the time wise damping ratio of the signal by fitting a negative exponential curve to the Signal envelop and computing the ratio with the Signal fundamental frequency. :return: The damping ratio, a scalar. """ # Get the envelop data envelop_time = self.normalize().envelop_time() envelop = self.normalize().envelop() # First point is the maximum because e^-kt is stricly decreasing first_index = np.argmax(envelop) # The second point is the first point where the signal crosses the lower_threshold line second_point_thresh = self.SP.damping.lower_threshold.value try: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh)[0] except IndexError: second_index = np.flatnonzero(envelop[first_index:] <= second_point_thresh * 2)[0] # Function to compute the residual for the exponential curve fit def residual_function(zeta_w, t, s): """ Function computing the residual to curve fit a negative exponential to the signal envelop :param zeta_w: zeta*omega constant :param t: time vector :param s: signal :return: residual """ return np.exp(zeta_w[0] * t) - s zeta_guess = [-0.5] result = scipy.optimize.least_squares(residual_function, zeta_guess, args=(envelop_time[first_index:second_index], envelop[first_index:second_index])) # Get the zeta*omega constant zeta_omega = result.x[0] # Compute the fundamental frequency in radiants of the signal wd = 2 * np.pi * self.fundamental() return -zeta_omega / wd
def trim_onset(self, verbose=True)
-
Trim the signal at the onset (max) minus the delay in milliseconds as Specified in the SoundParameters :param : verbose if False the warning comments are not displayed :return : a trimmed signal with a noise attribute
Expand source code
def trim_onset(self, verbose=True): """ Trim the signal at the onset (max) minus the delay in milliseconds as Specified in the SoundParameters :param : verbose if False the warning comments are not displayed :return : a trimmed signal with a noise attribute """ # nb of samples to keep before the onset delay_samples = int((self.SP.onset.onset_delay.value / 1000) * self.sr) onset = self.find_onset(verbose=verbose) # find the onset if onset > delay_samples: # To make sure the index is positive trimmed_signal = Signal(self.signal[onset - delay_samples:], self.sr, self.SP) trimmed_signal.noise = self.signal[:onset - delay_samples] trimmed_signal.trimmed = True trimmed_signal.onset = np.argmax(trimmed_signal.signal) return trimmed_signal else: if verbose: print('Signal is too short to be trimmed before onset.') print('') self.trimmed = False return self
def trim_time(self, time_length)
-
Trims the signal to the specified length and returns a new Signal instance. :param time_length: desired length of the new signal in seconds. :return: A trimmed Signal
Expand source code
def trim_time(self, time_length): """ Trims the signal to the specified length and returns a new Signal instance. :param time_length: desired length of the new signal in seconds. :return: A trimmed Signal """ max_index = int(time_length * self.sr) time_trimmed_signal = Signal(self.signal[:max_index], self.sr, self.SP) time_trimmed_signal.time_length = time_length return time_trimmed_signal
class Sound (file, name='', fundamental=None, SoundParams=None)
-
A class to store audio signals obtained from a sound and compare them
Creates a Sound instance from a .wav file, name as a string and fundamental frequency value can be user specified. :param file: file path to the .wav file :param name: Sound instance name to use in plot legend and titles :param fundamental: Fundamental frequency value if None the value is estimated from the FFT (see
Signal.fundamental()
). :param SoundParams: SoundParameters to use in the Sound instanceExpand source code
class Sound(object): """ A class to store audio signals obtained from a sound and compare them """ def __init__(self, file, name='', fundamental=None, SoundParams=None): """ Creates a Sound instance from a .wav file, name as a string and fundamental frequency value can be user specified. :param file: file path to the .wav file :param name: Sound instance name to use in plot legend and titles :param fundamental: Fundamental frequency value if None the value is estimated from the FFT (see `Signal.fundamental`). :param SoundParams: SoundParameters to use in the Sound instance """ # create a reference of the parameters if SoundParams is None: self.SP = SP else: self.SP = SoundParams if type(file) == str: # Load the soundfile using librosa signal, sr = librosa.load(file) self.file = file elif type(file) == tuple: signal, sr = file # create a Signal class from the signal and sample rate self.raw_signal = Signal(signal, sr, self.SP) # Allow user specified fundamental self.fundamental = fundamental self.name = name def condition(self, verbose=True, return_self=False): """ A method conditioning the Sound instance. - Trimming to just before the onset - Filtering the noise :param verbose: if True problem with trimming and filtering are reported :param return_self: If True the method returns the conditioned Sound instance :return: a conditioned Sound instance if `return_self = True` """ self.trim_signal(verbose=verbose) self.filter_noise(verbose=verbose) self.bin_divide() if self.fundamental is None: self.fundamental = self.signal.fundamental() self.plot = self.signal.plot if return_self: return self def use_raw_signal(self, normalized=False): """ Assigns the raw signal to the `signal` attribute of the Sound instance to analyze it :param normalized: if True, the raw signal is first normalized :return: None """ if normalized: self.signal = self.raw_signal.normalize() else: self.signal = self.raw_signal def bin_divide(self): """ Calls the `.make_freq_bins` method of the signal to create the signals associated to the frequency bins. The bins are all stored in the `.bin` attribute and also as their names (Ex: `Sound.mid` contains the mid signal). :return: None """ """ a method to divide the main signal into frequency bins""" # divide in frequency bins self.bins = self.signal.make_freq_bins() # unpack the bins self.bass, self.mid, self.highmid, self.uppermid, self.presence, self.brillance = self.bins.values() def filter_noise(self, verbose=True): """ Filters the noise in the signal attribute :param verbose: if True problem are printed to the terminal :return: None """ # filter the noise in the Signal class self.signal = self.trimmed_signal.filter_noise(verbose=verbose) def trim_signal(self, verbose=True): """ A method to trim the signal to a specific time before the onset. The time value can be changed in the SoundParameters. :param verbose: if True problems encountered are printed to the terminal :return: None """ # Trim the signal in the signal class self.trimmed_signal = self.raw_signal.trim_onset(verbose=verbose) def listen_freq_bins(self): """ Method to listen to all the frequency bins of a sound The bins signals are obtained by filtering the sound signal with band pass filters. See guitarsounds.parameters.sound_parameters().bins.info() for the frequency bin intervals. """ for key in self.bins.keys(): print(key) self.bins[key].normalize().listen() def plot_freq_bins(self, bins=None): """ Method to plot all the frequency bins logarithmic envelops of a sound The parameter `bins` allows choosing specific frequency bins to plot By default the function plots all the bins Supported bins arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' Example : `Sound.plot_freq_bins(bins=['all])` plots all the frequency bins `Sound.plot_freq_bins(bins=['bass', 'mid'])` plots the bass and mid bins """ try: value = bins[0] if value == 'all': bins = self.bins.keys() except TypeError: if bins is None: bins = self.bins.keys() for key in bins: lab = key + ' : ' + str(int(self.bins[key].range[0])) + ' - ' + str(int(self.bins[key].range[1])) + ' Hz' self.bins[key].old_plot('log envelop', label=lab) plt.xscale('log') plt.yscale('log') plt.legend(fontsize="x-small") # using a named size def peak_damping(self): """ Prints a table with peak damping values and peak frequency values The peaks are found with the `signal.peaks()` function and the damping values are computed with the half power bandwith method. """ peak_indexes = self.signal.peaks() frequencies = self.signal.fft_frequencies()[peak_indexes] damping = self.signal.peak_damping() table_data = np.array([frequencies, np.array(damping) * 100]).transpose() print(tabulate(table_data, headers=['Frequency (Hz)', 'Damping ratio (%)'])) def bin_hist(self): """ Histogram of the frequency bin power frequency bin power is computed as the integral of the bin envelop. See guitarsounds.parameters.sound_parameters().bins.info() for the frequency bin intervals. """ # Compute the bin powers bin_strings = list(self.bins.keys()) integral = [] for f_bin in bin_strings: log_envelop, log_time = self.bins[f_bin].normalize().log_envelop() integral.append(scipy.integrate.trapezoid(log_envelop, log_time)) # create the bar plotting vectors fig, ax = plt.subplots(figsize=(6, 6)) x = np.arange(0, len(bin_strings)) y = integral ax.bar(x, y, tick_label=list(bin_strings))
Methods
def bin_divide(self)
-
Calls the
.make_freq_bins
method of the signal to create the signals associated to the frequency bins. The bins are all stored in the.bin
attribute and also as their names (Ex:Sound.mid
contains the mid signal). :return: NoneExpand source code
def bin_divide(self): """ Calls the `.make_freq_bins` method of the signal to create the signals associated to the frequency bins. The bins are all stored in the `.bin` attribute and also as their names (Ex: `Sound.mid` contains the mid signal). :return: None """ """ a method to divide the main signal into frequency bins""" # divide in frequency bins self.bins = self.signal.make_freq_bins() # unpack the bins self.bass, self.mid, self.highmid, self.uppermid, self.presence, self.brillance = self.bins.values()
def bin_hist(self)
-
Histogram of the frequency bin power
frequency bin power is computed as the integral of the bin envelop. See guitarsounds.parameters.sound_parameters().bins.info() for the frequency bin intervals.
Expand source code
def bin_hist(self): """ Histogram of the frequency bin power frequency bin power is computed as the integral of the bin envelop. See guitarsounds.parameters.sound_parameters().bins.info() for the frequency bin intervals. """ # Compute the bin powers bin_strings = list(self.bins.keys()) integral = [] for f_bin in bin_strings: log_envelop, log_time = self.bins[f_bin].normalize().log_envelop() integral.append(scipy.integrate.trapezoid(log_envelop, log_time)) # create the bar plotting vectors fig, ax = plt.subplots(figsize=(6, 6)) x = np.arange(0, len(bin_strings)) y = integral ax.bar(x, y, tick_label=list(bin_strings))
def condition(self, verbose=True, return_self=False)
-
A method conditioning the Sound instance. - Trimming to just before the onset - Filtering the noise :param verbose: if True problem with trimming and filtering are reported :param return_self: If True the method returns the conditioned Sound instance :return: a conditioned Sound instance if
return_self = True
Expand source code
def condition(self, verbose=True, return_self=False): """ A method conditioning the Sound instance. - Trimming to just before the onset - Filtering the noise :param verbose: if True problem with trimming and filtering are reported :param return_self: If True the method returns the conditioned Sound instance :return: a conditioned Sound instance if `return_self = True` """ self.trim_signal(verbose=verbose) self.filter_noise(verbose=verbose) self.bin_divide() if self.fundamental is None: self.fundamental = self.signal.fundamental() self.plot = self.signal.plot if return_self: return self
def filter_noise(self, verbose=True)
-
Filters the noise in the signal attribute :param verbose: if True problem are printed to the terminal :return: None
Expand source code
def filter_noise(self, verbose=True): """ Filters the noise in the signal attribute :param verbose: if True problem are printed to the terminal :return: None """ # filter the noise in the Signal class self.signal = self.trimmed_signal.filter_noise(verbose=verbose)
def listen_freq_bins(self)
-
Method to listen to all the frequency bins of a sound
The bins signals are obtained by filtering the sound signal with band pass filters.
See guitarsounds.parameters.sound_parameters().bins.info() for the frequency bin intervals.
Expand source code
def listen_freq_bins(self): """ Method to listen to all the frequency bins of a sound The bins signals are obtained by filtering the sound signal with band pass filters. See guitarsounds.parameters.sound_parameters().bins.info() for the frequency bin intervals. """ for key in self.bins.keys(): print(key) self.bins[key].normalize().listen()
def peak_damping(self)
-
Prints a table with peak damping values and peak frequency values
The peaks are found with the
signal.peaks()
function and the damping values are computed with the half power bandwith method.Expand source code
def peak_damping(self): """ Prints a table with peak damping values and peak frequency values The peaks are found with the `signal.peaks()` function and the damping values are computed with the half power bandwith method. """ peak_indexes = self.signal.peaks() frequencies = self.signal.fft_frequencies()[peak_indexes] damping = self.signal.peak_damping() table_data = np.array([frequencies, np.array(damping) * 100]).transpose() print(tabulate(table_data, headers=['Frequency (Hz)', 'Damping ratio (%)']))
def plot_freq_bins(self, bins=None)
-
Method to plot all the frequency bins logarithmic envelops of a sound
The parameter
bins
allows choosing specific frequency bins to plot By default the function plots all the bins Supported bins arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'Example :
Sound.plot_freq_bins(bins=['all])
plots all the frequency binsSound.plot_freq_bins(bins=['bass', 'mid'])
plots the bass and mid binsExpand source code
def plot_freq_bins(self, bins=None): """ Method to plot all the frequency bins logarithmic envelops of a sound The parameter `bins` allows choosing specific frequency bins to plot By default the function plots all the bins Supported bins arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' Example : `Sound.plot_freq_bins(bins=['all])` plots all the frequency bins `Sound.plot_freq_bins(bins=['bass', 'mid'])` plots the bass and mid bins """ try: value = bins[0] if value == 'all': bins = self.bins.keys() except TypeError: if bins is None: bins = self.bins.keys() for key in bins: lab = key + ' : ' + str(int(self.bins[key].range[0])) + ' - ' + str(int(self.bins[key].range[1])) + ' Hz' self.bins[key].old_plot('log envelop', label=lab) plt.xscale('log') plt.yscale('log') plt.legend(fontsize="x-small") # using a named size
def trim_signal(self, verbose=True)
-
A method to trim the signal to a specific time before the onset. The time value can be changed in the SoundParameters. :param verbose: if True problems encountered are printed to the terminal :return: None
Expand source code
def trim_signal(self, verbose=True): """ A method to trim the signal to a specific time before the onset. The time value can be changed in the SoundParameters. :param verbose: if True problems encountered are printed to the terminal :return: None """ # Trim the signal in the signal class self.trimmed_signal = self.raw_signal.trim_onset(verbose=verbose)
def use_raw_signal(self, normalized=False)
-
Assigns the raw signal to the
signal
attribute of the Sound instance to analyze it :param normalized: if True, the raw signal is first normalized :return: NoneExpand source code
def use_raw_signal(self, normalized=False): """ Assigns the raw signal to the `signal` attribute of the Sound instance to analyze it :param normalized: if True, the raw signal is first normalized :return: None """ if normalized: self.signal = self.raw_signal.normalize() else: self.signal = self.raw_signal
class SoundPack (*sounds, names=None, fundamentals=None, SoundParams=None, equalize_time=True)
-
A class to store and analyse multiple sounds Some methods are only available for the case with two sounds
The SoundPack can be instantiated from existing Sound class instances, either in a list or as multiple arguments
The class can also handle the creation of Sound class instances if the arguments are filenames, either a list or multiple arguments.
If the number of Sound contained is equal to two, the SoundPack will be 'dual' and the associated methods will be available
If it contains multiple sounds the SoundPack will be multiple and a reduced number of methods will work
A list of names as strings and fundamental frequencies can be specified when creating the SoundPack
If equalize_time is set to False, the contained sounds will not be trimmed to the same length.
Examples :
Sound_Test = SoundPack('sounds/test1.wav', 'sounds/test2.wav', names=['A', 'B'], fundamentals = [134, 134]) sounds = [sound1, sound2, sound3, sound4, sound5] # instances of the Sound class large_Test = SoundPack(sounds, names=['1', '2', '3', '4', '5'])
Expand source code
class SoundPack(object): """ A class to store and analyse multiple sounds Some methods are only available for the case with two sounds """ def __init__(self, *sounds, names=None, fundamentals=None, SoundParams=None, equalize_time=True): """ The SoundPack can be instantiated from existing Sound class instances, either in a list or as multiple arguments The class can also handle the creation of Sound class instances if the arguments are filenames, either a list or multiple arguments. If the number of Sound contained is equal to two, the SoundPack will be 'dual' and the associated methods will be available If it contains multiple sounds the SoundPack will be multiple and a reduced number of methods will work A list of names as strings and fundamental frequencies can be specified when creating the SoundPack If equalize_time is set to False, the contained sounds will not be trimmed to the same length. Examples : ``` Sound_Test = SoundPack('sounds/test1.wav', 'sounds/test2.wav', names=['A', 'B'], fundamentals = [134, 134]) sounds = [sound1, sound2, sound3, sound4, sound5] # instances of the Sound class large_Test = SoundPack(sounds, names=['1', '2', '3', '4', '5']) ``` """ # create a copy of the sound parameters if SoundParams is None: self.SP = SP else: self.SP = SoundParams # Check if the sounds argument is a list if type(sounds[0]) is list: sounds = sounds[0] # unpack the list # Check for special case if len(sounds) == 2: # special case to compare two sounds self.kind = 'dual' elif len(sounds) > 1: # general case for multiple sounds self.kind = 'multiple' if type(sounds[0]) is str: self.sounds_from_files(sounds, names=names, fundamentals=fundamentals) else: self.sounds = sounds # Assign a default value to names if names is None: names = [str(n) for n in np.arange(1, len(sounds) + 1)] for sound, n in zip(self.sounds, names): sound.name = n # sound name defined in constructor elif names and (len(names) == len(self.sounds)): for sound, n in zip(self.sounds, names): sound.name = n if equalize_time: self.equalize_time() # Define bin strings self.bin_strings = [*list(self.SP.bins.__dict__.keys())[1:], 'brillance'] # Sort according to fundamental key = np.argsort([sound.fundamental for sound in self.sounds]) self.sounds = np.array(self.sounds)[key] def sounds_from_files(self, sound_files, names=None, fundamentals=None): """ Create Sound class instances and assign them to the SoundPack from a list of files :param sound_files: sound filenames :param names: sound names :param fundamentals: user specified fundamental frequencies :return: None """ # Make the default name list from sound filenames if none is supplied if (names is None) or (len(names) != len(sound_files)): names = [file[:-4] for file in sound_files] # remove the .wav # If the fundamentals are not supplied or mismatch in number None is used if (fundamentals is None) or (len(fundamentals) != len(sound_files)): fundamentals = len(sound_files) * [None] # Create Sound instances from files self.sounds = [] for file, name, fundamental in zip(sound_files, names, fundamentals): self.sounds.append(Sound(file, name=name, fundamental=fundamental, SoundParams=self.SP).condition(return_self=True)) def equalize_time(self): """ Trim the sounds so that they all have the length of the shortest sound, trimming is done at the end. :return: None """ trim_index = np.min([len(sound.signal.signal) for sound in self.sounds]) trimmed_sounds = [] for sound in self.sounds: new_sound = sound new_sound.signal = new_sound.signal.trim_time(trim_index / sound.signal.sr) new_sound.bin_divide() trimmed_sounds.append(new_sound) self.sounds = trimmed_sounds def normalize(self): """ Normalize all the signals in the SoundPack and returns a normaized instance of itself :return: SoundPack with normalized signals """ new_sounds = [] names = [sound.name for sound in self.sounds] fundamentals = [sound.fundamental for sound in self.sounds] for sound in self.sounds: sound.signal = sound.signal.normalize() new_sounds.append(sound) return SoundPack(new_sounds, names=names, fundamentals=fundamentals, SoundParams=self.SP, equalize_time=False) """ Methods for all SoundPacks """ def plot(self, kind, **kwargs): """ Superimposed plot of all the sounds on one figure for a specific kind __ Multiple SoundPack Method __ Plots a specific signal.plot for all sounds on the same figure Ex : compare_plot('fft') plots the fft of all sounds on a single figure The color argument is set to none so that the plots have different colors :param kind: Attribute passed to the `signal.plot()` method :param kwargs: key words arguments to pass to the `signal.plot()` method :return: None """ plt.figure(figsize=(8, 6)) for sound in self.sounds: kwargs['label'] = sound.name kwargs['color'] = None sound.signal.old_plot(kind, **kwargs) plt.title(kind + ' plot') if kind == 'timbre': plt.legend(bbox_to_anchor=(1.3, 0.9)) else: plt.legend() def compare_plot(self, kind, **kwargs): """ Plots all the sounds on different figures to compare them for a specific kind __ Multiple SoundPack Method __ Draws the same kind of plot on a different axis for each sound Example : `SoundPack.compare_plot('peaks')` with 4 Sounds will plot a figure with 4 axes, with each a different 'peak' plot. :param kind: kind argument passed to `Signal.plot()` :param kwargs: key word arguments passed to Signal.plot() :return: None """ # if a dual SoundPack : only plot two big plots if self.kind == 'dual': if kind == 'timbre': fig, axs = plt.subplots(1, 2, figsize=(8, 4), subplot_kw={'projection': 'polar'}) for sound, ax in zip(self.sounds, axs): plt.sca(ax) sound.signal.old_plot(kind, **kwargs) ax.set_title(kind + ' ' + sound.name) else: fig, axs = plt.subplots(1, 2, figsize=(12, 4)) for sound, ax in zip(self.sounds, axs): plt.sca(ax) sound.signal.old_plot(kind, **kwargs) ax.set_title(kind + ' ' + sound.name) plt.tight_layout() # If a multiple SoundPack : plot on a grid of axes elif self.kind == 'multiple': # find the n, m values for the subplots line and columns n = len(self.sounds) if n // 4 >= 10: # a lot of sounds cols = 4 elif n // 3 >= 10: # many sounds cols = 3 elif n // 2 <= 4: # a few sounds cols = 2 remainder = n % cols if remainder == 0: rows = n // cols else: rows = n // cols + 1 fig, axs = plt.subplots(rows, cols, figsize=(12, 4 * rows)) axs = axs.reshape(-1) for sound, ax in zip(self.sounds, axs): plt.sca(ax) sound.signal.old_plot(kind, **kwargs) title = ax.get_title() title = sound.name + ' ' + title ax.set_title(title) if remainder != 0: for ax in axs[-(cols - remainder):]: ax.set_axis_off() plt.tight_layout() def freq_bin_plot(self, f_bin='all'): """ Plots the log envelop of specified frequency bins __ Multiple SoundPack Method __ A function to compare signals decomposed frequency wise in the time domain on a logarithm scale. The methods plots all the sounds and plots their frequency bins according to the frequency bin argument f_bin. Example : SoundPack.freq_bin_plot(f_bin='mid') will plot the log-scale envelop of the 'mid' signal of every sound in the SoundPack f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' """ if f_bin == 'all': # Create one plot per bin fig, axs = plt.subplots(3, 2, figsize=(12, 12)) axs = axs.reshape(-1) for key, ax in zip([*list(self.SP.bins.__dict__.keys())[1:], 'brillance'], axs): plt.sca(ax) # plot every sound for a frequency bin norm_factors = np.array([son.bins[key].normalize().norm_factor for son in self.sounds]) for i, son in enumerate(self.sounds): son.bins[key].normalize().old_plot('log envelop', label=son.name) plt.xscale('log') plt.legend() title0 = ' ' + key + ' : ' + str(int(son.bins[key].range[0])) + ' - ' + str( int(son.bins[key].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) plt.tight_layout() elif f_bin in [*list(SP.bins.__dict__.keys())[1:], 'brillance']: plt.figure(figsize=(10, 4)) # Plot every envelop for a single frequency bin norm_factors = np.array([son.bins[f_bin].normalize().norm_factor for son in self.sounds]) for i, son in enumerate(self.sounds): son.bins[f_bin].normalize().old_plot('log envelop', label=(str(i + 1) + '. ' + son.name)) plt.xscale('log') plt.legend() title0 = ' ' + f_bin + ' : ' + str(int(son.bins[f_bin].range[0])) + ' - ' + str( int(son.bins[f_bin].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) else: print('invalid frequency bin') def combine_envelop(self, kind='signal', difference_factor=1, show_sounds=True, show_rejects=True, **kwargs): """ __ Multiple SoundPack Method __ Combines the envelops of the Sounds contained in the SoundPack, Sounds having a too large difference factor from the average are rejected. :param kind: wich signal to use from : 'signal', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' :param difference_factor: threshold to reject a sound from the combinaison, can be adjusted to reject or include more sounds. :param show_sounds: If True all the included Sounds are shown on the plot :param show_rejects: If True all the rejected Sounds are shown on the plot :param kwargs: Key word arguments to pass to the envelop plot. :return: None """ sounds = self.sounds sample_number = np.min([len(s1.signal.log_envelop()[0]) for s1 in sounds]) if kind == 'signal': log_envelops = np.stack([s1.signal.normalize().log_envelop()[0][:sample_number] for s1 in sounds]) elif kind in SP.bins.__dict__.keys(): log_envelops = np.stack([s1.bins[kind].normalize().log_envelop()[0][:sample_number] for s1 in sounds]) else: print('Wrong kind') average_log_envelop = np.mean(log_envelops, axis=0) means = np.tile(average_log_envelop, (len(sounds), 1)) diffs = np.sum(np.abs(means - log_envelops), axis=1) diff = np.mean(diffs) * difference_factor good_sounds = np.array(sounds)[diffs < diff] rejected_sounds = np.array(sounds)[diffs > diff] average_log_envelop = np.mean(log_envelops[diffs < diff], axis=0) norm_factors = np.array([s1.signal.normalize().norm_factor for s1 in good_sounds]) if kind == 'signal': if show_sounds: for s1 in good_sounds[:-1]: s1.signal.normalize().old_plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].signal.normalize().old_plot(kind='log envelop', alpha=0.2, color='k', label='sounds') if show_rejects: if len(rejected_sounds) > 1: for s1 in rejected_sounds[:-1]: s1.signal.normalize().old_plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].signal.normalize().old_plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(rejected_sounds) == 1: rejected_sounds[0].signal.normalize().plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(good_sounds) > 0: if 'label' in kwargs.keys(): plt.plot(good_sounds[0].signal.log_envelop()[1][:len(average_log_envelop)], average_log_envelop, **kwargs) else: plt.plot(good_sounds[0].signal.log_envelop()[1][:len(average_log_envelop)], average_log_envelop, label='average', color='k', **kwargs) else: if show_sounds: for s1 in good_sounds[:-1]: s1.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].bins[kind].normalize().old_plot(kind='log envelop', alpha=0.2, color='k', label='sounds') if show_rejects: if len(rejected_sounds) > 1: for s2 in rejected_sounds[:-1]: s2.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(rejected_sounds) == 1: rejected_sounds.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') plt.plot(good_sounds[0].signal.log_envelop()[1][:sample_number], average_log_envelop, color='k', **kwargs) plt.xlabel('time (s)') plt.ylabel('Amplitude') plt.legend() plt.xscale('log') print('Number of rejected sounds : ' + str(len(rejected_sounds))) print('Number of sounds included : ' + str(len(good_sounds))) print('Maximum normalisation factor : ' + str(np.around(np.max(norm_factors), 0)) + 'x') print('Minimum normalisation factor : ' + str(np.around(np.min(norm_factors), 0)) + 'x') def fundamentals(self): """ __ Multiple Soundpack Method __ Displays the fundamentals of every sound in the SoundPack :return: None """ names = np.array([sound.name for sound in self.sounds]) fundamentals = np.array([np.around(sound.fundamental, 1) for sound in self.sounds]) key = np.argsort(fundamentals) table_data = [names[key], fundamentals[key]] table_data = np.array(table_data).transpose() print(tabulate(table_data, headers=['Name', 'Fundamental (Hz)'])) def integral_plot(self, f_bin='all'): """ Normalized cumulative bin power plot for the frequency bins __ Multiple SoundPack Method __ Plots the cumulative integral plot of specified frequency bins see help(Plot.integral) f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' """ if f_bin == 'all': # create a figure with 6 axes fig, axs = plt.subplots(3, 2, figsize=(12, 12)) axs = axs.reshape(-1) for key, ax in zip(self.bin_strings, axs): plt.sca(ax) norm_factors = np.array([son.bins[key].normalize().norm_factor for son in self.sounds]) for sound in self.sounds: sound.bins[key].plot.integral(label=sound.name) plt.legend() title0 = ' ' + key + ' : ' + str(int(sound.bins[key].range[0])) + ' - ' + str( int(sound.bins[key].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) plt.title(title0 + title1 + title2) plt.tight_layout() elif f_bin in self.bin_strings: fig, ax = plt.subplots(figsize=(6, 4)) plt.sca(ax) norm_factors = np.array([son.bins[f_bin].normalize().norm_factor for son in self.sounds]) for sound in self.sounds: sound.bins[f_bin].plot.integral(label=sound.name) plt.legend() title0 = ' ' + f_bin + ' : ' + str(int(sound.bins[f_bin].range[0])) + ' - ' + str( int(sound.bins[f_bin].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) else: print('invalid frequency bin') def bin_power_table(self): """ Displays a table with the signal power contained in every frequency bin The power is computed as the time integral of the signal """ # Bin power distribution table bin_strings = self.bin_strings integrals = [] # for every sound in the SoundPack for sound in self.sounds: integral = [] # for every frequency bin in the sound for f_bin in bin_strings: log_envelop, log_time = sound.bins[f_bin].normalize().log_envelop() integral.append(scipy.integrate.trapezoid(log_envelop, log_time)) # a list of dict for every sound integrals.append(integral) # make the table table_data = np.array([list(bin_strings), *integrals]).transpose() sound_names = [sound.name for sound in self.sounds] print('___ Signal Power Frequency Bin Distribution ___ \n') print(tabulate(table_data, headers=['bin', *sound_names])) def bin_power_hist(self): """ Histogram of the frequency bin power for multiple sounds frequency bin power is computed as the integral of the bin envelop """ # Compute the bin powers bin_strings = self.bin_strings integrals = [] # for every sound in the SoundPack for sound in self.sounds: integral = [] # for every frequency bin in the sound for f_bin in bin_strings: log_envelop, log_time = sound.bins[f_bin].normalize().log_envelop() integral.append(scipy.integrate.trapezoid(log_envelop, log_time)) # a list of dict for every sound integrals.append(integral) # create the bar plotting vectors fig, ax = plt.subplots(figsize=(6, 6)) # make the bar plot n = len(self.sounds) width = 0.8 / n # get nice colors cmap = matplotlib.cm.get_cmap('Set2') for i, sound in enumerate(self.sounds): x = np.arange(i * width, len(bin_strings) + i * width) y = integrals[i] if n < 8: color = cmap(i) else: color = None if i == n // 2: ax.bar(x, y, width=width, tick_label=list(bin_strings), label=sound.name, color=color) else: ax.bar(x, y, width=width, label=sound.name, color=color) plt.legend() """ Methods for dual SoundPacks """ def compare_peaks(self): """ Plot to compare the FFT peaks values of two sounds __ Dual SoundPack Method __ Compares the peaks in the Fourier Transform of two Sounds, the peak with the highest difference is highlighted """ if self.kind == 'dual': son1 = self.sounds[0] son2 = self.sounds[1] index1 = np.where(son1.signal.fft_frequencies() > self.SP.general.fft_range.value)[0][0] index2 = np.where(son2.signal.fft_frequencies() > self.SP.general.fft_range.value)[0][0] # Get the peak data from the sounds peaks1 = son1.signal.peaks() peaks2 = son2.signal.peaks() freq1 = son1.signal.fft_frequencies()[:index1] freq2 = son2.signal.fft_frequencies()[:index2] fft1 = son1.signal.fft()[:index1] fft2 = son2.signal.fft()[:index2] peak_distance1 = np.mean([freq1[peaks1[i]] - freq1[peaks1[i + 1]] for i in range(len(peaks1) - 1)]) / 4 peak_distance2 = np.mean([freq2[peaks2[i]] - freq2[peaks2[i + 1]] for i in range(len(peaks2) - 1)]) / 4 peak_distance = np.abs(np.mean([peak_distance1, peak_distance2])) # Align the two peak vectors new_peaks1 = [] new_peaks2 = [] for peak1 in peaks1: for peak2 in peaks2: if np.abs(freq1[peak1] - freq2[peak2]) < peak_distance: new_peaks1.append(peak1) new_peaks2.append(peak2) new_peaks1 = np.unique(np.array(new_peaks1)) new_peaks2 = np.unique(np.array(new_peaks2)) different_peaks1 = [] different_peaks2 = [] difference_threshold = 0.5 while len(different_peaks1) < 1: for peak1, peak2 in zip(new_peaks1, new_peaks2): if np.abs(fft1[peak1] - fft2[peak2]) > difference_threshold: different_peaks1.append(peak1) different_peaks2.append(peak2) difference_threshold -= 0.01 # Plot the output plt.figure(figsize=(10, 6)) plt.yscale('symlog', linthresh=10e-1) # Sound 1 plt.plot(freq1, fft1, color='#919191', label=son1.name) plt.scatter(freq1[new_peaks1], fft1[new_peaks1], color='b', label='peaks') plt.scatter(freq1[different_peaks1], fft1[different_peaks1], color='g', label='diff peaks') annotation_string = 'Peaks with ' + str(np.around(difference_threshold, 2)) + ' difference' plt.annotate(annotation_string, (freq1[different_peaks1[0]] + peak_distance / 2, fft1[different_peaks1[0]])) # Sound2 plt.plot(freq2, -fft2, color='#3d3d3d', label=son2.name) plt.scatter(freq2[new_peaks2], -fft2[new_peaks2], color='b') plt.scatter(freq2[different_peaks2], -fft2[different_peaks2], color='g') plt.title('Fourier Transform Peak Analysis for ' + son1.name + ' and ' + son2.name) plt.grid('on') plt.legend() else: print('Unsupported for multiple sounds SoundPacks') def fft_mirror(self): """ Plot the Fourier Transforms of two sounds on opposed axis to compare the spectras __ Dual SoundPack Method __ The fourier transforms are normalized between 0 and [-1, 1], the y scale is logarithmic :return: None """ if self.kind == 'dual': son1 = self.sounds[0] son2 = self.sounds[1] index = np.where(son1.signal.fft_frequencies() > SP.general.fft_range.value)[0][0] plt.figure(figsize=(10, 6)) plt.yscale('symlog') plt.grid('on') plt.plot(son1.signal.fft_frequencies()[:index], son1.signal.fft()[:index], label=son1.name) plt.plot(son2.signal.fft_frequencies()[:index], -son2.signal.fft()[:index], label=son2.name) plt.xlabel('Fréquence (Hz)') plt.ylabel('Amplitude') plt.title('Mirror Fourier Transform for ' + son1.name + ' and ' + son2.name) plt.legend() else: print('Unsupported for multiple sounds SoundPacks') def fft_diff(self, fraction=3, ticks=None): """ Plot the difference between the spectral distribution in the two sounds __ Dual SoundPack Method __ Compare the Fourier Transform of two sounds by computing the differences of the octave bins heights. The two FTs are superimposed on the first plot to show differences The difference between the two FTs is plotted on the second plot :param fraction: octave fraction value used to compute the frequency bins A higher number will show a more precise comparison, but conclusions may be harder to draw. :param ticks: If True the frequency bins intervals are used as X axis ticks :return: None """ if self.kind == 'dual': # Separate the sounds son1 = self.sounds[0] son2 = self.sounds[1] # Compute plotting bins x_values = utils.octave_values(fraction) hist_bins = utils.octave_histogram(fraction) bar_widths = np.array([hist_bins[i + 1] - hist_bins[i] for i in range(0, len(hist_bins) - 1)]) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) plot1 = ax1.hist(son1.signal.fft_bins(), utils.octave_histogram(fraction), color='blue', alpha=0.6, label=son1.name) plot2 = ax1.hist(son2.signal.fft_bins(), utils.octave_histogram(fraction), color='orange', alpha=0.6, label=son2.name) ax1.set_title('FT Histogram for ' + son1.name + ' and ' + son2.name) ax1.set_xscale('log') ax1.set_xlabel('Fréquence (Hz)') ax1.set_ylabel('Amplitude') ax1.grid('on') ax1.legend() diff = plot1[0] - plot2[0] n_index = np.where(diff <= 0)[0] p_index = np.where(diff >= 0)[0] # Negative difference corresponding to sound 2 ax2.bar(x_values[n_index], diff[n_index], width=bar_widths[n_index], color='orange', alpha=0.6) # Positive difference corresponding to sound1 ax2.bar(x_values[p_index], diff[p_index], width=bar_widths[p_index], color='blue', alpha=0.6) ax2.set_title('Difference ' + son1.name + ' - ' + son2.name) ax2.set_xscale('log') ax2.set_xlabel('Fréquence (Hz)') ax2.set_ylabel('<- Son 2 : Son 1 ->') ax2.grid('on') if ticks == 'bins': labels = [label for label in self.SP.bins.__dict__ if label != 'name'] labels.append('brillance') x = [param.value for param in self.SP.bins.__dict__.values() if param != 'bins'] x.append(11250) x_formatter = ticker.FixedFormatter(labels) x_locator = ticker.FixedLocator(x) ax1.xaxis.set_major_locator(x_locator) ax1.xaxis.set_major_formatter(x_formatter) ax1.tick_params(axis="x", labelrotation=90) ax2.xaxis.set_major_locator(x_locator) ax2.xaxis.set_major_formatter(x_formatter) ax2.tick_params(axis="x", labelrotation=90) else: print('Unsupported for multiple sounds SoundPacks') def integral_compare(self, f_bin='all'): """ Cumulative bin envelop integral comparison for two signals __ Dual SoundPack Method __ Plots the cumulative integral plot of specified frequency bins and their difference as surfaces f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' """ # Case when plotting all the frequency bins if f_bin == 'all': fig, axs = plt.subplots(3, 2, figsize=(16, 16)) axs = axs.reshape(-1) self.bin_strings = self.sounds[0].bins.keys() bins1 = self.sounds[0].bins.values() bins2 = self.sounds[1].bins.values() for signal1, signal2, bin_string, ax in zip(bins1, bins2, self.bin_strings, axs): log_envelop1, log_time1 = signal1.normalize().log_envelop() log_envelop2, log_time2 = signal2.normalize().log_envelop() integ = scipy.integrate.trapezoid integral1 = np.array([integ(log_envelop1[:i], log_time1[:i]) for i in np.arange(2, len(log_envelop1), 1)]) integral2 = np.array([integ(log_envelop2[:i], log_time2[:i]) for i in np.arange(2, len(log_envelop2), 1)]) time1 = log_time1[2:len(log_time1):1] time2 = log_time2[2:len(log_time2):1] int_index = np.min([integral1.shape[0], integral2.shape[0]]) ax.fill_between(time1, integral1, label=self.sounds[0].name, alpha=0.4) ax.fill_between(time2, -integral2, label=self.sounds[1].name, alpha=0.4) ax.fill_between(time2, integral1 - integral2, color='g', label='int diff', alpha=0.6) ax.set_xlabel('time (s)') ax.set_ylabel('cummulative power') ax.set_xscale('log') ax.set_title(bin_string) ax.legend() ax.grid('on') plt.tight_layout() elif f_bin in self.bin_strings: fig, ax = plt.subplots(figsize=(8, 6)) signal1 = self.sounds[0].bins[f_bin] signal2 = self.sounds[1].bins[f_bin] log_envelop1, log_time1 = signal1.normalize().log_envelop() log_envelop2, log_time2 = signal2.normalize().log_envelop() integ = scipy.integrate.trapezoid integral1 = np.array([integ(log_envelop1[:i], log_time1[:i]) for i in np.arange(2, len(log_envelop1), 1)]) integral2 = np.array([integ(log_envelop2[:i], log_time2[:i]) for i in np.arange(2, len(log_envelop2), 1)]) time1 = log_time1[2:len(log_time1):1] time2 = log_time2[2:len(log_time2):1] int_index = np.min([integral1.shape[0], integral2.shape[0]]) ax.fill_between(time1, integral1, label=self.sounds[0].name, alpha=0.4) ax.fill_between(time2, -integral2, label=self.sounds[1].name, alpha=0.4) ax.fill_between(time2, integral1 - integral2, color='g', label='int diff', alpha=0.6) ax.set_xlabel('time (s)') ax.set_ylabel('cummulative power') ax.set_xscale('log') ax.set_title(f_bin) ax.legend(loc='upper left') ax.grid('on') else: print('invalid frequency bin') def coherence_plot(self): """ __ Dual SoundPack Method __ computes and plots the coherence between the time signal of two Sounds :return: None """ if self.kind == 'dual': f, C = sig.coherence(self.sounds[0].signal.signal, self.sounds[1].signal.signal, self.sounds[0].signal.sr) plt.plot(f, C, color='b') plt.yscale('log') plt.xlabel('Fréquence (Hz)') plt.ylabel('Coherence [0, 1]') title = 'Cohérence entre les sons ' + self.sounds[0].name + ' et ' + self.sounds[1].name plt.title(title) else: print('Unsupported for multiple sounds SoundPacks')
Methods
def bin_power_hist(self)
-
Histogram of the frequency bin power for multiple sounds
frequency bin power is computed as the integral of the bin envelop
Expand source code
def bin_power_hist(self): """ Histogram of the frequency bin power for multiple sounds frequency bin power is computed as the integral of the bin envelop """ # Compute the bin powers bin_strings = self.bin_strings integrals = [] # for every sound in the SoundPack for sound in self.sounds: integral = [] # for every frequency bin in the sound for f_bin in bin_strings: log_envelop, log_time = sound.bins[f_bin].normalize().log_envelop() integral.append(scipy.integrate.trapezoid(log_envelop, log_time)) # a list of dict for every sound integrals.append(integral) # create the bar plotting vectors fig, ax = plt.subplots(figsize=(6, 6)) # make the bar plot n = len(self.sounds) width = 0.8 / n # get nice colors cmap = matplotlib.cm.get_cmap('Set2') for i, sound in enumerate(self.sounds): x = np.arange(i * width, len(bin_strings) + i * width) y = integrals[i] if n < 8: color = cmap(i) else: color = None if i == n // 2: ax.bar(x, y, width=width, tick_label=list(bin_strings), label=sound.name, color=color) else: ax.bar(x, y, width=width, label=sound.name, color=color) plt.legend()
def bin_power_table(self)
-
Displays a table with the signal power contained in every frequency bin
The power is computed as the time integral of the signal
Expand source code
def bin_power_table(self): """ Displays a table with the signal power contained in every frequency bin The power is computed as the time integral of the signal """ # Bin power distribution table bin_strings = self.bin_strings integrals = [] # for every sound in the SoundPack for sound in self.sounds: integral = [] # for every frequency bin in the sound for f_bin in bin_strings: log_envelop, log_time = sound.bins[f_bin].normalize().log_envelop() integral.append(scipy.integrate.trapezoid(log_envelop, log_time)) # a list of dict for every sound integrals.append(integral) # make the table table_data = np.array([list(bin_strings), *integrals]).transpose() sound_names = [sound.name for sound in self.sounds] print('___ Signal Power Frequency Bin Distribution ___ \n') print(tabulate(table_data, headers=['bin', *sound_names]))
def coherence_plot(self)
-
Dual SoundPack Method computes and plots the coherence between the time signal of two Sounds :return: None
Expand source code
def coherence_plot(self): """ __ Dual SoundPack Method __ computes and plots the coherence between the time signal of two Sounds :return: None """ if self.kind == 'dual': f, C = sig.coherence(self.sounds[0].signal.signal, self.sounds[1].signal.signal, self.sounds[0].signal.sr) plt.plot(f, C, color='b') plt.yscale('log') plt.xlabel('Fréquence (Hz)') plt.ylabel('Coherence [0, 1]') title = 'Cohérence entre les sons ' + self.sounds[0].name + ' et ' + self.sounds[1].name plt.title(title) else: print('Unsupported for multiple sounds SoundPacks')
def combine_envelop(self, kind='signal', difference_factor=1, show_sounds=True, show_rejects=True, **kwargs)
-
Multiple SoundPack Method Combines the envelops of the Sounds contained in the SoundPack, Sounds having a too large difference factor from the average are rejected.
:param kind: wich signal to use from : 'signal', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' :param difference_factor: threshold to reject a sound from the combinaison, can be adjusted to reject or include more sounds. :param show_sounds: If True all the included Sounds are shown on the plot :param show_rejects: If True all the rejected Sounds are shown on the plot :param kwargs: Key word arguments to pass to the envelop plot. :return: None
Expand source code
def combine_envelop(self, kind='signal', difference_factor=1, show_sounds=True, show_rejects=True, **kwargs): """ __ Multiple SoundPack Method __ Combines the envelops of the Sounds contained in the SoundPack, Sounds having a too large difference factor from the average are rejected. :param kind: wich signal to use from : 'signal', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' :param difference_factor: threshold to reject a sound from the combinaison, can be adjusted to reject or include more sounds. :param show_sounds: If True all the included Sounds are shown on the plot :param show_rejects: If True all the rejected Sounds are shown on the plot :param kwargs: Key word arguments to pass to the envelop plot. :return: None """ sounds = self.sounds sample_number = np.min([len(s1.signal.log_envelop()[0]) for s1 in sounds]) if kind == 'signal': log_envelops = np.stack([s1.signal.normalize().log_envelop()[0][:sample_number] for s1 in sounds]) elif kind in SP.bins.__dict__.keys(): log_envelops = np.stack([s1.bins[kind].normalize().log_envelop()[0][:sample_number] for s1 in sounds]) else: print('Wrong kind') average_log_envelop = np.mean(log_envelops, axis=0) means = np.tile(average_log_envelop, (len(sounds), 1)) diffs = np.sum(np.abs(means - log_envelops), axis=1) diff = np.mean(diffs) * difference_factor good_sounds = np.array(sounds)[diffs < diff] rejected_sounds = np.array(sounds)[diffs > diff] average_log_envelop = np.mean(log_envelops[diffs < diff], axis=0) norm_factors = np.array([s1.signal.normalize().norm_factor for s1 in good_sounds]) if kind == 'signal': if show_sounds: for s1 in good_sounds[:-1]: s1.signal.normalize().old_plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].signal.normalize().old_plot(kind='log envelop', alpha=0.2, color='k', label='sounds') if show_rejects: if len(rejected_sounds) > 1: for s1 in rejected_sounds[:-1]: s1.signal.normalize().old_plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].signal.normalize().old_plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(rejected_sounds) == 1: rejected_sounds[0].signal.normalize().plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(good_sounds) > 0: if 'label' in kwargs.keys(): plt.plot(good_sounds[0].signal.log_envelop()[1][:len(average_log_envelop)], average_log_envelop, **kwargs) else: plt.plot(good_sounds[0].signal.log_envelop()[1][:len(average_log_envelop)], average_log_envelop, label='average', color='k', **kwargs) else: if show_sounds: for s1 in good_sounds[:-1]: s1.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].bins[kind].normalize().old_plot(kind='log envelop', alpha=0.2, color='k', label='sounds') if show_rejects: if len(rejected_sounds) > 1: for s2 in rejected_sounds[:-1]: s2.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(rejected_sounds) == 1: rejected_sounds.bins[kind].normalize().old_plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') plt.plot(good_sounds[0].signal.log_envelop()[1][:sample_number], average_log_envelop, color='k', **kwargs) plt.xlabel('time (s)') plt.ylabel('Amplitude') plt.legend() plt.xscale('log') print('Number of rejected sounds : ' + str(len(rejected_sounds))) print('Number of sounds included : ' + str(len(good_sounds))) print('Maximum normalisation factor : ' + str(np.around(np.max(norm_factors), 0)) + 'x') print('Minimum normalisation factor : ' + str(np.around(np.min(norm_factors), 0)) + 'x')
def compare_peaks(self)
-
Plot to compare the FFT peaks values of two sounds
Dual SoundPack Method Compares the peaks in the Fourier Transform of two Sounds, the peak with the highest difference is highlighted
Expand source code
def compare_peaks(self): """ Plot to compare the FFT peaks values of two sounds __ Dual SoundPack Method __ Compares the peaks in the Fourier Transform of two Sounds, the peak with the highest difference is highlighted """ if self.kind == 'dual': son1 = self.sounds[0] son2 = self.sounds[1] index1 = np.where(son1.signal.fft_frequencies() > self.SP.general.fft_range.value)[0][0] index2 = np.where(son2.signal.fft_frequencies() > self.SP.general.fft_range.value)[0][0] # Get the peak data from the sounds peaks1 = son1.signal.peaks() peaks2 = son2.signal.peaks() freq1 = son1.signal.fft_frequencies()[:index1] freq2 = son2.signal.fft_frequencies()[:index2] fft1 = son1.signal.fft()[:index1] fft2 = son2.signal.fft()[:index2] peak_distance1 = np.mean([freq1[peaks1[i]] - freq1[peaks1[i + 1]] for i in range(len(peaks1) - 1)]) / 4 peak_distance2 = np.mean([freq2[peaks2[i]] - freq2[peaks2[i + 1]] for i in range(len(peaks2) - 1)]) / 4 peak_distance = np.abs(np.mean([peak_distance1, peak_distance2])) # Align the two peak vectors new_peaks1 = [] new_peaks2 = [] for peak1 in peaks1: for peak2 in peaks2: if np.abs(freq1[peak1] - freq2[peak2]) < peak_distance: new_peaks1.append(peak1) new_peaks2.append(peak2) new_peaks1 = np.unique(np.array(new_peaks1)) new_peaks2 = np.unique(np.array(new_peaks2)) different_peaks1 = [] different_peaks2 = [] difference_threshold = 0.5 while len(different_peaks1) < 1: for peak1, peak2 in zip(new_peaks1, new_peaks2): if np.abs(fft1[peak1] - fft2[peak2]) > difference_threshold: different_peaks1.append(peak1) different_peaks2.append(peak2) difference_threshold -= 0.01 # Plot the output plt.figure(figsize=(10, 6)) plt.yscale('symlog', linthresh=10e-1) # Sound 1 plt.plot(freq1, fft1, color='#919191', label=son1.name) plt.scatter(freq1[new_peaks1], fft1[new_peaks1], color='b', label='peaks') plt.scatter(freq1[different_peaks1], fft1[different_peaks1], color='g', label='diff peaks') annotation_string = 'Peaks with ' + str(np.around(difference_threshold, 2)) + ' difference' plt.annotate(annotation_string, (freq1[different_peaks1[0]] + peak_distance / 2, fft1[different_peaks1[0]])) # Sound2 plt.plot(freq2, -fft2, color='#3d3d3d', label=son2.name) plt.scatter(freq2[new_peaks2], -fft2[new_peaks2], color='b') plt.scatter(freq2[different_peaks2], -fft2[different_peaks2], color='g') plt.title('Fourier Transform Peak Analysis for ' + son1.name + ' and ' + son2.name) plt.grid('on') plt.legend() else: print('Unsupported for multiple sounds SoundPacks')
def compare_plot(self, kind, **kwargs)
-
Plots all the sounds on different figures to compare them for a specific kind
Multiple SoundPack Method Draws the same kind of plot on a different axis for each sound Example :
SoundPack.compare_plot('peaks')
with 4 Sounds will plot a figure with 4 axes, with each a different 'peak' plot.:param kind: kind argument passed to
Signal.plot()
:param kwargs: key word arguments passed to Signal.plot() :return: NoneExpand source code
def compare_plot(self, kind, **kwargs): """ Plots all the sounds on different figures to compare them for a specific kind __ Multiple SoundPack Method __ Draws the same kind of plot on a different axis for each sound Example : `SoundPack.compare_plot('peaks')` with 4 Sounds will plot a figure with 4 axes, with each a different 'peak' plot. :param kind: kind argument passed to `Signal.plot()` :param kwargs: key word arguments passed to Signal.plot() :return: None """ # if a dual SoundPack : only plot two big plots if self.kind == 'dual': if kind == 'timbre': fig, axs = plt.subplots(1, 2, figsize=(8, 4), subplot_kw={'projection': 'polar'}) for sound, ax in zip(self.sounds, axs): plt.sca(ax) sound.signal.old_plot(kind, **kwargs) ax.set_title(kind + ' ' + sound.name) else: fig, axs = plt.subplots(1, 2, figsize=(12, 4)) for sound, ax in zip(self.sounds, axs): plt.sca(ax) sound.signal.old_plot(kind, **kwargs) ax.set_title(kind + ' ' + sound.name) plt.tight_layout() # If a multiple SoundPack : plot on a grid of axes elif self.kind == 'multiple': # find the n, m values for the subplots line and columns n = len(self.sounds) if n // 4 >= 10: # a lot of sounds cols = 4 elif n // 3 >= 10: # many sounds cols = 3 elif n // 2 <= 4: # a few sounds cols = 2 remainder = n % cols if remainder == 0: rows = n // cols else: rows = n // cols + 1 fig, axs = plt.subplots(rows, cols, figsize=(12, 4 * rows)) axs = axs.reshape(-1) for sound, ax in zip(self.sounds, axs): plt.sca(ax) sound.signal.old_plot(kind, **kwargs) title = ax.get_title() title = sound.name + ' ' + title ax.set_title(title) if remainder != 0: for ax in axs[-(cols - remainder):]: ax.set_axis_off() plt.tight_layout()
def equalize_time(self)
-
Trim the sounds so that they all have the length of the shortest sound, trimming is done at the end. :return: None
Expand source code
def equalize_time(self): """ Trim the sounds so that they all have the length of the shortest sound, trimming is done at the end. :return: None """ trim_index = np.min([len(sound.signal.signal) for sound in self.sounds]) trimmed_sounds = [] for sound in self.sounds: new_sound = sound new_sound.signal = new_sound.signal.trim_time(trim_index / sound.signal.sr) new_sound.bin_divide() trimmed_sounds.append(new_sound) self.sounds = trimmed_sounds
def fft_diff(self, fraction=3, ticks=None)
-
Plot the difference between the spectral distribution in the two sounds
Dual SoundPack Method Compare the Fourier Transform of two sounds by computing the differences of the octave bins heights. The two FTs are superimposed on the first plot to show differences The difference between the two FTs is plotted on the second plot
:param fraction: octave fraction value used to compute the frequency bins A higher number will show a more precise comparison, but conclusions may be harder to draw. :param ticks: If True the frequency bins intervals are used as X axis ticks :return: None
Expand source code
def fft_diff(self, fraction=3, ticks=None): """ Plot the difference between the spectral distribution in the two sounds __ Dual SoundPack Method __ Compare the Fourier Transform of two sounds by computing the differences of the octave bins heights. The two FTs are superimposed on the first plot to show differences The difference between the two FTs is plotted on the second plot :param fraction: octave fraction value used to compute the frequency bins A higher number will show a more precise comparison, but conclusions may be harder to draw. :param ticks: If True the frequency bins intervals are used as X axis ticks :return: None """ if self.kind == 'dual': # Separate the sounds son1 = self.sounds[0] son2 = self.sounds[1] # Compute plotting bins x_values = utils.octave_values(fraction) hist_bins = utils.octave_histogram(fraction) bar_widths = np.array([hist_bins[i + 1] - hist_bins[i] for i in range(0, len(hist_bins) - 1)]) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) plot1 = ax1.hist(son1.signal.fft_bins(), utils.octave_histogram(fraction), color='blue', alpha=0.6, label=son1.name) plot2 = ax1.hist(son2.signal.fft_bins(), utils.octave_histogram(fraction), color='orange', alpha=0.6, label=son2.name) ax1.set_title('FT Histogram for ' + son1.name + ' and ' + son2.name) ax1.set_xscale('log') ax1.set_xlabel('Fréquence (Hz)') ax1.set_ylabel('Amplitude') ax1.grid('on') ax1.legend() diff = plot1[0] - plot2[0] n_index = np.where(diff <= 0)[0] p_index = np.where(diff >= 0)[0] # Negative difference corresponding to sound 2 ax2.bar(x_values[n_index], diff[n_index], width=bar_widths[n_index], color='orange', alpha=0.6) # Positive difference corresponding to sound1 ax2.bar(x_values[p_index], diff[p_index], width=bar_widths[p_index], color='blue', alpha=0.6) ax2.set_title('Difference ' + son1.name + ' - ' + son2.name) ax2.set_xscale('log') ax2.set_xlabel('Fréquence (Hz)') ax2.set_ylabel('<- Son 2 : Son 1 ->') ax2.grid('on') if ticks == 'bins': labels = [label for label in self.SP.bins.__dict__ if label != 'name'] labels.append('brillance') x = [param.value for param in self.SP.bins.__dict__.values() if param != 'bins'] x.append(11250) x_formatter = ticker.FixedFormatter(labels) x_locator = ticker.FixedLocator(x) ax1.xaxis.set_major_locator(x_locator) ax1.xaxis.set_major_formatter(x_formatter) ax1.tick_params(axis="x", labelrotation=90) ax2.xaxis.set_major_locator(x_locator) ax2.xaxis.set_major_formatter(x_formatter) ax2.tick_params(axis="x", labelrotation=90) else: print('Unsupported for multiple sounds SoundPacks')
def fft_mirror(self)
-
Plot the Fourier Transforms of two sounds on opposed axis to compare the spectras
Dual SoundPack Method The fourier transforms are normalized between 0 and [-1, 1], the y scale is logarithmic :return: None
Expand source code
def fft_mirror(self): """ Plot the Fourier Transforms of two sounds on opposed axis to compare the spectras __ Dual SoundPack Method __ The fourier transforms are normalized between 0 and [-1, 1], the y scale is logarithmic :return: None """ if self.kind == 'dual': son1 = self.sounds[0] son2 = self.sounds[1] index = np.where(son1.signal.fft_frequencies() > SP.general.fft_range.value)[0][0] plt.figure(figsize=(10, 6)) plt.yscale('symlog') plt.grid('on') plt.plot(son1.signal.fft_frequencies()[:index], son1.signal.fft()[:index], label=son1.name) plt.plot(son2.signal.fft_frequencies()[:index], -son2.signal.fft()[:index], label=son2.name) plt.xlabel('Fréquence (Hz)') plt.ylabel('Amplitude') plt.title('Mirror Fourier Transform for ' + son1.name + ' and ' + son2.name) plt.legend() else: print('Unsupported for multiple sounds SoundPacks')
def freq_bin_plot(self, f_bin='all')
-
Plots the log envelop of specified frequency bins
Multiple SoundPack Method A function to compare signals decomposed frequency wise in the time domain on a logarithm scale. The methods plots all the sounds and plots their frequency bins according to the frequency bin argument f_bin.
Example : SoundPack.freq_bin_plot(f_bin='mid') will plot the log-scale envelop of the 'mid' signal of every sound in the SoundPack
f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
Expand source code
def freq_bin_plot(self, f_bin='all'): """ Plots the log envelop of specified frequency bins __ Multiple SoundPack Method __ A function to compare signals decomposed frequency wise in the time domain on a logarithm scale. The methods plots all the sounds and plots their frequency bins according to the frequency bin argument f_bin. Example : SoundPack.freq_bin_plot(f_bin='mid') will plot the log-scale envelop of the 'mid' signal of every sound in the SoundPack f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' """ if f_bin == 'all': # Create one plot per bin fig, axs = plt.subplots(3, 2, figsize=(12, 12)) axs = axs.reshape(-1) for key, ax in zip([*list(self.SP.bins.__dict__.keys())[1:], 'brillance'], axs): plt.sca(ax) # plot every sound for a frequency bin norm_factors = np.array([son.bins[key].normalize().norm_factor for son in self.sounds]) for i, son in enumerate(self.sounds): son.bins[key].normalize().old_plot('log envelop', label=son.name) plt.xscale('log') plt.legend() title0 = ' ' + key + ' : ' + str(int(son.bins[key].range[0])) + ' - ' + str( int(son.bins[key].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) plt.tight_layout() elif f_bin in [*list(SP.bins.__dict__.keys())[1:], 'brillance']: plt.figure(figsize=(10, 4)) # Plot every envelop for a single frequency bin norm_factors = np.array([son.bins[f_bin].normalize().norm_factor for son in self.sounds]) for i, son in enumerate(self.sounds): son.bins[f_bin].normalize().old_plot('log envelop', label=(str(i + 1) + '. ' + son.name)) plt.xscale('log') plt.legend() title0 = ' ' + f_bin + ' : ' + str(int(son.bins[f_bin].range[0])) + ' - ' + str( int(son.bins[f_bin].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) else: print('invalid frequency bin')
def fundamentals(self)
-
Multiple Soundpack Method Displays the fundamentals of every sound in the SoundPack :return: None
Expand source code
def fundamentals(self): """ __ Multiple Soundpack Method __ Displays the fundamentals of every sound in the SoundPack :return: None """ names = np.array([sound.name for sound in self.sounds]) fundamentals = np.array([np.around(sound.fundamental, 1) for sound in self.sounds]) key = np.argsort(fundamentals) table_data = [names[key], fundamentals[key]] table_data = np.array(table_data).transpose() print(tabulate(table_data, headers=['Name', 'Fundamental (Hz)']))
def integral_compare(self, f_bin='all')
-
Cumulative bin envelop integral comparison for two signals
Dual SoundPack Method Plots the cumulative integral plot of specified frequency bins and their difference as surfaces
f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
Expand source code
def integral_compare(self, f_bin='all'): """ Cumulative bin envelop integral comparison for two signals __ Dual SoundPack Method __ Plots the cumulative integral plot of specified frequency bins and their difference as surfaces f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' """ # Case when plotting all the frequency bins if f_bin == 'all': fig, axs = plt.subplots(3, 2, figsize=(16, 16)) axs = axs.reshape(-1) self.bin_strings = self.sounds[0].bins.keys() bins1 = self.sounds[0].bins.values() bins2 = self.sounds[1].bins.values() for signal1, signal2, bin_string, ax in zip(bins1, bins2, self.bin_strings, axs): log_envelop1, log_time1 = signal1.normalize().log_envelop() log_envelop2, log_time2 = signal2.normalize().log_envelop() integ = scipy.integrate.trapezoid integral1 = np.array([integ(log_envelop1[:i], log_time1[:i]) for i in np.arange(2, len(log_envelop1), 1)]) integral2 = np.array([integ(log_envelop2[:i], log_time2[:i]) for i in np.arange(2, len(log_envelop2), 1)]) time1 = log_time1[2:len(log_time1):1] time2 = log_time2[2:len(log_time2):1] int_index = np.min([integral1.shape[0], integral2.shape[0]]) ax.fill_between(time1, integral1, label=self.sounds[0].name, alpha=0.4) ax.fill_between(time2, -integral2, label=self.sounds[1].name, alpha=0.4) ax.fill_between(time2, integral1 - integral2, color='g', label='int diff', alpha=0.6) ax.set_xlabel('time (s)') ax.set_ylabel('cummulative power') ax.set_xscale('log') ax.set_title(bin_string) ax.legend() ax.grid('on') plt.tight_layout() elif f_bin in self.bin_strings: fig, ax = plt.subplots(figsize=(8, 6)) signal1 = self.sounds[0].bins[f_bin] signal2 = self.sounds[1].bins[f_bin] log_envelop1, log_time1 = signal1.normalize().log_envelop() log_envelop2, log_time2 = signal2.normalize().log_envelop() integ = scipy.integrate.trapezoid integral1 = np.array([integ(log_envelop1[:i], log_time1[:i]) for i in np.arange(2, len(log_envelop1), 1)]) integral2 = np.array([integ(log_envelop2[:i], log_time2[:i]) for i in np.arange(2, len(log_envelop2), 1)]) time1 = log_time1[2:len(log_time1):1] time2 = log_time2[2:len(log_time2):1] int_index = np.min([integral1.shape[0], integral2.shape[0]]) ax.fill_between(time1, integral1, label=self.sounds[0].name, alpha=0.4) ax.fill_between(time2, -integral2, label=self.sounds[1].name, alpha=0.4) ax.fill_between(time2, integral1 - integral2, color='g', label='int diff', alpha=0.6) ax.set_xlabel('time (s)') ax.set_ylabel('cummulative power') ax.set_xscale('log') ax.set_title(f_bin) ax.legend(loc='upper left') ax.grid('on') else: print('invalid frequency bin')
def integral_plot(self, f_bin='all')
-
Normalized cumulative bin power plot for the frequency bins
Multiple SoundPack Method Plots the cumulative integral plot of specified frequency bins see help(Plot.integral)
f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
Expand source code
def integral_plot(self, f_bin='all'): """ Normalized cumulative bin power plot for the frequency bins __ Multiple SoundPack Method __ Plots the cumulative integral plot of specified frequency bins see help(Plot.integral) f_bin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' """ if f_bin == 'all': # create a figure with 6 axes fig, axs = plt.subplots(3, 2, figsize=(12, 12)) axs = axs.reshape(-1) for key, ax in zip(self.bin_strings, axs): plt.sca(ax) norm_factors = np.array([son.bins[key].normalize().norm_factor for son in self.sounds]) for sound in self.sounds: sound.bins[key].plot.integral(label=sound.name) plt.legend() title0 = ' ' + key + ' : ' + str(int(sound.bins[key].range[0])) + ' - ' + str( int(sound.bins[key].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) plt.title(title0 + title1 + title2) plt.tight_layout() elif f_bin in self.bin_strings: fig, ax = plt.subplots(figsize=(6, 4)) plt.sca(ax) norm_factors = np.array([son.bins[f_bin].normalize().norm_factor for son in self.sounds]) for sound in self.sounds: sound.bins[f_bin].plot.integral(label=sound.name) plt.legend() title0 = ' ' + f_bin + ' : ' + str(int(sound.bins[f_bin].range[0])) + ' - ' + str( int(sound.bins[f_bin].range[1])) + ' Hz, ' title1 = 'Norm. Factors : ' title2 = 'x, '.join(str(np.around(norm_factor, 0)) for norm_factor in norm_factors) plt.title(title0 + title1 + title2) else: print('invalid frequency bin')
def normalize(self)
-
Normalize all the signals in the SoundPack and returns a normaized instance of itself :return: SoundPack with normalized signals
Expand source code
def normalize(self): """ Normalize all the signals in the SoundPack and returns a normaized instance of itself :return: SoundPack with normalized signals """ new_sounds = [] names = [sound.name for sound in self.sounds] fundamentals = [sound.fundamental for sound in self.sounds] for sound in self.sounds: sound.signal = sound.signal.normalize() new_sounds.append(sound) return SoundPack(new_sounds, names=names, fundamentals=fundamentals, SoundParams=self.SP, equalize_time=False)
def plot(self, kind, **kwargs)
-
Superimposed plot of all the sounds on one figure for a specific kind
Multiple SoundPack Method Plots a specific signal.plot for all sounds on the same figure Ex : compare_plot('fft') plots the fft of all sounds on a single figure The color argument is set to none so that the plots have different colors
:param kind: Attribute passed to the
signal.plot()
method :param kwargs: key words arguments to pass to thesignal.plot()
method :return: NoneExpand source code
def plot(self, kind, **kwargs): """ Superimposed plot of all the sounds on one figure for a specific kind __ Multiple SoundPack Method __ Plots a specific signal.plot for all sounds on the same figure Ex : compare_plot('fft') plots the fft of all sounds on a single figure The color argument is set to none so that the plots have different colors :param kind: Attribute passed to the `signal.plot()` method :param kwargs: key words arguments to pass to the `signal.plot()` method :return: None """ plt.figure(figsize=(8, 6)) for sound in self.sounds: kwargs['label'] = sound.name kwargs['color'] = None sound.signal.old_plot(kind, **kwargs) plt.title(kind + ' plot') if kind == 'timbre': plt.legend(bbox_to_anchor=(1.3, 0.9)) else: plt.legend()
def sounds_from_files(self, sound_files, names=None, fundamentals=None)
-
Create Sound class instances and assign them to the SoundPack from a list of files :param sound_files: sound filenames :param names: sound names :param fundamentals: user specified fundamental frequencies :return: None
Expand source code
def sounds_from_files(self, sound_files, names=None, fundamentals=None): """ Create Sound class instances and assign them to the SoundPack from a list of files :param sound_files: sound filenames :param names: sound names :param fundamentals: user specified fundamental frequencies :return: None """ # Make the default name list from sound filenames if none is supplied if (names is None) or (len(names) != len(sound_files)): names = [file[:-4] for file in sound_files] # remove the .wav # If the fundamentals are not supplied or mismatch in number None is used if (fundamentals is None) or (len(fundamentals) != len(sound_files)): fundamentals = len(sound_files) * [None] # Create Sound instances from files self.sounds = [] for file, name, fundamental in zip(sound_files, names, fundamentals): self.sounds.append(Sound(file, name=name, fundamental=fundamental, SoundParams=self.SP).condition(return_self=True))