From b5b11cfcd9ed0cc314ada6e266ef18df2b33dfb3 Mon Sep 17 00:00:00 2001 From: Ole Bialas <38684453+OleBialas@users.noreply.github.com> Date: Fri, 28 May 2021 22:37:15 +0200 Subject: [PATCH] kwargs & bugs (#40) * exclude dynamicripple from pytest * test workflow * add flake8 config file * exclude soundfile from flake8 * exclude soundcard from flake8 * ignore soundcard for pytest * ignore soundfile for pytest * fix bu in test routine * don't check sound level * add testing for filters * change filter documentation * import re * change filters to filter in documentation * change f_lower to low_cutoff * remove brownnoise * ignore build * adding filter doc * ignore build * ignore build * ignore build * removing _build folder * convert tabs to spaces * debug calibrate * remove time_windows from test * remove matlabdoc builder * add matlabdoc builder to requirements * remove matlabdoc * resolve some warnings * syntax fixes * . * test recording * add play method * do test * move test folder one level up * addplotting code in the .rst file * one mire plotting example * add filter docu * add example for equalization filter * fix plotting * fix plotting * remove pyplots folder * debug plotting * ignoring cached scripts * simplify plot_tf * change header * remake intro * add reference for spectral features * move audiogram to examples * add some references * add fft filter test * chnage header * merge * fix reference * add psychoacoustics test * integrate mmn in Trialsequence class * for mmn sequence n_reps = n_trials * condition numbers start at 1 * condtions start at 1 * rename function to oddball, encode deviant as 0 * change oddball to deviant_indices * deviants should work now * make random permutation if n_conds = 1 * give correct trialnr * write response in list of lists * make list of None elementrs fro response * make list of None for response * add deviants to n conditions * fix bug * trial sequence should always be a list * dont insert empty array to avoid warning * add test routines * more tests * fix bug when loading sequence * fix import order * copy _init_ from upstream * all functions except curses now tested * fix syntax error * use uniform syntax for plotting * add hrtf tests * add h5netcdf * add SoundFile * add sudo apt-get install sndfile-tools * fix syntax * separate system and python dependencies * fix syntax error * return condition in __next__ * fix bug in deviant list generation * in self.data change None to -999 to avoid error when writing json files * make triallist python int * syntax error * return 0 in __next__() when trial is deviant * resolve type error * never modify anything in place, remove copy_channel * fix syntax error * remove copychannel, add am method * changing doku * overhaul of the psychoacoustics documentation * fix flake8 error * fix syntax error * fix assertion error * remove redundant function * writing stuff * add bibliography * change paper * new paper version * new unittests for Trialsequence + debugging * debug variable name * convert trials to list * raise error when condition is a string but not a file * add method for saving and loading pickle files and rename to LoadSave_mixin * when writing JSON, catch value AND type errors * when making a Trialsequence instance from a file, try to load JSON, catch exception and then load pickle * catch JSONDecodeError when loading trialsequence from file * JSON decode error is part of json module * fix indentation error * change class names and add docstrings * rename classes according to python conventions * removing attributes this_trial_n and this_rep_n from trialsequence * adding docstrings * add docstrings * adding docstrings * docstrings * move simulate_response to mixin * refactoring docstrings * debug test_psychoacoustics * avoid endless loop when computing deviant indices * enable plotting to axis when plotting an "image" * change plotting routine to avoid timeout when pytesting * add docstrings * change nchannels to n_channels and nsamples to n_samples * change Signal.resize to return a new instance * changed the crossfade function to deal with multiple inputs (still a BUG) * dont transpose array if data array is empty * add unit test for crossfading * docstrings * add test for frames and frametimes * finished docstring overhaul * add tests for signal.py * add test routine for generating and reading/writing sound * add test for power law noise * add tests for binaural cues * pack get and set envelope into one function * change envelope and itd/ild to get/set scheme * add docstrings for binaural generator functions * adding tests for band filters * finish filter class refractor * dont use unittest class * debug test_tone * add type in docstring * negative ild means sound to the left * add test for loading/saving filters * add test routines for externalize and interaural level spectrum * add the interaural level spectrum file to gitignore * add test for making HRTF instances * overhaul hrtf docstrings & tests * resolve some TODOs * remove normalise argument * debuging tests * debug endless loop * debig test_envelope * debug assertion errors * debug assertion errors * change filter n_samples * debug assertion error * renamed the n_ parameters * debug sound docs * implement writing objects to resultsfile * debugging docs * debug save results file * add import statements to docs * debug tests * debug test frames * add default level, changel handling of default sampling rate * hide default level and samplerate variables * remove conditions from setup dependencies * remove level argument from all functions * add SoudCard * remove soundfile * remove soundcard from setup.py * install pulseaudio * debug * remove SounCard from pipeline * debug test * debug * debug get_future_trial * improve documentation * uninstall soundcard after setup.py * add soundcard to requirements * say yes to pip uninstall * fix some warnings from pytest * add error message if libsndfile is not installed * add data_path function * change usage of data_path in docs * Update paper.bib * Update paper.bib * dont print every iteration in Trialsequence.transitions * some small fixes in psychoacoustics docs * catch h5py warning when loading sofa file * change docstrings for proper sphinx rendering * add level argument to generator methods * add kemar * remove all plot kwargs and add kwargs to spectrogram * debug binaural.externalize * debug hrtf plot_tf * make assert condition more liberal Co-authored-by: Marc --- slab/binaural.py | 2 +- slab/filter.py | 5 ++--- slab/hrtf.py | 4 ++-- slab/psychoacoustics.py | 10 ++++------ slab/signal.py | 7 +++---- slab/sound.py | 34 +++++++++++++++++----------------- tests/test_binaural.py | 2 +- 7 files changed, 30 insertions(+), 34 deletions(-) diff --git a/slab/binaural.py b/slab/binaural.py index 5c4e5a5..65f4286 100644 --- a/slab/binaural.py +++ b/slab/binaural.py @@ -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: diff --git a/slab/filter.py b/slab/filter.py index a4643b5..8a4c75d 100644 --- a/slab/filter.py +++ b/slab/filter.py @@ -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. @@ -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`. @@ -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: diff --git a/slab/hrtf.py b/slab/hrtf.py index 0c19b84..b93595c 100644 --- a/slab/hrtf.py +++ b/slab/hrtf.py @@ -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 @@ -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) diff --git a/slab/psychoacoustics.py b/slab/psychoacoustics.py index a0044a1..54355b8 100644 --- a/slab/psychoacoustics.py +++ b/slab/psychoacoustics.py @@ -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() @@ -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!') @@ -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) diff --git a/slab/signal.py b/slab/signal.py index 4724252..5104340 100644 --- a/slab/signal.py +++ b/slab/signal.py @@ -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: diff --git a/slab/sound.py b/slab/sound.py index 6c42fae..5f0f006 100644 --- a/slab/sound.py +++ b/slab/sound.py @@ -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. @@ -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.') @@ -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: @@ -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. @@ -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.') @@ -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. @@ -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. @@ -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. @@ -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 @@ -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) diff --git a/tests/test_binaural.py b/tests/test_binaural.py index 9785937..1700661 100644 --- a/tests/test_binaural.py +++ b/tests/test_binaural.py @@ -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():