import numpy as np
from .firfilterbank import *
__all__ = ['FractionalDelay']
[docs]class FractionalDelay(FIRFilterbank):
'''
Filterbank for applying delays which are fractional multiples of the timestep
Initialised with arguments:
``source``
Source sound or filterbank.
``delays``
A list or array of delays to apply (the number of channels in the
filterbank will be equal to the length of this).
``filter_length=None``
Use this to explicitly set the length of the impulse response, should
be odd. If not specified, it will be automatically determined from
the delays. See notes below.
``**args``
Arguments to pass to :class:`FIRFilterbank` (from which this class
is derived).
**Attributes**
.. attribute:: delay_offset
The global delay offset. If the specified delay in a given channel is
``delay`` the actual delay will be ``delay_offset+delay``. It is equal
to ``(filter_length/2)/source.samplerate``.
.. attribute:: filter_length
The length of the filter to use. This is automatically determined
from the delays. Note that ``delay_offset`` should be larger than the
maximum positive or negative delay. The minimum filter length is
by default 2048 samples, which allows for good accuracy for signals
with power above 20 Hz. For low frequency analysis, longer filters will
be necessary. For high frequency analysis, a shorter filter length could
be used for a more efficient computation.
**Notes**
Inducing a delay for a sound that is an integer multiple of the timestep
(1/samplerate) can be done simply by offsetting the samples, e.g.
``sound[3:]`` is ``sound`` delayed by ``3/sound.samplerate``. However,
for fractional multiples of the timestep, the sound needs to be filtered.
The theory and code for this was adapted from
`http://www.labbookpages.co.uk/audio/beamforming/fractionalDelay.html <http://www.labbookpages.co.uk/audio/beamforming/fractionalDelay.html>`__.
The filters induce a delay of ``delay_offset+delay`` where ``delay_offset``
is a positive value larger than the maximum positive or negative delay.
This value is available as the attribute ``delay_offset``.
'''
def __init__(self, source, delays, filter_length=None, **args):
delays = np.asarray(np.atleast_1d(delays))
delay_max = np.amax(abs(delays))
delay_max_int = int(np.ceil(source.samplerate*delay_max))
if filter_length is None:
filter_length = 2*int(delay_max_int*1.25)+1
if filter_length<2048:
filter_length = 2048
if filter_length/2<=delay_max_int:
raise ValueError('Filter length not long enough for selected delays.')
self.delay_offset = (filter_length//2)/source.samplerate
self.filter_length = filter_length
self.delays = delays
irs = [fractional_delay_ir(delay, source.samplerate,
filter_length=filter_length) for delay in delays]
irs = np.array(irs)
self.impulse_response = irs
FIRFilterbank.__init__(self, source, irs, **args)
# Adapted from
# http://www.labbookpages.co.uk/audio/beamforming/fractionalDelay.html
def fractional_delay_ir(delay, samplerate, filter_length=151):
delay = float(delay*samplerate)
centre_tap = filter_length // 2
t = np.arange(filter_length)
x = t-delay
if abs(round(delay)-float(delay))<1e-10:
return np.array(x==centre_tap, dtype=float)
sinc = np.sin(np.pi*(x-centre_tap))/(np.pi*(x-centre_tap))
window = 0.54-0.46*np.cos(2.0*np.pi*(x+0.5)/filter_length) # Hamming window
tap_weight = window*sinc
return tap_weight