Skip to content

Commit 991b74f

Browse files
committed
new .name attribute
1 parent 0095d80 commit 991b74f

File tree

6 files changed

+150
-81
lines changed

6 files changed

+150
-81
lines changed

slab/binaural.py

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ class Binaural(Sound):
1818
data (slab.Signal | numpy.ndarray | list | str): see documentation of slab.Sound for details. the `data` must
1919
have either one or two channels. If it has one, that channel is duplicated
2020
samplerate (int): samplerate in Hz, must only be specified when creating an instance from an array.
21+
name (str): A string label for the Sound object. The inbuilt sound generating functions will automatically
22+
set .name to the name of the method used. Useful for logging during experiments.
23+
2124
Attributes:
2225
.left: the first data channel, containing the sound for the left ear.
2326
.right: the second data channel, containing the sound for the right ear
2427
.data: the data-array of the Sound object which has the shape `n_samples` x `n_channels`.
2528
.n_channels: the number of channels in `data`. Must be 2 for a binaural sound.
2629
.n_samples: the number of samples in `data`. Equals `duration` * `samplerate`.
2730
.duration: the duration of the sound in seconds. Equals `n_samples` / `samplerate`.
28-
"""
31+
.name: string label of the sound.
32+
"""
2933
# instance properties
3034
def _set_left(self, other):
3135
if hasattr(other, 'samplerate'): # probably an slab object
@@ -44,33 +48,34 @@ def _set_right(self, other):
4448
right = property(fget=lambda self: Sound(self.channel(1)), fset=_set_right,
4549
doc='The right channel for a stereo sound.')
4650

47-
def __init__(self, data, samplerate=None):
51+
def __init__(self, data, samplerate=None, name='unnamed'):
4852
if isinstance(data, (Sound, Signal)):
53+
self.name = data.name
4954
if data.n_channels == 1: # if there is only one channel, duplicate it.
5055
self.data = numpy.tile(data.data, 2)
5156
elif data.n_channels == 2:
5257
self.data = data.data
5358
else:
5459
raise ValueError("Data must have one or two channel!")
5560
self.samplerate = data.samplerate
56-
elif isinstance(data, (list, tuple)):
61+
elif isinstance(data, (list, tuple)): # list of Sounds
5762
if isinstance(data[0], (Sound, Signal)):
5863
if data[0].n_samples != data[1].n_samples:
5964
raise ValueError('Sounds must have same number of samples!')
6065
if data[0].samplerate != data[1].samplerate:
6166
raise ValueError('Sounds must have same samplerate!')
62-
super().__init__([data[0].data[:, 0], data[1].data[:, 0]], data[0].samplerate)
63-
else:
64-
super().__init__(data, samplerate)
65-
elif isinstance(data, str):
67+
super().__init__([data[0].data[:, 0], data[1].data[:, 0]], data[0].samplerate, name=data[0].name)
68+
else: # list of samples
69+
super().__init__(data, samplerate, name=name)
70+
elif isinstance(data, str): # file name
6671
super().__init__(data, samplerate)
6772
if self.n_channels == 1:
6873
self.data = numpy.tile(self.data, 2) # duplicate channel if monaural file
69-
else:
70-
super().__init__(data, samplerate)
74+
else: # anything but Sound, list, or file name
75+
super().__init__(data, samplerate, name=name)
7176
if self.n_channels == 1:
7277
self.data = numpy.tile(self.data, 2) # duplicate channel if monaural file
73-
if self.n_channels != 2:
78+
if self.n_channels != 2: # bail if unable to enforce 2 channels
7479
ValueError('Binaural sounds must have two channels!')
7580

7681
def itd(self, duration=None, max_lag=0.001):
@@ -96,7 +101,9 @@ def itd(self, duration=None, max_lag=0.001):
96101
"""
97102
if duration is None:
98103
return self._get_itd(max_lag)
99-
return self._apply_itd(duration)
104+
out = copy.deepcopy(self)
105+
out.name = f'{str(duration)}-itd_{self.name}'
106+
return out._apply_itd(duration)
100107

101108
def _get_itd(self, max_lag):
102109
max_lag = Sound.in_samples(max_lag, self.samplerate)
@@ -135,11 +142,12 @@ def ild(self, dB=None):
135142
"""
136143
if dB is None:
137144
return self.right.level - self.left.level
138-
new = copy.deepcopy(self) # so that we can return a new sound
145+
out = copy.deepcopy(self) # so that we can return a new sound
139146
level = numpy.mean(self.level)
140-
new_levels = (level - dB/2, level + dB/2)
141-
new.level = new_levels
142-
return new
147+
out_levels = (level - dB/2, level + dB/2)
148+
out.level = out_levels
149+
out.name = f'{str(dB)}-ild_{self.name}'
150+
return out
143151

144152
def itd_ramp(self, from_itd=-6e-4, to_itd=6e-4):
145153
"""
@@ -158,7 +166,7 @@ def itd_ramp(self, from_itd=-6e-4, to_itd=6e-4):
158166
moving = sig.itd_ramp(from_itd=-0.001, to_itd=0.01)
159167
moving.play()
160168
"""
161-
new = copy.deepcopy(self)
169+
out = copy.deepcopy(self)
162170
# make the ITD ramps
163171
left_ramp = numpy.linspace(from_itd / 2, to_itd / 2, self.n_samples)
164172
right_ramp = numpy.linspace(-from_itd / 2, -to_itd / 2, self.n_samples)
@@ -168,9 +176,10 @@ def itd_ramp(self, from_itd=-6e-4, to_itd=6e-4):
168176
filter_length = self.n_samples // 16 * 2 # 1/8th of n_samples, always even
169177
else:
170178
raise ValueError('Signal too short! (min 512 samples)')
171-
new = new.delay(duration=left_ramp, channel=0, filter_length=filter_length)
172-
new = new.delay(duration=right_ramp, channel=1, filter_length=filter_length)
173-
return new
179+
out = out.delay(duration=left_ramp, channel=0, filter_length=filter_length)
180+
out = out.delay(duration=right_ramp, channel=1, filter_length=filter_length)
181+
out.name = f'ild-ramp_{self.name}'
182+
return out
174183

175184
def ild_ramp(self, from_ild=-50, to_ild=50):
176185
"""
@@ -190,16 +199,17 @@ def ild_ramp(self, from_ild=-50, to_ild=50):
190199
moving = sig.ild_ramp(from_ild=-10, to_ild=10)
191200
move.play()
192201
"""
193-
new = self.ild(0) # set ild to zero
202+
out = self.ild(0) # set ild to zero
194203
# make ramps
195204
left_ramp = numpy.linspace(-from_ild / 2, -to_ild / 2, self.n_samples)
196205
right_ramp = numpy.linspace(from_ild / 2, to_ild / 2, self.n_samples)
197206
left_ramp = 10**(left_ramp/20.)
198207
right_ramp = 10**(right_ramp/20.)
199208
# multiply channels with ramps
200-
new.data[:, 0] *= left_ramp
201-
new.data[:, 1] *= right_ramp
202-
return new
209+
out.data[:, 0] *= left_ramp
210+
out.data[:, 1] *= right_ramp
211+
out.name = f'ild-ramp_{self.name}'
212+
return out
203213

204214
@staticmethod
205215
def azimuth_to_itd(azimuth, frequency=2000, head_radius=8.75):
@@ -275,6 +285,7 @@ def at_azimuth(self, azimuth=0, ils=None):
275285
itd = Binaural.azimuth_to_itd(azimuth, frequency=centroid)
276286
ild = Binaural.azimuth_to_ild(azimuth, frequency=centroid, ils=ils)
277287
out = self.itd(duration=itd)
288+
out.name = f'{azimuth}-azi_{self.name}'
278289
return out.ild(dB=ild)
279290

280291
def externalize(self, hrtf=None):
@@ -301,9 +312,10 @@ def externalize(self, hrtf=None):
301312
# if sound and HRTF has different samplerates, resample the sound, apply the HRTF, and resample back:
302313
resampled_signal = resampled_signal.resample(hrtf.data[0].samplerate) # resample to hrtf rate
303314
filt = Filter(10**(h/20), fir='TF', samplerate=hrtf.data[0].samplerate)
304-
filtered_signal = filt.apply(resampled_signal)
305-
filtered_signal = filtered_signal.resample(self.samplerate)
306-
return filtered_signal
315+
out = filt.apply(resampled_signal)
316+
out = out.resample(self.samplerate)
317+
out.name = f'externalized_{self.name}'
318+
return out
307319

308320
@staticmethod
309321
def make_interaural_level_spectrum(hrtf=None):
@@ -397,6 +409,7 @@ def interaural_level_spectrum(self, azimuth, ils=None):
397409
out_left = Filter.collapse_subbands(subbands_left, filter_bank=fbank)
398410
out_right = Filter.collapse_subbands(subbands_right, filter_bank=fbank)
399411
out = Binaural([out_left, out_right])
412+
out.name = f'ils_{self.name}'
400413
return out.resample(samplerate=original_samplerate)
401414

402415
def drr(self, winlength=0.0025):
@@ -465,6 +478,7 @@ def whitenoise(kind='diotic', **kwargs):
465478
out.left = out.right
466479
else:
467480
raise ValueError("kind must be 'dichotic' or 'diotic'.")
481+
out.name = f'{kind}-{out.name}'
468482
return out
469483

470484
@staticmethod
@@ -473,7 +487,9 @@ def pinknoise(kind='diotic', **kwargs):
473487
Generate binaural pink noise. `kind`='diotic' produces the same noise samples in both channels,
474488
`kind`='dichotic' produces uncorrelated noise. The rest is identical to `slab.Sound.pinknoise`.
475489
"""
476-
return Binaural.powerlawnoise(alpha=1, kind=kind, **kwargs)
490+
out = Binaural.powerlawnoise(alpha=1, kind=kind, **kwargs)
491+
out.name = f'{kind}-pinknoise'
492+
return out
477493

478494
@staticmethod
479495
def powerlawnoise(kind='diotic', **kwargs):
@@ -489,6 +505,7 @@ def powerlawnoise(kind='diotic', **kwargs):
489505
out.left = out.right
490506
else:
491507
raise ValueError("kind must be 'dichotic' or 'diotic'.")
508+
out.name = f'{kind}-{out.name}'
492509
return out
493510

494511
@staticmethod
@@ -502,6 +519,7 @@ def irn(kind='diotic', **kwargs):
502519
out.left = out.right
503520
else:
504521
raise ValueError("kind must be 'dichotic' or 'diotic'.")
522+
out.name = f'{kind}-{out.name}'
505523
return out
506524

507525
@staticmethod

slab/signal.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ class Signal:
3232
it must have a .data attribute containing an array. If it's a list, the elements can be arrays or objects.
3333
The output will be a multi-channel sound with each channel corresponding to an element of the list.
3434
samplerate (int | None): the samplerate of the sound. If None, use the default samplerate.
35+
name (str): a string label for the signal object. Default is 'unnamed'.
3536
Attributes:
3637
.duration: duration of the sound in seconds
3738
.n_samples: duration of the sound in samples
3839
.n_channels: number of channels in the sound
3940
.times: list with the time point of each sample
41+
.name: string label
4042
Examples::
4143
4244
import slab, numpy
@@ -57,7 +59,7 @@ class Signal:
5759
doc='The number of channels in the Signal.')
5860

5961
# __methods (class creation, printing, and slice functionality)
60-
def __init__(self, data, samplerate=None):
62+
def __init__(self, data, samplerate=None, name='unnamed'):
6163
if hasattr(data, 'samplerate') and samplerate is not None:
6264
warnings.warn('First argument has a samplerate property. Ignoring given samplerate.')
6365
if isinstance(data, numpy.ndarray):
@@ -90,19 +92,24 @@ def __init__(self, data, samplerate=None):
9092
self.samplerate = _default_samplerate
9193
else:
9294
self.samplerate = samplerate
95+
if hasattr(data, 'name') and name == 'unnamed': # carry over name if source object has one and no new name provided
96+
self.name = data.name
97+
else:
98+
self.name = name
99+
93100

94101
def __repr__(self):
95-
return f'{type(self)} (\n{repr(self.data)}\n{repr(self.samplerate)})'
102+
return f'{type(self)} (\n{repr(self.data)}\n{repr(self.samplerate)}\n{repr(self.name)})'
96103

97104
def __str__(self):
98-
return f'{type(self)} duration {self.duration}, samples {self.n_samples}, channels {self.n_channels},' \
105+
return f'{type(self)} ({self.name}) duration {self.duration}, samples {self.n_samples}, channels {self.n_channels},' \
99106
f'samplerate {self.samplerate}'
100107

101108
def _repr_html_(self):
102109
'HTML image representation for Jupyter notebook support'
103110
elipses = '\u2026'
104111
class_name = str(type(self))[8:-2]
105-
html = [f'<h4>{class_name} with samplerate = {self.samplerate}</h4>']
112+
html = [f'<h4>{class_name} ({self.name}) with samplerate = {self.samplerate}</h4>']
106113
html += ['<table><tr><th>#</th>']
107114
samps, chans = self.data.shape
108115
html += (f'<th>channel {j}</th>' for j in range(chans))
@@ -336,7 +343,7 @@ def _get_envelope(self, kind, cutoff):
336343
envs = 20 * numpy.log10(envs) # convert amplitude to dB
337344
elif not kind == 'gain':
338345
raise ValueError('Kind must be either "gain" or "dB"!')
339-
return Signal(envs, samplerate=self.samplerate)
346+
return Signal(envs, samplerate=self.samplerate, name='envelope')
340347

341348
def _apply_envelope(self, envelope, times, kind):
342349
# TODO: write tests for the new options!

0 commit comments

Comments
 (0)