@@ -18,14 +18,18 @@ class Binaural(Sound):
18
18
data (slab.Signal | numpy.ndarray | list | str): see documentation of slab.Sound for details. the `data` must
19
19
have either one or two channels. If it has one, that channel is duplicated
20
20
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
+
21
24
Attributes:
22
25
.left: the first data channel, containing the sound for the left ear.
23
26
.right: the second data channel, containing the sound for the right ear
24
27
.data: the data-array of the Sound object which has the shape `n_samples` x `n_channels`.
25
28
.n_channels: the number of channels in `data`. Must be 2 for a binaural sound.
26
29
.n_samples: the number of samples in `data`. Equals `duration` * `samplerate`.
27
30
.duration: the duration of the sound in seconds. Equals `n_samples` / `samplerate`.
28
- """
31
+ .name: string label of the sound.
32
+ """
29
33
# instance properties
30
34
def _set_left (self , other ):
31
35
if hasattr (other , 'samplerate' ): # probably an slab object
@@ -44,33 +48,34 @@ def _set_right(self, other):
44
48
right = property (fget = lambda self : Sound (self .channel (1 )), fset = _set_right ,
45
49
doc = 'The right channel for a stereo sound.' )
46
50
47
- def __init__ (self , data , samplerate = None ):
51
+ def __init__ (self , data , samplerate = None , name = 'unnamed' ):
48
52
if isinstance (data , (Sound , Signal )):
53
+ self .name = data .name
49
54
if data .n_channels == 1 : # if there is only one channel, duplicate it.
50
55
self .data = numpy .tile (data .data , 2 )
51
56
elif data .n_channels == 2 :
52
57
self .data = data .data
53
58
else :
54
59
raise ValueError ("Data must have one or two channel!" )
55
60
self .samplerate = data .samplerate
56
- elif isinstance (data , (list , tuple )):
61
+ elif isinstance (data , (list , tuple )): # list of Sounds
57
62
if isinstance (data [0 ], (Sound , Signal )):
58
63
if data [0 ].n_samples != data [1 ].n_samples :
59
64
raise ValueError ('Sounds must have same number of samples!' )
60
65
if data [0 ].samplerate != data [1 ].samplerate :
61
66
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
66
71
super ().__init__ (data , samplerate )
67
72
if self .n_channels == 1 :
68
73
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 )
71
76
if self .n_channels == 1 :
72
77
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
74
79
ValueError ('Binaural sounds must have two channels!' )
75
80
76
81
def itd (self , duration = None , max_lag = 0.001 ):
@@ -96,7 +101,9 @@ def itd(self, duration=None, max_lag=0.001):
96
101
"""
97
102
if duration is None :
98
103
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 )
100
107
101
108
def _get_itd (self , max_lag ):
102
109
max_lag = Sound .in_samples (max_lag , self .samplerate )
@@ -135,11 +142,12 @@ def ild(self, dB=None):
135
142
"""
136
143
if dB is None :
137
144
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
139
146
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
143
151
144
152
def itd_ramp (self , from_itd = - 6e-4 , to_itd = 6e-4 ):
145
153
"""
@@ -158,7 +166,7 @@ def itd_ramp(self, from_itd=-6e-4, to_itd=6e-4):
158
166
moving = sig.itd_ramp(from_itd=-0.001, to_itd=0.01)
159
167
moving.play()
160
168
"""
161
- new = copy .deepcopy (self )
169
+ out = copy .deepcopy (self )
162
170
# make the ITD ramps
163
171
left_ramp = numpy .linspace (from_itd / 2 , to_itd / 2 , self .n_samples )
164
172
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):
168
176
filter_length = self .n_samples // 16 * 2 # 1/8th of n_samples, always even
169
177
else :
170
178
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
174
183
175
184
def ild_ramp (self , from_ild = - 50 , to_ild = 50 ):
176
185
"""
@@ -190,16 +199,17 @@ def ild_ramp(self, from_ild=-50, to_ild=50):
190
199
moving = sig.ild_ramp(from_ild=-10, to_ild=10)
191
200
move.play()
192
201
"""
193
- new = self .ild (0 ) # set ild to zero
202
+ out = self .ild (0 ) # set ild to zero
194
203
# make ramps
195
204
left_ramp = numpy .linspace (- from_ild / 2 , - to_ild / 2 , self .n_samples )
196
205
right_ramp = numpy .linspace (from_ild / 2 , to_ild / 2 , self .n_samples )
197
206
left_ramp = 10 ** (left_ramp / 20. )
198
207
right_ramp = 10 ** (right_ramp / 20. )
199
208
# 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
203
213
204
214
@staticmethod
205
215
def azimuth_to_itd (azimuth , frequency = 2000 , head_radius = 8.75 ):
@@ -275,6 +285,7 @@ def at_azimuth(self, azimuth=0, ils=None):
275
285
itd = Binaural .azimuth_to_itd (azimuth , frequency = centroid )
276
286
ild = Binaural .azimuth_to_ild (azimuth , frequency = centroid , ils = ils )
277
287
out = self .itd (duration = itd )
288
+ out .name = f'{ azimuth } -azi_{ self .name } '
278
289
return out .ild (dB = ild )
279
290
280
291
def externalize (self , hrtf = None ):
@@ -301,9 +312,10 @@ def externalize(self, hrtf=None):
301
312
# if sound and HRTF has different samplerates, resample the sound, apply the HRTF, and resample back:
302
313
resampled_signal = resampled_signal .resample (hrtf .data [0 ].samplerate ) # resample to hrtf rate
303
314
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
307
319
308
320
@staticmethod
309
321
def make_interaural_level_spectrum (hrtf = None ):
@@ -397,6 +409,7 @@ def interaural_level_spectrum(self, azimuth, ils=None):
397
409
out_left = Filter .collapse_subbands (subbands_left , filter_bank = fbank )
398
410
out_right = Filter .collapse_subbands (subbands_right , filter_bank = fbank )
399
411
out = Binaural ([out_left , out_right ])
412
+ out .name = f'ils_{ self .name } '
400
413
return out .resample (samplerate = original_samplerate )
401
414
402
415
def drr (self , winlength = 0.0025 ):
@@ -465,6 +478,7 @@ def whitenoise(kind='diotic', **kwargs):
465
478
out .left = out .right
466
479
else :
467
480
raise ValueError ("kind must be 'dichotic' or 'diotic'." )
481
+ out .name = f'{ kind } -{ out .name } '
468
482
return out
469
483
470
484
@staticmethod
@@ -473,7 +487,9 @@ def pinknoise(kind='diotic', **kwargs):
473
487
Generate binaural pink noise. `kind`='diotic' produces the same noise samples in both channels,
474
488
`kind`='dichotic' produces uncorrelated noise. The rest is identical to `slab.Sound.pinknoise`.
475
489
"""
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
477
493
478
494
@staticmethod
479
495
def powerlawnoise (kind = 'diotic' , ** kwargs ):
@@ -489,6 +505,7 @@ def powerlawnoise(kind='diotic', **kwargs):
489
505
out .left = out .right
490
506
else :
491
507
raise ValueError ("kind must be 'dichotic' or 'diotic'." )
508
+ out .name = f'{ kind } -{ out .name } '
492
509
return out
493
510
494
511
@staticmethod
@@ -502,6 +519,7 @@ def irn(kind='diotic', **kwargs):
502
519
out .left = out .right
503
520
else :
504
521
raise ValueError ("kind must be 'dichotic' or 'diotic'." )
522
+ out .name = f'{ kind } -{ out .name } '
505
523
return out
506
524
507
525
@staticmethod
0 commit comments