Skip to content

Commit

Permalink
minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
DrMarc committed Feb 22, 2024
1 parent 8deb4a0 commit b921b5b
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 28 deletions.
2 changes: 1 addition & 1 deletion slab/binaural.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def externalize(self, hrtf=None):
resampled_signal = copy.deepcopy(self)
# if sound and HRTF has different samplerates, resample the sound, apply the HRTF, and resample back:
resampled_signal = resampled_signal.resample(hrtf.data[0].samplerate) # resample to hrtf rate
filt = Filter(10**(h/20), fir=False, samplerate=hrtf.data[0].samplerate)
filt = Filter(10**(h/20), fir='TF', samplerate=hrtf.data[0].samplerate)
filtered_signal = filt.apply(resampled_signal)
filtered_signal = filtered_signal.resample(self.samplerate)
return filtered_signal
Expand Down
55 changes: 33 additions & 22 deletions slab/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def __init__(self, data, samplerate=None):
self.samplerate = data.samplerate
else:
raise TypeError('Cannot initialise Signal with data of class ' + str(data.__class__))
if len(self.data.shape) == 1:
self.data.shape = (len(self.data), 1)
if self.data.ndim == 1:
self.data = self.data[:,numpy.newaxis]
elif self.data.shape[1] > self.data.shape[0]:
if not len(data) == 0: # dont transpose if data is an empty array
self.data = self.data.T
Expand Down Expand Up @@ -326,34 +326,45 @@ def envelope(self, apply_envelope=None, times=None, kind='gain', cutoff=50):

def _get_envelope(self, kind, cutoff):
if scipy is False:
raise ImportError('Calculating envelopes requires scipy.sound.')
else:
envs = numpy.abs(scipy.signal.hilbert(self.data, axis=0))
# 50Hz lowpass filter to remove fine-structure
filt = scipy.signal.firwin(1000, cutoff, pass_zero=True, fs=self.samplerate)
envs = scipy.signal.filtfilt(filt, [1], envs, axis=0)
envs[envs <= 0] = numpy.finfo(float).eps # remove negative values and zeroes
if kind == 'dB':
envs = 20 * numpy.log10(envs) # convert amplitude to dB
elif not kind == 'gain':
raise ValueError('Kind must be either "gain" or "dB"!')
return Signal(envs, samplerate=self.samplerate)
raise ImportError('Calculating envelopes requires scipy.signal.')
envs = numpy.abs(scipy.signal.hilbert(self.data, axis=0))
# 50Hz lowpass filter to remove fine-structure
filt = scipy.signal.firwin(1000, cutoff, pass_zero=True, fs=self.samplerate)
envs = scipy.signal.filtfilt(filt, [1], envs, axis=0)
envs[envs <= 0] = numpy.finfo(float).eps # remove negative values and zeroes
if kind == 'dB':
envs = 20 * numpy.log10(envs) # convert amplitude to dB
elif not kind == 'gain':
raise ValueError('Kind must be either "gain" or "dB"!')
return Signal(envs, samplerate=self.samplerate)

def _apply_envelope(self, envelope, times, kind):
# TODO: write tests for the new options!
if hasattr(envelope, 'data') and hasattr(envelope, 'samplerate'): # it's a child object
envelope = envelope.resample(samplerate=self.samplerate)
elif isinstance(envelope, (list, numpy.ndarray)): # make it a Signal, so we can use .n_samples and .n_channels
envelope = Signal(numpy.array(envelope), samplerate=self.samplerate)
new = copy.deepcopy(self)
if times is None:
times = numpy.linspace(0, 1, len(envelope)) * self.duration
times = numpy.linspace(0, 1, envelope.n_samples) * self.duration
else: # times vector was supplied
if len(times) != len(envelope):
if len(times) != envelope.n_samples:
raise ValueError('Envelope and times need to be of equal length!')
times = numpy.array(times)
times[times > self.duration] = self.duration # clamp between 0 and sound duration
times[times < 0] = 0
# get an envelope value for each sample time
envelope = numpy.interp(self.times, times, numpy.array(envelope))
envelope_interp = numpy.empty((self.n_samples,envelope.n_channels)) # prep array for interpolated env
for i in range(envelope.n_channels): # interp each channel
envelope_interp[:,i] = numpy.interp(self.times, times, envelope[:,i])
if kind == 'dB':
envelope = 10**(envelope/20.) # convert dB to gain factors
new.data *= envelope[:, numpy.newaxis] # multiply
envelope_interp = 10**(envelope_interp/20.) # convert dB to gain factors
if envelope.n_channels == new.n_channels: # corresponding chans -> just multiply
new.data *= envelope_interp
elif envelope.n_channels == 1 and new.n_channels > 1: # env 1 chan, sound multichan -> apply env to each chan
for i in range(new.n_channels):
new.data[:,i] *= envelope_interp[:,0]
else: # or neither (raise error)
raise ValueError('Envelope needs to be 1d or have the same number of channels as the sound!')
return new

def delay(self, duration=1, channel=0, filter_length=2048):
Expand Down Expand Up @@ -411,7 +422,7 @@ def delay(self, duration=1, channel=0, filter_length=2048):
sig_portion = sig[i:i+filter_length]
# sig_portion and tap_weight have the same length, so the valid part of the convolution is just
# one sample, which gets written into the sound at the current index
new.data[i, channel] = numpy.convolve(sig_portion, tap_weight, mode='valid')
new.data[i, channel] = numpy.convolve(sig_portion, tap_weight, mode='valid')[0]
return new

def plot_samples(self, show=True, axis=None):
Expand All @@ -430,7 +441,7 @@ def plot_samples(self, show=True, axis=None):
axis.stem(self.channel(0))
else:
for i in range(self.n_channels):
axis.stem(self.channel(i), label=f'channel {i}', color=f'C{i}')
axis.stem(self.channel(i), label=f'channel {i}', linefmt=f'C{i}')
plt.legend()
axis.set(title='Samples', xlabel='Number', ylabel='Value')
if show:
Expand Down
9 changes: 4 additions & 5 deletions slab/sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class Sound(Signal):
sig.level = 80 # set the level to 80 dB
sig = sig.ramp(duration=0.05) # add a 50 millisecond ramp
sig.spectrum(log_power=True) # plot the spectrum
sig. waveform() # plot the time courses
sig.waveform() # plot the time courses
"""

def _get_level(self):
Expand Down Expand Up @@ -331,12 +331,11 @@ def whitenoise(duration=1.0, samplerate=None, level=None, n_channels=1):
@staticmethod
def powerlawnoise(duration=1.0, alpha=1, samplerate=None, level=None, n_channels=1):
"""
Generate a power-law noise with a spectral density per unit of bandwidth scales as 1/(f**alpha).
Generate a power-law noise where the spectral density per unit of bandwidth scales as 1/(f**alpha).
Arguments:
duration (float | int): duration of the sound in seconds (given a float) or in samples (given an int).
alpha (int) : power law exponent.
samplerate: output samplerate
samplerate (int | None): the samplerate of the sound. If None, use the default samplerate.
level (None | int | float | list): the sounds level in decibel. For a multichannel sound, a list of values
can be provided to set the level of each channel individually. If None, the level is set to the default
Expand Down Expand Up @@ -386,7 +385,7 @@ def powerlawnoise(duration=1.0, alpha=1, samplerate=None, level=None, n_channels
@staticmethod
def pinknoise(duration=1.0, samplerate=None, level=None, n_channels=1):
"""
Generate pink noise (power law noise with exponent alpha==1. This is simply a wrapper for calling
Generate pink noise (power law noise with exponent alpha==1). This is simply a wrapper for calling
the `powerlawnoise` method.
Arguments:
Expand Down Expand Up @@ -1078,7 +1077,7 @@ def spectrogram(self, window_dur=0.005, dyn_range=120, upper_frequency=None, oth
def cochleagram(self, bandwidth=1 / 5, n_bands=None, show=True, axis=None):
"""
Computes a cochleagram of the sound by filtering with a bank of cosine-shaped filters
and applying a cube-root compression to the resulting envelopes. The number of bands
and applying a cube-root compression to the resulting envelopes. The number of bands
is either calculated based on the desired `bandwidth` or specified by the `n_bands`
argument.
Expand Down

0 comments on commit b921b5b

Please sign in to comment.