Module guitarsounds
Expand source code
import librosa
import librosa.display
from soundfile import write
import IPython.display as ipd
import matplotlib
import matplotlib.pyplot as plt
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
"""
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
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
if equalize_time:
self.equalize_time()
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):
"""
__ 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
"""
for sound in self.sounds:
kwargs['label'] = sound.name
kwargs['color'] = None
sound.signal.plot(kind, **kwargs)
plt.legend()
def compare_plot(self, kind, **kwargs):
"""
__ 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':
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
for sound, ax in zip(self.sounds, axs):
plt.sca(ax)
kwargs['label'] = sound.name
sound.signal.plot(kind, **kwargs)
# 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.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, fbin='all'):
"""
__ 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 fbin.
Example : SoundPack.freq_bin_plot(fbin='mid') will plot the log-scale envelop of the 'mid' signal of every
sound in the SoundPack
:param fbin: frequency bins to compare, Supported arguments are :
'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance'
:return: None
"""
if fbin == 'all':
# Create one plot per bin
for key in [*list(self.SP.bins.__dict__.keys())[1:], 'brillance']:
plt.figure(figsize=(10, 8))
# 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):
lab = ' ' + key + ' : ' + str(int(son.bins[key].range[0])) + ' - ' + str(
int(son.bins[key].range[1])) + ' Hz'
son.bins[key].normalize().plot('log envelop', label=(str(i + 1) + '. ' + son.name + lab))
plt.xscale('log')
plt.legend()
title1 = 'Normalisation Factor 1 : ' + str(np.around(norm_factors[0], 0)) + 'x, '
title2 = 'Normalisation Factor 2 : ' + str(np.around(norm_factors[1], 0)) + 'x'
plt.title(title1 + title2)
elif fbin in [*list(SP.bins.__dict__.keys())[1:], 'brillance']:
plt.figure(figsize=(10, 8))
# Plot every envelop for a single frequency bin
norm_factors = np.array([son.bins[fbin].normalize().norm_factor for son in self.sounds])
for i, son in enumerate(self.sounds):
lab = ' ' + fbin + ' : ' + str(int(son.bins[fbin].range[0])) + ' - ' + str(
int(son.bins[fbin].range[1])) + ' Hz'
son.bins[fbin].normalize().plot('log envelop', label=(str(i + 1) + '. ' + son.name + lab))
plt.xscale('log')
plt.legend()
title1 = 'Normalisation Factor 1 : ' + str(np.around(norm_factors[0], 0)) + 'x\n'
title2 = 'Normalisation Factor 2 : ' + str(np.around(norm_factors[1], 0)) + 'x'
plt.title(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().plot(kind='log envelop', alpha=0.2, color='k')
sounds[-1].signal.normalize().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().plot(kind='log envelop', alpha=0.3, color='r')
rejected_sounds[-1].signal.normalize().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().plot(kind='log envelop', alpha=0.2, color='k')
sounds[-1].bins[kind].normalize().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().plot(kind='log envelop', alpha=0.3, color='r')
rejected_sounds[-1].bins[kind].normalize().plot(kind='log envelop', alpha=0.3, color='r',
label='rejected sounds')
if len(rejected_sounds) == 1:
rejected_sounds.bins[kind].normalize().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
"""
print(tabulate([[sound.name, np.around(sound.fundamental, 1)] for sound in self.sounds],
headers=['Name', 'Fundamental (Hz)']))
"""
Methods for dual SoundPacks
"""
def compare_peaks(self):
"""
__ Dual SoundPack Method __
Compares the peaks in the Fourier Transform of two Sounds,
the peak with the highest difference is highlighted
:return: None
"""
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, 8))
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, 1)) + ' 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')
plt.legend()
else:
print('Unsupported for multiple sounds SoundPacks')
def fft_mirror(self):
"""
__ Dual SoundPack Method __
Plot the fourier transforms of two sounds on the y and -y axes to compare them.
: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, 8))
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.legend()
plt.show()
else:
print('Unsupported for multiple sounds SoundPacks')
def fft_diff(self, fraction=3, ticks=None):
"""
__ 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('Histogramme de la FT des deux sons')
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('Différence Son 1 - Son 2')
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 = matplotlib.ticker.FixedFormatter(labels)
x_locator = matplotlib.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 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 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
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.arange(0, len(self.signal) * (1 / self.sr), 1 / self.sr)
def listen(self):
"""Method to listen the sound signal in a Jupyter Notebook"""
file = 'temp.wav'
write(file, self.signal, self.sr)
ipd.display(ipd.Audio(file))
os.remove(file)
# noinspection PyUnresolvedReferences
def plot(self, kind, **kwargs):
"""
General plotting method for the Signal class, supported plots are:
'signal' :
Plots the time varying real signal as amplitude vs time.
'envelop' :
Plots the envelop of the signal as amplitude vs time.
'log envelop' :
Plots the envelop with logarithmic window widths on a logarithmic x axis scale.
'fft' :
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
'fft hist' :
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
'peaks' :
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.
'peak damping' :
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.
'time damping' :
Shows the signal envelop with the fitted negative exponential curve used to determine the
time damping ratio of the signal.
"""
illegal_kwargs = ['max_time', 'n', 'ticks', 'normalize', 'inverse', 'peak_height']
plot_kwargs = {i: kwargs[i] for i in kwargs if i not in illegal_kwargs}
if kind == 'signal':
plt.plot(self.time(), self.signal, alpha=0.6, **plot_kwargs)
plt.xlabel('time (s)')
plt.ylabel('amplitude')
elif kind == 'envelop':
plt.plot(self.envelop_time(), self.envelop(), **plot_kwargs)
plt.xlabel("time (s)")
plt.ylabel("amplitude")
plt.grid('on')
elif kind == 'log envelop':
log_envelop, log_envelop_time = self.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')
elif kind == 'fft':
# find the index corresponding to the fft range
result = np.where(self.fft_frequencies() >= self.SP.general.fft_range.value)[0]
if len(result) == 0:
last_index = -1
else:
last_index = result[0]
plt.plot(self.fft_frequencies()[:last_index], self.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':
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 = matplotlib.ticker.FixedFormatter(labels)
x_locator = matplotlib.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)
elif kind == 'fft hist':
# Histogram of frequency values occurences in octave bins
plt.hist(self.fft_bins(), utils.octave_histogram(self.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':
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 = matplotlib.ticker.FixedFormatter(labels)
x_locator = matplotlib.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)
elif kind == 'peaks':
fft_freqs = self.fft_frequencies()
fft = self.fft()
max_index = np.where(fft_freqs >= self.SP.general.fft_range.value)[0][0]
peak_indexes, height = self.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')
elif kind == 'peak damping':
# Get the damping ration and peak frequencies
if 'inverse' in kwargs.keys() and kwargs['inverse'] is False:
zetas = np.array(self.peak_damping())
ylabel = r'Damping $\zeta$'
else:
zetas = 1 / np.array(self.peak_damping())
ylabel = r'Inverse Damping $1/\zeta$'
peak_freqs = self.fft_frequencies()[self.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':
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 = matplotlib.ticker.FixedFormatter(labels)
x_locator = matplotlib.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)
elif kind == 'time damping':
# 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):
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()
plt.scatter(envelop_time[first_index], envelop[first_index], color='r')
plt.scatter(envelop_time[second_index], envelop[second_index], color='r')
plt.plot(envelop_time[first_index:second_index], np.exp(zeta_omega*envelop_time[first_index:second_index]),
c='b')
self.normalize().plot('envelop', **plot_kwargs)
title = 'Zeta : ' + str(np.around(-zeta_omega/wd, 5)) + ' Fundamental ' + \
str(np.around(self.fundamental(), 0)) + 'Hz'
plt.title(title)
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 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 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()
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 validate_trim(self):
"""
Graphic validation of the `.trim_onset` method.
Used to see if the signal onset was determined accurately
:return: None
"""
if hasattr(self, 'trimmed_signal'):
fig, (ax1, ax2) = plt.subplots(2, figsize=(10, 6))
ax1.plot(self.raw_signal.envelop_time(), self.raw_signal.envelop(), color='k')
ax1.set(title='Old Envelop', xlabel='time', ylabel='amplitude')
ax2.plot(self.trimmed_signal.envelop_time(), self.trimmed_signal.envelop(), color='k')
onset_index = self.trimmed_signal.onset
ax2.scatter(self.trimmed_signal.time()[onset_index], self.trimmed_signal.signal[onset_index], color='r')
ax2.set(title='Trimmed signal', xlabel='time', ylabel='amplitude')
plt.tight_layout()
else:
print('signal was not trimmed')
def validate_noise(self):
"""
Audio validation of the `.filter_noise()` method.
Allows the user to listen to the filtered and unfiltered signals
in a jupyter notebook
:return: None
"""
if hasattr(self, 'trimmed_signal'):
print('not filtered')
self.trimmed_signal.listen()
print('filtered')
self.signal.listen()
else:
print('signal was not filtered')
def listen_freq_bins(self):
"""
Method to listen to all the frequency bins of a sound
in a Jupyter Notebook
:return : None
"""
for key in self.bins.keys():
print(key)
self.bins[key].listen()
def plot_freq_bins(self, bins=None):
"""
Method to plot all the frequency bins of a sound
:param bins: frequency bins as a list to plot on the graph
if none are specified, all the bins are plotted.
"""
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].plot('log envelop', label=lab)
plt.xscale('log')
plt.yscale('log')
plt.legend()
Global variables
var SP
-
Classes
Classes
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 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.arange(0, len(self.signal) * (1 / self.sr), 1 / self.sr) def listen(self): """Method to listen the sound signal in a Jupyter Notebook""" file = 'temp.wav' write(file, self.signal, self.sr) ipd.display(ipd.Audio(file)) os.remove(file) # noinspection PyUnresolvedReferences def plot(self, kind, **kwargs): """ General plotting method for the Signal class, supported plots are: 'signal' : Plots the time varying real signal as amplitude vs time. 'envelop' : Plots the envelop of the signal as amplitude vs time. 'log envelop' : Plots the envelop with logarithmic window widths on a logarithmic x axis scale. 'fft' : 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 'fft hist' : 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 'peaks' : 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. 'peak damping' : 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. 'time damping' : Shows the signal envelop with the fitted negative exponential curve used to determine the time damping ratio of the signal. """ illegal_kwargs = ['max_time', 'n', 'ticks', 'normalize', 'inverse', 'peak_height'] plot_kwargs = {i: kwargs[i] for i in kwargs if i not in illegal_kwargs} if kind == 'signal': plt.plot(self.time(), self.signal, alpha=0.6, **plot_kwargs) plt.xlabel('time (s)') plt.ylabel('amplitude') elif kind == 'envelop': plt.plot(self.envelop_time(), self.envelop(), **plot_kwargs) plt.xlabel("time (s)") plt.ylabel("amplitude") plt.grid('on') elif kind == 'log envelop': log_envelop, log_envelop_time = self.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') elif kind == 'fft': # find the index corresponding to the fft range result = np.where(self.fft_frequencies() >= self.SP.general.fft_range.value)[0] if len(result) == 0: last_index = -1 else: last_index = result[0] plt.plot(self.fft_frequencies()[:last_index], self.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': 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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) elif kind == 'fft hist': # Histogram of frequency values occurences in octave bins plt.hist(self.fft_bins(), utils.octave_histogram(self.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': 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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) elif kind == 'peaks': fft_freqs = self.fft_frequencies() fft = self.fft() max_index = np.where(fft_freqs >= self.SP.general.fft_range.value)[0][0] peak_indexes, height = self.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') elif kind == 'peak damping': # Get the damping ration and peak frequencies if 'inverse' in kwargs.keys() and kwargs['inverse'] is False: zetas = np.array(self.peak_damping()) ylabel = r'Damping $\zeta$' else: zetas = 1 / np.array(self.peak_damping()) ylabel = r'Inverse Damping $1/\zeta$' peak_freqs = self.fft_frequencies()[self.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': 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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) elif kind == 'time damping': # 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): 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() plt.scatter(envelop_time[first_index], envelop[first_index], color='r') plt.scatter(envelop_time[second_index], envelop[second_index], color='r') plt.plot(envelop_time[first_index:second_index], np.exp(zeta_omega*envelop_time[first_index:second_index]), c='b') self.normalize().plot('envelop', **plot_kwargs) title = 'Zeta : ' + str(np.around(-zeta_omega/wd, 5)) + ' Fundamental ' + \ str(np.around(self.fundamental(), 0)) + 'Hz' plt.title(title) 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 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
Expand source code
def listen(self): """Method to listen the sound signal in a Jupyter Notebook""" 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 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 plot(self, kind, **kwargs)
-
General plotting method for the Signal class, supported plots are:
'signal' : Plots the time varying real signal as amplitude vs time.
'envelop' : Plots the envelop of the signal as amplitude vs time.
'log envelop' : Plots the envelop with logarithmic window widths on a logarithmic x axis scale.
'fft' : 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'fft hist' : 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'peaks' : Plots the Fourier Transform of the Signal, with the peaks detected with the
Signal.peaks()
method. Ifpeak_height = True
is supplied in the keyword arguments the computed height threshold is shown on the plot.'peak damping' : 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.
'time damping' : Shows the signal envelop with the fitted negative exponential curve used to determine the time damping ratio of the signal.
Expand source code
def plot(self, kind, **kwargs): """ General plotting method for the Signal class, supported plots are: 'signal' : Plots the time varying real signal as amplitude vs time. 'envelop' : Plots the envelop of the signal as amplitude vs time. 'log envelop' : Plots the envelop with logarithmic window widths on a logarithmic x axis scale. 'fft' : 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 'fft hist' : 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 'peaks' : 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. 'peak damping' : 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. 'time damping' : Shows the signal envelop with the fitted negative exponential curve used to determine the time damping ratio of the signal. """ illegal_kwargs = ['max_time', 'n', 'ticks', 'normalize', 'inverse', 'peak_height'] plot_kwargs = {i: kwargs[i] for i in kwargs if i not in illegal_kwargs} if kind == 'signal': plt.plot(self.time(), self.signal, alpha=0.6, **plot_kwargs) plt.xlabel('time (s)') plt.ylabel('amplitude') elif kind == 'envelop': plt.plot(self.envelop_time(), self.envelop(), **plot_kwargs) plt.xlabel("time (s)") plt.ylabel("amplitude") plt.grid('on') elif kind == 'log envelop': log_envelop, log_envelop_time = self.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') elif kind == 'fft': # find the index corresponding to the fft range result = np.where(self.fft_frequencies() >= self.SP.general.fft_range.value)[0] if len(result) == 0: last_index = -1 else: last_index = result[0] plt.plot(self.fft_frequencies()[:last_index], self.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': 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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) elif kind == 'fft hist': # Histogram of frequency values occurences in octave bins plt.hist(self.fft_bins(), utils.octave_histogram(self.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': 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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) elif kind == 'peaks': fft_freqs = self.fft_frequencies() fft = self.fft() max_index = np.where(fft_freqs >= self.SP.general.fft_range.value)[0][0] peak_indexes, height = self.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') elif kind == 'peak damping': # Get the damping ration and peak frequencies if 'inverse' in kwargs.keys() and kwargs['inverse'] is False: zetas = np.array(self.peak_damping()) ylabel = r'Damping $\zeta$' else: zetas = 1 / np.array(self.peak_damping()) ylabel = r'Inverse Damping $1/\zeta$' peak_freqs = self.fft_frequencies()[self.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': 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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) elif kind == 'time damping': # 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): 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() plt.scatter(envelop_time[first_index], envelop[first_index], color='r') plt.scatter(envelop_time[second_index], envelop[second_index], color='r') plt.plot(envelop_time[first_index:second_index], np.exp(zeta_omega*envelop_time[first_index:second_index]), c='b') self.normalize().plot('envelop', **plot_kwargs) title = 'Zeta : ' + str(np.around(-zeta_omega/wd, 5)) + ' Fundamental ' + \ str(np.around(self.fundamental(), 0)) + 'Hz' plt.title(title)
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 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.arange(0, len(self.signal) * (1 / self.sr), 1 / self.sr)
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() 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 validate_trim(self): """ Graphic validation of the `.trim_onset` method. Used to see if the signal onset was determined accurately :return: None """ if hasattr(self, 'trimmed_signal'): fig, (ax1, ax2) = plt.subplots(2, figsize=(10, 6)) ax1.plot(self.raw_signal.envelop_time(), self.raw_signal.envelop(), color='k') ax1.set(title='Old Envelop', xlabel='time', ylabel='amplitude') ax2.plot(self.trimmed_signal.envelop_time(), self.trimmed_signal.envelop(), color='k') onset_index = self.trimmed_signal.onset ax2.scatter(self.trimmed_signal.time()[onset_index], self.trimmed_signal.signal[onset_index], color='r') ax2.set(title='Trimmed signal', xlabel='time', ylabel='amplitude') plt.tight_layout() else: print('signal was not trimmed') def validate_noise(self): """ Audio validation of the `.filter_noise()` method. Allows the user to listen to the filtered and unfiltered signals in a jupyter notebook :return: None """ if hasattr(self, 'trimmed_signal'): print('not filtered') self.trimmed_signal.listen() print('filtered') self.signal.listen() else: print('signal was not filtered') def listen_freq_bins(self): """ Method to listen to all the frequency bins of a sound in a Jupyter Notebook :return : None """ for key in self.bins.keys(): print(key) self.bins[key].listen() def plot_freq_bins(self, bins=None): """ Method to plot all the frequency bins of a sound :param bins: frequency bins as a list to plot on the graph if none are specified, all the bins are plotted. """ 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].plot('log envelop', label=lab) plt.xscale('log') plt.yscale('log') plt.legend()
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 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() 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 in a Jupyter Notebook :return : None
Expand source code
def listen_freq_bins(self): """ Method to listen to all the frequency bins of a sound in a Jupyter Notebook :return : None """ for key in self.bins.keys(): print(key) self.bins[key].listen()
def plot_freq_bins(self, bins=None)
-
Method to plot all the frequency bins of a sound :param bins: frequency bins as a list to plot on the graph if none are specified, all the bins are plotted.
Expand source code
def plot_freq_bins(self, bins=None): """ Method to plot all the frequency bins of a sound :param bins: frequency bins as a list to plot on the graph if none are specified, all the bins are plotted. """ 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].plot('log envelop', label=lab) plt.xscale('log') plt.yscale('log') plt.legend()
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
def validate_noise(self)
-
Audio validation of the
.filter_noise()
method. Allows the user to listen to the filtered and unfiltered signals in a jupyter notebook :return: NoneExpand source code
def validate_noise(self): """ Audio validation of the `.filter_noise()` method. Allows the user to listen to the filtered and unfiltered signals in a jupyter notebook :return: None """ if hasattr(self, 'trimmed_signal'): print('not filtered') self.trimmed_signal.listen() print('filtered') self.signal.listen() else: print('signal was not filtered')
def validate_trim(self)
-
Graphic validation of the
.trim_onset
method. Used to see if the signal onset was determined accurately :return: NoneExpand source code
def validate_trim(self): """ Graphic validation of the `.trim_onset` method. Used to see if the signal onset was determined accurately :return: None """ if hasattr(self, 'trimmed_signal'): fig, (ax1, ax2) = plt.subplots(2, figsize=(10, 6)) ax1.plot(self.raw_signal.envelop_time(), self.raw_signal.envelop(), color='k') ax1.set(title='Old Envelop', xlabel='time', ylabel='amplitude') ax2.plot(self.trimmed_signal.envelop_time(), self.trimmed_signal.envelop(), color='k') onset_index = self.trimmed_signal.onset ax2.scatter(self.trimmed_signal.time()[onset_index], self.trimmed_signal.signal[onset_index], color='r') ax2.set(title='Trimmed signal', xlabel='time', ylabel='amplitude') plt.tight_layout() else: print('signal was not trimmed')
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 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 if equalize_time: self.equalize_time() 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): """ __ 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 """ for sound in self.sounds: kwargs['label'] = sound.name kwargs['color'] = None sound.signal.plot(kind, **kwargs) plt.legend() def compare_plot(self, kind, **kwargs): """ __ 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': fig, axs = plt.subplots(1, 2, figsize=(12, 6)) for sound, ax in zip(self.sounds, axs): plt.sca(ax) kwargs['label'] = sound.name sound.signal.plot(kind, **kwargs) # 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.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, fbin='all'): """ __ 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 fbin. Example : SoundPack.freq_bin_plot(fbin='mid') will plot the log-scale envelop of the 'mid' signal of every sound in the SoundPack :param fbin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' :return: None """ if fbin == 'all': # Create one plot per bin for key in [*list(self.SP.bins.__dict__.keys())[1:], 'brillance']: plt.figure(figsize=(10, 8)) # 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): lab = ' ' + key + ' : ' + str(int(son.bins[key].range[0])) + ' - ' + str( int(son.bins[key].range[1])) + ' Hz' son.bins[key].normalize().plot('log envelop', label=(str(i + 1) + '. ' + son.name + lab)) plt.xscale('log') plt.legend() title1 = 'Normalisation Factor 1 : ' + str(np.around(norm_factors[0], 0)) + 'x, ' title2 = 'Normalisation Factor 2 : ' + str(np.around(norm_factors[1], 0)) + 'x' plt.title(title1 + title2) elif fbin in [*list(SP.bins.__dict__.keys())[1:], 'brillance']: plt.figure(figsize=(10, 8)) # Plot every envelop for a single frequency bin norm_factors = np.array([son.bins[fbin].normalize().norm_factor for son in self.sounds]) for i, son in enumerate(self.sounds): lab = ' ' + fbin + ' : ' + str(int(son.bins[fbin].range[0])) + ' - ' + str( int(son.bins[fbin].range[1])) + ' Hz' son.bins[fbin].normalize().plot('log envelop', label=(str(i + 1) + '. ' + son.name + lab)) plt.xscale('log') plt.legend() title1 = 'Normalisation Factor 1 : ' + str(np.around(norm_factors[0], 0)) + 'x\n' title2 = 'Normalisation Factor 2 : ' + str(np.around(norm_factors[1], 0)) + 'x' plt.title(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().plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].signal.normalize().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().plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].signal.normalize().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().plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].bins[kind].normalize().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().plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].bins[kind].normalize().plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(rejected_sounds) == 1: rejected_sounds.bins[kind].normalize().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 """ print(tabulate([[sound.name, np.around(sound.fundamental, 1)] for sound in self.sounds], headers=['Name', 'Fundamental (Hz)'])) """ Methods for dual SoundPacks """ def compare_peaks(self): """ __ Dual SoundPack Method __ Compares the peaks in the Fourier Transform of two Sounds, the peak with the highest difference is highlighted :return: None """ 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, 8)) 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, 1)) + ' 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') plt.legend() else: print('Unsupported for multiple sounds SoundPacks') def fft_mirror(self): """ __ Dual SoundPack Method __ Plot the fourier transforms of two sounds on the y and -y axes to compare them. :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, 8)) 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.legend() plt.show() else: print('Unsupported for multiple sounds SoundPacks') def fft_diff(self, fraction=3, ticks=None): """ __ 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('Histogramme de la FT des deux sons') 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('Différence Son 1 - Son 2') 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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 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 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().plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].signal.normalize().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().plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].signal.normalize().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().plot(kind='log envelop', alpha=0.2, color='k') sounds[-1].bins[kind].normalize().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().plot(kind='log envelop', alpha=0.3, color='r') rejected_sounds[-1].bins[kind].normalize().plot(kind='log envelop', alpha=0.3, color='r', label='rejected sounds') if len(rejected_sounds) == 1: rejected_sounds.bins[kind].normalize().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)
-
Dual SoundPack Method Compares the peaks in the Fourier Transform of two Sounds, the peak with the highest difference is highlighted :return: None
Expand source code
def compare_peaks(self): """ __ Dual SoundPack Method __ Compares the peaks in the Fourier Transform of two Sounds, the peak with the highest difference is highlighted :return: None """ 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, 8)) 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, 1)) + ' 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') plt.legend() else: print('Unsupported for multiple sounds SoundPacks')
def compare_plot(self, kind, **kwargs)
-
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): """ __ 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': fig, axs = plt.subplots(1, 2, figsize=(12, 6)) for sound, ax in zip(self.sounds, axs): plt.sca(ax) kwargs['label'] = sound.name sound.signal.plot(kind, **kwargs) # 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.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)
-
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): """ __ 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('Histogramme de la FT des deux sons') 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('Différence Son 1 - Son 2') 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 = matplotlib.ticker.FixedFormatter(labels) x_locator = matplotlib.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)
-
Dual SoundPack Method Plot the fourier transforms of two sounds on the y and -y axes to compare them. :return: None
Expand source code
def fft_mirror(self): """ __ Dual SoundPack Method __ Plot the fourier transforms of two sounds on the y and -y axes to compare them. :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, 8)) 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.legend() plt.show() else: print('Unsupported for multiple sounds SoundPacks')
def freq_bin_plot(self, fbin='all')
-
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 fbin.
Example : SoundPack.freq_bin_plot(fbin='mid') will plot the log-scale envelop of the 'mid' signal of every sound in the SoundPack
:param fbin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' :return: None
Expand source code
def freq_bin_plot(self, fbin='all'): """ __ 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 fbin. Example : SoundPack.freq_bin_plot(fbin='mid') will plot the log-scale envelop of the 'mid' signal of every sound in the SoundPack :param fbin: frequency bins to compare, Supported arguments are : 'all', 'bass', 'mid', 'highmid', 'uppermid', 'presence', 'brillance' :return: None """ if fbin == 'all': # Create one plot per bin for key in [*list(self.SP.bins.__dict__.keys())[1:], 'brillance']: plt.figure(figsize=(10, 8)) # 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): lab = ' ' + key + ' : ' + str(int(son.bins[key].range[0])) + ' - ' + str( int(son.bins[key].range[1])) + ' Hz' son.bins[key].normalize().plot('log envelop', label=(str(i + 1) + '. ' + son.name + lab)) plt.xscale('log') plt.legend() title1 = 'Normalisation Factor 1 : ' + str(np.around(norm_factors[0], 0)) + 'x, ' title2 = 'Normalisation Factor 2 : ' + str(np.around(norm_factors[1], 0)) + 'x' plt.title(title1 + title2) elif fbin in [*list(SP.bins.__dict__.keys())[1:], 'brillance']: plt.figure(figsize=(10, 8)) # Plot every envelop for a single frequency bin norm_factors = np.array([son.bins[fbin].normalize().norm_factor for son in self.sounds]) for i, son in enumerate(self.sounds): lab = ' ' + fbin + ' : ' + str(int(son.bins[fbin].range[0])) + ' - ' + str( int(son.bins[fbin].range[1])) + ' Hz' son.bins[fbin].normalize().plot('log envelop', label=(str(i + 1) + '. ' + son.name + lab)) plt.xscale('log') plt.legend() title1 = 'Normalisation Factor 1 : ' + str(np.around(norm_factors[0], 0)) + 'x\n' title2 = 'Normalisation Factor 2 : ' + str(np.around(norm_factors[1], 0)) + 'x' plt.title(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 """ print(tabulate([[sound.name, np.around(sound.fundamental, 1)] for sound in self.sounds], headers=['Name', 'Fundamental (Hz)']))
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)
-
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): """ __ 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 """ for sound in self.sounds: kwargs['label'] = sound.name kwargs['color'] = None sound.signal.plot(kind, **kwargs) 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))