3
from ..filtering import FIRFilterbank
6
__all__ = ['HRTF', 'HRTFSet', 'HRTFDatabase',
11
Head related transfer function.
16
The pair of impulse responses (as stereo :class:`Sound` objects)
18
The impulse responses in a format suitable for using with
19
:class:`FIRFilterbank` (the transpose of ``impulse_response``).
21
The two HRTFs (mono :class:`Sound` objects)
23
The sample rate of the HRTFs.
28
.. automethod:: filterbank
30
You can get the number of samples in the impulse response with ``len(hrtf)``.
32
def __init__(self, hrir_l, hrir_r=None):
36
hrir = Sound((hrir_l, hrir_r), samplerate=hrir_l.samplerate)
37
self.samplerate = hrir.samplerate
38
self.impulse_response = hrir
40
self.right = hrir.right
42
def apply(self, sound):
44
Returns a stereo :class:`Sound` object formed by applying the pair of
45
HRTFs to the mono ``sound`` input. Equivalently, you can write
46
``hrtf(sound)`` for ``hrtf`` an :class:`HRTF` object.
48
# Note we use an FFT based method for applying HRTFs that is
49
# mathematically equivalent to using convolution (accurate to 1e-15
50
# in practice) and around 100x faster.
51
if not sound.nchannels==1:
52
raise ValueError('HRTF can only be applied to mono sounds')
53
if len(unique(array([self.samplerate, sound.samplerate], dtype=int)))>1:
54
raise ValueError('HRTF and sound samplerates do not match.')
55
sound = asarray(sound).flatten()
56
# Pad left/right/sound with zeros of length max(impulse response length)
57
# at the beginning, and at the end so that they are all the same length
58
# which should be a power of 2 for efficiency. The reason to pad at
59
# the beginning is that the first output samples are not guaranteed to
60
# be equal because of the delays in the impulse response, but they
61
# exactly equalise after the length of the impulse response, so we just
62
# zero pad. The reason for padding at the end is so that for the FFT we
63
# can just multiply the arrays, which should have the same shape.
64
left = asarray(self.left).flatten()
65
right = asarray(self.right).flatten()
66
ir_nmax = max(len(left), len(right))
67
nmax = max(ir_nmax, len(sound))+ir_nmax
68
nmax = 2**int(ceil(log2(nmax)))
69
leftpad = hstack((left, zeros(nmax-len(left))))
70
rightpad = hstack((right, zeros(nmax-len(right))))
71
soundpad = hstack((zeros(ir_nmax), sound, zeros(nmax-ir_nmax-len(sound))))
72
# Compute FFTs, multiply and compute IFFT
73
left_fft = fft(leftpad, n=nmax)
74
right_fft = fft(rightpad, n=nmax)
75
sound_fft = fft(soundpad, n=nmax)
76
left_sound_fft = left_fft*sound_fft
77
right_sound_fft = right_fft*sound_fft
78
left_sound = ifft(left_sound_fft).real
79
right_sound = ifft(right_sound_fft).real
80
# finally, we take only the unpadded parts of these
81
left_sound = left_sound[ir_nmax:ir_nmax+len(sound)]
82
right_sound = right_sound[ir_nmax:ir_nmax+len(sound)]
83
return Sound((left_sound, right_sound), samplerate=self.samplerate)
87
return array(self.impulse_response.T, copy=True)
88
fir = property(fget=get_fir)
90
def filterbank(self, source, **kwds):
92
Returns an :class:`FIRFilterbank` object that can be used to apply
93
the HRTF as part of a chain of filterbanks.
95
return FIRFilterbank(source, self.fir, **kwds)
98
return self.impulse_response.shape[0]
100
def make_coordinates(**kwds):
102
Creates a numpy record array from the keywords passed to the function.
103
Each keyword/value pair should be the name of the coordinate the array of
104
values of that coordinate for each location.
105
Returns a numpy record array. For example::
107
coords = make_coordinates(azimuth=[0, 30, 60, 0, 30, 60],
108
elevation=[0, 0, 0, 30, 30, 30])
109
print coords['azimuth']
111
dtype = [(name, float) for name in kwds.keys()]
112
n = len(kwds.values()[0])
113
x = zeros(n, dtype=dtype)
114
for name, values in kwds.items():
118
class HRTFSet(object):
120
A collection of HRTFs, typically for a single individual.
122
Normally this object is created automatically by an :class:`HRTFDatabase`.
127
A list of ``HRTF`` objects for each index.
129
The number of HRTF locations. You can also use ``len(hrtfset)``.
131
The sample length of each HRTF.
132
``fir_serial``, ``fir_interleaved``
133
The impulse responses in a format suitable for using with
134
:class:`FIRFilterbank`, in serial (LLLLL...RRRRR....) or interleaved
139
.. automethod:: subset
140
.. automethod:: filterbank
142
You can access an HRTF by index via ``hrtfset[index]``, or
143
by its coordinates via ``hrtfset(coord1=val1, coord2=val2)``.
148
An array of shape (2, num_indices, num_samples) where data[0,:,:] is
149
the left ear and data[1,:,:] is the right ear, num_indices is the number
150
of HRTFs for each ear, and num_samples is the length of the HRTF.
152
The sample rate for the HRTFs (should have units of Hz).
154
A record array of length ``num_indices`` giving the coordinates of each
155
HRTF. You can use :func:`make_coordinates` to help with this.
157
def __init__(self, data, samplerate, coordinates):
159
self.samplerate = samplerate
160
self.coordinates = coordinates
162
for i in xrange(self.num_indices):
163
l = Sound(self.data[0, i, :], samplerate=self.samplerate)
164
r = Sound(self.data[1, i, :], samplerate=self.samplerate)
165
self.hrtf.append(HRTF(l, r))
167
def __getitem__(self, key):
168
return self.hrtf[key]
170
def __call__(self, **kwds):
171
I = ones(self.num_indices, dtype=bool)
172
for key, value in kwds.items():
173
I = logical_and(I, abs(self.coordinates[key]-value)<1e-10)
174
indices = I.nonzero()[0]
176
raise IndexError('No HRTF exists with those coordinates')
178
raise IndexError('More than one HRTF exists with those coordinates')
179
return self.hrtf[indices[0]]
181
def subset(self, condition):
183
Generates the subset of the set of HRTFs whose coordinates satisfy
184
the ``condition``. This should be one of: a boolean array of
185
length the number of HRTFs in the set, with values
186
of True/False to indicate if the corresponding HRTF should be included
187
or not; an integer array with the indices of the HRTFs to keep; or a
188
function whose argument names are
189
names of the parameters of the coordinate system, e.g.
190
``condition=lambda azim:azim<pi/2``.
192
if callable(condition):
193
ns = dict((name, self.coordinates[name]) for name in condition.func_code.co_varnames)
199
if isinstance(I, bool): # vector-based calculation doesn't work
200
n = len(ns[condition.func_code.co_varnames[0]])
201
I = array([condition(**dict((name, ns[name][j]) for name in condition.func_code.co_varnames)) for j in range(n)])
204
if condition.dtype==bool:
205
I = condition.nonzero()[0]
208
hrtf = [self.hrtf[i] for i in I]
209
coords = self.coordinates[I]
210
data = self.data[:, I, :]
213
obj.coordinates = coords
218
return self.num_indices
221
def num_indices(self):
222
return self.data.shape[1]
225
def num_samples(self):
226
return self.data.shape[2]
229
def fir_serial(self):
230
return reshape(self.data, (self.num_indices*2, self.num_samples))
233
def fir_interleaved(self):
234
fir = empty((self.num_indices*2, self.num_samples))
235
fir[::2, :] = self.data[0, :, :]
236
fir[1::2, :] = self.data[1, :, :]
239
def filterbank(self, source, interleaved=False, **kwds):
241
Returns an :class:`FIRFilterbank` object which applies all of the HRTFs
242
in the set. If ``interleaved=False`` then
243
the channels are arranged in the order LLLL...RRRR..., otherwise they
244
are arranged in the order LRLRLR....
247
fir = self.fir_interleaved
249
fir = self.fir_serial
250
return FIRFilterbank(source, fir, **kwds)
253
class HRTFDatabase(object):
255
Base class for databases of HRTFs
257
Should have an attribute 'subjects' giving a list of available subjects,
258
and a method ``load_subject(subject)`` which returns an ``HRTFSet`` for that
261
The initialiser should take (optional) keywords:
264
The intended samplerate (resampling will be used if it is wrong). If
265
left unset, the natural samplerate of the data set will be used.
267
def __init__(self, samplerate=None):
268
raise NotImplementedError
270
def load_subject(self, subject):
271
raise NotImplementedError