Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/DrMarc/slab
Browse files Browse the repository at this point in the history
  • Loading branch information
DrMarc committed Jun 1, 2021
2 parents 38f7be5 + b5b11cf commit df7a3b2
Show file tree
Hide file tree
Showing 7 changed files with 30 additions and 34 deletions.
2 changes: 1 addition & 1 deletion slab/binaural.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def externalize(self, hrtf=None):
idx_frontal = numpy.where((hrtf.sources[:, 1] == 0) & (hrtf.sources[:, 0] == 0))[0][0]
if not idx_frontal.size: # idx_frontal is empty
raise ValueError('No frontal direction [0,0] found in HRTF.')
_, h = hrtf.data[idx_frontal].tf(channels=0, nbins=12, show=False) # get low-res version of HRTF spectrum
_, h = hrtf.data[idx_frontal].tf(channels=0, n_bins=12, show=False) # get low-res version of HRTF spectrum
h[0] = 1 # avoids low-freq attenuation in KEMAR HRTF (unproblematic for other HRTFs)
resampled_signal = copy.deepcopy(self)
# if sound and HRTF has different samplerates, resample the sound, apply the HRTF, and resample back:
Expand Down
5 changes: 2 additions & 3 deletions slab/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def apply(self, sig):
'Number of filters must equal number of sound channels, or either one of them must be equal to 1.')
return out

def tf(self, channels='all', n_bins=None, show=True, axis=None, **kwargs):
def tf(self, channels='all', n_bins=None, show=True, axis=None):
"""
Compute a filter's transfer function (magnitude over frequency) and optionally plot it.
Expand All @@ -201,7 +201,6 @@ def tf(self, channels='all', n_bins=None, show=True, axis=None, **kwargs):
If None, use the maximum number of bins.
show (bool): whether to show the plot right after drawing.
axis (matplotlib.axes.Axes | None): axis to plot to. If None create a new plot.
** kwargs: keyword arguments for the plot, see documentation of matplotlib.pyplot.plot for details.
Returns:
(numpy.ndarray): the frequency bins in the range from 0 Hz to the Nyquist frequency.
(numpy.ndarray: the magnitude of each frequency in `w`.
Expand Down Expand Up @@ -253,7 +252,7 @@ def tf(self, channels='all', n_bins=None, show=True, axis=None, **kwargs):
raise ImportError('Plotting transfer functions requires matplotlib.')
if axis is None:
_, axis = plt.subplots()
axis.plot(w, h, **kwargs)
axis.plot(w, h)
axis.set(title='Frequency [Hz]', xlabel='Amplitude [dB]', ylabel='Frequency Response')
axis.grid(True)
if show:
Expand Down
4 changes: 2 additions & 2 deletions slab/hrtf.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def plot_tf(self, sourceidx, ear='left', xlim=(1000, 18000), n_bins=None, kind='
vlines = numpy.arange(0, len(sourceidx)) * linesep
for idx, s in enumerate(sourceidx):
filt = self.data[s]
freqs, h = filt.tf(channels=chan, nbins=n_bins, show=False)
freqs, h = filt.tf(channels=chan, n_bins=n_bins, show=False)
axis.plot(freqs, h + vlines[idx],
linewidth=0.75, color='0.0', alpha=0.7)
ticks = vlines[::3] # plot every third elevation
Expand All @@ -329,7 +329,7 @@ def plot_tf(self, sourceidx, ear='left', xlim=(1000, 18000), n_bins=None, kind='
elevations = self.sources[sourceidx, 1]
for idx, source in enumerate(sourceidx):
filt = self.data[source]
freqs, h = filt.tf(channels=chan, nbins=n_bins, show=False)
freqs, h = filt.tf(channels=chan, n_bins=n_bins, show=False)
img[:, idx] = h.flatten()
img[img < -25] = -25 # clip at -40 dB transfer
contour = axis.contourf(freqs, elevations, img.T, cmap='hot', origin='upper', levels=20)
Expand Down
10 changes: 4 additions & 6 deletions slab/psychoacoustics.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,20 +595,19 @@ def response_summary(self):
else:
return None

def plot(self, axis=None, show=True, **kwargs):
def plot(self, axis=None, show=True):
"""
Plot the trial sequence as scatter plot.
Arguments:
axis (matplotlib.pyplot.Axes): plot axis to draw on, if none a new plot is generated
show (bool): show the plot immediately, defaults to True
**kwargs: pyplot.plot keyword arguments, see matplotlib documentation
"""
if plt is None:
raise ImportError('Plotting requires matplotlib!')
if axis is None:
axis = plt.subplot()
axis.scatter(range(self.n_trials), self.trials, **kwargs)
axis.scatter(range(self.n_trials), self.trials)
axis.set(title='Trial sequence', xlabel='Trials', ylabel='Condition index')
if show:
plt.show()
Expand Down Expand Up @@ -864,14 +863,13 @@ def save_csv(self, filename):
f.write(responses)
return True

def plot(self, axis=None, show=True, **kwargs):
def plot(self, axis=None, show=True):
"""
Plot the staircase. If called after each trial, one plot is created and updated.
Arguments:
axis (matplotlib.pyplot.Axes): plot axis to draw on, if none a new plot is generated
show (bool): whether to show the plot right after drawing.
**kwargs: pyplot.plot keyword arguments, see matplotlib documentation.
"""
if plt is None:
raise ImportError('Plotting requires matplotlib!')
Expand All @@ -883,7 +881,7 @@ def plot(self, axis=None, show=True, **kwargs):
fig = plt.figure('stairs') # figure 'stairs' is created or made current
axis = fig.gca()
axis.clear()
axis.plot(x, y, **kwargs)
axis.plot(x, y)
axis.set_xlim(-self.n_pretrials, max(20, (self.this_trial_n + 15)//10*10))
axis.set_ylim(min(0, min(y)) if self.min_val == -numpy.Inf else self.min_val,
max(y) if self.max_val == numpy.Inf else self.max_val)
Expand Down
7 changes: 3 additions & 4 deletions slab/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,24 +403,23 @@ def delay(self, duration=1, channel=0, filter_length=2048):
new.data[i, channel] = numpy.convolve(sig_portion, tap_weight, mode='valid')
return new

def plot_samples(self, show=True, axis=None, **kwargs):
def plot_samples(self, show=True, axis=None):
"""
Stem plot of the samples of the signal.
Arguments:
show (bool): whether to show the plot right after drawing.
axis (matplotlib.axes.Axes | None): axis to plot to. If None create a new plot.
** kwargs: keyword arguments for the plot, see documentation of matplotlib.pyplot.plot for details.
"""
if matplotlib is False:
raise ImportError('Plotting signals requires matplotlib.')
if axis is None:
_, axis = plt.subplots()
if self.n_channels == 1:
axis.stem(self.channel(0), **kwargs)
axis.stem(self.channel(0))
else:
for i in range(self.n_channels):
axis.stem(self.channel(i), label=f'channel {i}', **kwargs)
axis.stem(self.channel(i), label=f'channel {i}')
plt.legend()
axis.set(title='Samples', xlabel='Number', ylabel='Value')
if show:
Expand Down
34 changes: 17 additions & 17 deletions slab/sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ def play_file(filename):
'Playing from files on Linux without SoundCard module requires SoX. '
'Install: sudo apt-get install sox libsox-fmt-all or pip install SoundCard')

def waveform(self, start=0, end=None, show=True, axis=None, **kwargs):
def waveform(self, start=0, end=None, show=True, axis=None):
"""
Plot the waveform of the sound.
Expand All @@ -934,7 +934,6 @@ def waveform(self, start=0, end=None, show=True, axis=None, **kwargs):
end (int | float | None): the end of the plot in seconds (float) or samples (int), defaults to None.
show (bool): whether to show the plot right after drawing.
axis (matplotlib.axes.Axes | None): axis to plot to. If None create a new plot.
** kwargs: keyword arguments for the plot, see documentation of matplotlib.pyplot.plot for details.
"""
if matplotlib is False:
raise ImportError('Plotting waveforms requires matplotlib.')
Expand All @@ -945,14 +944,14 @@ def waveform(self, start=0, end=None, show=True, axis=None, **kwargs):
if axis is None:
_, axis = plt.subplots()
if self.n_channels == 1:
axis.plot(self.times[start:end], self.channel(0)[start:end], **kwargs)
axis.plot(self.times[start:end], self.channel(0)[start:end])
elif self.n_channels == 2:
axis.plot(self.times[start:end], self.channel(0)[start:end], label='left', **kwargs)
axis.plot(self.times[start:end], self.channel(1)[start:end], label='right', **kwargs)
axis.plot(self.times[start:end], self.channel(0)[start:end], label='left')
axis.plot(self.times[start:end], self.channel(1)[start:end], label='right')
axis.legend()
else:
for i in range(self.n_channels):
axis.plot(self.times[start:end], self.channel(i)[start:end], label=f'channel {i}', **kwargs)
axis.plot(self.times[start:end], self.channel(i)[start:end], label=f'channel {i}')
plt.legend()
axis.set(title='Waveform', xlabel='Time [sec]', ylabel='Amplitude')
if show:
Expand All @@ -971,7 +970,7 @@ def spectrogram(self, window_dur=0.005, dyn_range=120, upper_frequency=None, oth
show (bool): whether to show the plot right after drawing. Note that if show is False and no `axis` is
passed, no plot will be created.
axis (matplotlib.axes.Axes | None): axis to plot to. If None create a new plot.
**kwargs: keyword arguments for the plot. See documentation for matplotlib.pyplot.imshow.
**kwargs: keyword arguments for computing the spectrogram. See documentation for scipy.signal.spectrogram.
Returns:
(None | tuple): If `show == True` or an axis was passed, a plot is drawn and nothing is returned. Else,
a tuple is returned which contains frequencies, time bins and a 2D array of powers.
Expand All @@ -993,10 +992,13 @@ def spectrogram(self, window_dur=0.005, dyn_range=120, upper_frequency=None, oth
window = scipy.signal.windows.gaussian(window_n_samples, window_sigma)
# convert step size into number of overlapping samples in adjacent analysis frames
n_overlap = window_n_samples - step_n_samples
# compute the power spectral density
freqs, times, power = scipy.signal.spectrogram(
x, mode='psd', fs=self.samplerate, scaling='density', noverlap=n_overlap, window=window,
nperseg=window_n_samples)
# compute the power spectral density, use default configuration if no values were specified
kwargs.setdefault("mode", "psd")
kwargs.setdefault("scaling", "density")
kwargs.setdefault("noverlap", n_overlap)
kwargs.setdefault("window", window)
kwargs.setdefault("nperseg", window_n_samples)
freqs, times, power = scipy.signal.spectrogram(x, fs=self.samplerate, **kwargs)
if show or (axis is not None):
if matplotlib is False:
raise ImportError('Ploting spectrograms requires matplotlib.')
Expand All @@ -1011,14 +1013,14 @@ def spectrogram(self, window_dur=0.005, dyn_range=120, upper_frequency=None, oth
if axis is None:
_, axis = plt.subplots()
axis.imshow(power, origin='lower', aspect='auto',
cmap=cmap, extent=extent, vmin=vmin, vmax=None, **kwargs)
cmap=cmap, extent=extent, vmin=vmin, vmax=None)
axis.set(title='Spectrogram', xlabel='Time [sec]', ylabel='Frequency [Hz]')
if show:
plt.show()
else:
return freqs, times, power

def cochleagram(self, bandwidth=1 / 5, show=True, axis=None, **kwargs):
def cochleagram(self, bandwidth=1 / 5, show=True, axis=None):
"""
Computes a cochleagram of the sound by filtering with a bank of cosine-shaped filters with given bandwidth
and applying a cube-root compression to the resulting envelopes.
Expand All @@ -1028,7 +1030,6 @@ def cochleagram(self, bandwidth=1 / 5, show=True, axis=None, **kwargs):
show (bool): whether to show the plot right after drawing. Note that if show is False and no `axis` is
passed, no plot will be created
axis (matplotlib.axes.Axes | None): axis to plot to. If None create a new plot.
**kwargs: keyword arguments for the plot. See documentation for matplotlib.pyplot.imshow.
Returns:
(None | numpy.ndarray): If `show == True` or an axis was passed, a plot is drawn and nothing is returned.
Else, an array with the envelope is returned.
Expand Down Expand Up @@ -1057,7 +1058,7 @@ def cochleagram(self, bandwidth=1 / 5, show=True, axis=None, **kwargs):
else:
return envs

def spectrum(self, low_cutoff=16, high_cutoff=None, log_power=True, axis=None, show=True, **kwargs):
def spectrum(self, low_cutoff=16, high_cutoff=None, log_power=True, axis=None, show=True):
"""
Compute the spectrum of the sound.
Expand All @@ -1069,7 +1070,6 @@ def spectrum(self, low_cutoff=16, high_cutoff=None, log_power=True, axis=None, s
show (bool): whether to show the plot right after drawing. Note that if show is False and no `axis` is
passed, no plot will be created.
axis (matplotlib.axes.Axes | None): axis to plot to. If None create a new plot.
**kwargs: keyword arguments for the plot. see documentation for matplotlib.pyplot.semilogx.
Returns:
If show=False, returns `Z, freqs`, where `Z` is a 1D array of powers
and `freqs` are the corresponding frequencies.n
Expand Down Expand Up @@ -1100,7 +1100,7 @@ def spectrum(self, low_cutoff=16, high_cutoff=None, log_power=True, axis=None, s
raise ImportError('Plotting spectra requires matplotlib.')
if axis is None:
_, axis = plt.subplots()
axis.semilogx(freqs, Z, **kwargs)
axis.semilogx(freqs, Z)
ticks_freqs = numpy.round(32000 * 2 **
(numpy.arange(12, dtype=float) * -1))
axis.set_xticks(ticks_freqs)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_binaural.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def test_externalize():
filtered = hrtf.data[idx_frontal].apply(sound)
external = sound.externalize()
assert numpy.abs(filtered.data-external.data).sum() < numpy.abs(filtered.data-sound.data).sum()
assert numpy.abs(sound.level - external.level).max() < 0.25
assert numpy.abs(sound.level - external.level).max() < 0.6


def test_interaural_level_spectrum():
Expand Down

0 comments on commit df7a3b2

Please sign in to comment.