mi@0
|
1 # Copyright 2014 Jason Heeris, jason.heeris@gmail.com
|
mi@0
|
2 #
|
mi@0
|
3 # This file is part of the gammatone toolkit, and is licensed under the 3-clause
|
mi@0
|
4 # BSD license: https://github.com/detly/gammatone/blob/master/COPYING
|
mi@0
|
5 """
|
mi@0
|
6 This module contains functions for constructing sets of equivalent rectangular
|
mi@0
|
7 bandwidth gammatone filters.
|
mi@0
|
8 """
|
mi@0
|
9 from __future__ import division
|
mi@0
|
10 from collections import namedtuple
|
mi@0
|
11
|
mi@0
|
12 import numpy as np
|
mi@0
|
13 import scipy as sp
|
mi@0
|
14 from scipy import signal as sgn
|
mi@0
|
15
|
mi@0
|
16 DEFAULT_FILTER_NUM = 100
|
mi@0
|
17 DEFAULT_LOW_FREQ = 100
|
mi@0
|
18 DEFAULT_HIGH_FREQ = 44100/4
|
mi@0
|
19
|
mi@0
|
20
|
mi@0
|
21 def erb_point(low_freq, high_freq, fraction):
|
mi@0
|
22 """
|
mi@0
|
23 Calculates a single point on an ERB scale between ``low_freq`` and
|
mi@0
|
24 ``high_freq``, determined by ``fraction``. When ``fraction`` is ``1``,
|
mi@0
|
25 ``low_freq`` will be returned. When ``fraction`` is ``0``, ``high_freq``
|
mi@0
|
26 will be returned.
|
mi@0
|
27
|
mi@0
|
28 ``fraction`` can actually be outside the range ``[0, 1]``, which in general
|
mi@0
|
29 isn't very meaningful, but might be useful when ``fraction`` is rounded a
|
mi@0
|
30 little above or below ``[0, 1]`` (eg. for plot axis labels).
|
mi@0
|
31 """
|
mi@0
|
32 # Change the following three parameters if you wish to use a different ERB
|
mi@0
|
33 # scale. Must change in MakeERBCoeffs too.
|
mi@0
|
34 # TODO: Factor these parameters out
|
mi@0
|
35 ear_q = 9.26449 # Glasberg and Moore Parameters
|
mi@0
|
36 min_bw = 24.7
|
mi@0
|
37 order = 1
|
mi@0
|
38
|
mi@0
|
39 # All of the following expressions are derived in Apple TR #35, "An
|
mi@0
|
40 # Efficient Implementation of the Patterson-Holdsworth Cochlear Filter
|
mi@0
|
41 # Bank." See pages 33-34.
|
mi@0
|
42 erb_point = (
|
mi@0
|
43 -ear_q*min_bw
|
mi@0
|
44 + np.exp(
|
mi@0
|
45 fraction * (
|
mi@0
|
46 -np.log(high_freq + ear_q*min_bw)
|
mi@0
|
47 + np.log(low_freq + ear_q*min_bw)
|
mi@0
|
48 )
|
mi@0
|
49 ) *
|
mi@0
|
50 (high_freq + ear_q*min_bw)
|
mi@0
|
51 )
|
mi@0
|
52
|
mi@0
|
53 return erb_point
|
mi@0
|
54
|
mi@0
|
55
|
mi@0
|
56 def erb_space(
|
mi@0
|
57 low_freq=DEFAULT_LOW_FREQ,
|
mi@0
|
58 high_freq=DEFAULT_HIGH_FREQ,
|
mi@0
|
59 num=DEFAULT_FILTER_NUM):
|
mi@0
|
60 """
|
mi@0
|
61 This function computes an array of ``num`` frequencies uniformly spaced
|
mi@0
|
62 between ``high_freq`` and ``low_freq`` on an ERB scale.
|
mi@0
|
63
|
mi@0
|
64 For a definition of ERB, see Moore, B. C. J., and Glasberg, B. R. (1983).
|
mi@0
|
65 "Suggested formulae for calculating auditory-filter bandwidths and
|
mi@0
|
66 excitation patterns," J. Acoust. Soc. Am. 74, 750-753.
|
mi@0
|
67 """
|
mi@0
|
68 return erb_point(
|
mi@0
|
69 low_freq,
|
mi@0
|
70 high_freq,
|
mi@0
|
71 np.arange(1, num+1)/num
|
mi@0
|
72 )
|
mi@0
|
73
|
mi@0
|
74
|
mi@0
|
75 def centre_freqs(high_freq, num_freqs, cutoff):
|
mi@0
|
76 """
|
mi@0
|
77 Calculates an array of centre frequencies (for :func:`make_erb_filters`)
|
mi@0
|
78 from a sampling frequency, lower cutoff frequency and the desired number of
|
mi@0
|
79 filters.
|
mi@0
|
80
|
mi@0
|
81 :param fs: sampling rate
|
mi@0
|
82 :param num_freqs: number of centre frequencies to calculate
|
mi@0
|
83 :type num_freqs: int
|
mi@0
|
84 :param cutoff: lower cutoff frequency
|
mi@0
|
85 :return: same as :func:`erb_space`
|
mi@0
|
86 """
|
mi@0
|
87 # print 'center freq', erb_space(cutoff, fs/2, num_freqs)
|
mi@0
|
88 return erb_space(cutoff, high_freq, num_freqs)
|
mi@0
|
89
|
mi@0
|
90
|
mi@0
|
91 def make_erb_filters(fs, centre_freqs, width=1.0):
|
mi@0
|
92 """
|
mi@0
|
93 This function computes the filter coefficients for a bank of
|
mi@0
|
94 Gammatone filters. These filters were defined by Patterson and Holdworth for
|
mi@0
|
95 simulating the cochlea.
|
mi@0
|
96
|
mi@0
|
97 The result is returned as a :class:`ERBCoeffArray`. Each row of the
|
mi@0
|
98 filter arrays contains the coefficients for four second order filters. The
|
mi@0
|
99 transfer function for these four filters share the same denominator (poles)
|
mi@0
|
100 but have different numerators (zeros). All of these coefficients are
|
mi@0
|
101 assembled into one vector that the ERBFilterBank can take apart to implement
|
mi@0
|
102 the filter.
|
mi@0
|
103
|
mi@0
|
104 The filter bank contains "numChannels" channels that extend from
|
mi@0
|
105 half the sampling rate (fs) to "lowFreq". Alternatively, if the numChannels
|
mi@0
|
106 input argument is a vector, then the values of this vector are taken to be
|
mi@0
|
107 the center frequency of each desired filter. (The lowFreq argument is
|
mi@0
|
108 ignored in this case.)
|
mi@0
|
109
|
mi@0
|
110 Note this implementation fixes a problem in the original code by
|
mi@0
|
111 computing four separate second order filters. This avoids a big problem with
|
mi@0
|
112 round off errors in cases of very small cfs (100Hz) and large sample rates
|
mi@0
|
113 (44kHz). The problem is caused by roundoff error when a number of poles are
|
mi@0
|
114 combined, all very close to the unit circle. Small errors in the eigth order
|
mi@0
|
115 coefficient, are multiplied when the eigth root is taken to give the pole
|
mi@0
|
116 location. These small errors lead to poles outside the unit circle and
|
mi@0
|
117 instability. Thanks to Julius Smith for leading me to the proper
|
mi@0
|
118 explanation.
|
mi@0
|
119
|
mi@0
|
120 Execute the following code to evaluate the frequency response of a 10
|
mi@0
|
121 channel filterbank::
|
mi@0
|
122
|
mi@0
|
123 fcoefs = MakeERBFilters(16000,10,100);
|
mi@0
|
124 y = ERBFilterBank([1 zeros(1,511)], fcoefs);
|
mi@0
|
125 resp = 20*log10(abs(fft(y')));
|
mi@0
|
126 freqScale = (0:511)/512*16000;
|
mi@0
|
127 semilogx(freqScale(1:255),resp(1:255,:));
|
mi@0
|
128 axis([100 16000 -60 0])
|
mi@0
|
129 xlabel('Frequency (Hz)'); ylabel('Filter Response (dB)');
|
mi@0
|
130
|
mi@0
|
131 | Rewritten by Malcolm Slaney@Interval. June 11, 1998.
|
mi@0
|
132 | (c) 1998 Interval Research Corporation
|
mi@0
|
133 |
|
mi@0
|
134 | (c) 2012 Jason Heeris (Python implementation)
|
mi@0
|
135 """
|
mi@0
|
136 T = 1/fs
|
mi@0
|
137 # Change the followFreqing three parameters if you wish to use a different
|
mi@0
|
138 # ERB scale. Must change in ERBSpace too.
|
mi@0
|
139 # TODO: factor these out
|
mi@0
|
140 ear_q = 9.26449 # Glasberg and Moore Parameters
|
mi@0
|
141 min_bw = 24.7
|
mi@0
|
142 order = 1
|
mi@0
|
143
|
mi@0
|
144 erb = width*((centre_freqs/ear_q)**order + min_bw**order)**(1/order)
|
mi@0
|
145 B = 1.019*2*np.pi*erb
|
mi@0
|
146
|
mi@0
|
147 arg = 2*centre_freqs*np.pi*T
|
mi@0
|
148 vec = np.exp(2j*arg)
|
mi@0
|
149
|
mi@0
|
150 A0 = T
|
mi@0
|
151 A2 = 0
|
mi@0
|
152 B0 = 1
|
mi@0
|
153 B1 = -2*np.cos(arg)/np.exp(B*T)
|
mi@0
|
154 B2 = np.exp(-2*B*T)
|
mi@0
|
155
|
mi@0
|
156 rt_pos = np.sqrt(3 + 2**1.5)
|
mi@0
|
157 rt_neg = np.sqrt(3 - 2**1.5)
|
mi@0
|
158
|
mi@0
|
159 common = -T * np.exp(-(B * T))
|
mi@0
|
160
|
mi@0
|
161 # TODO: This could be simplified to a matrix calculation involving the
|
mi@0
|
162 # constant first term and the alternating rt_pos/rt_neg and +/-1 second
|
mi@0
|
163 # terms
|
mi@0
|
164 k11 = np.cos(arg) + rt_pos * np.sin(arg)
|
mi@0
|
165 k12 = np.cos(arg) - rt_pos * np.sin(arg)
|
mi@0
|
166 k13 = np.cos(arg) + rt_neg * np.sin(arg)
|
mi@0
|
167 k14 = np.cos(arg) - rt_neg * np.sin(arg)
|
mi@0
|
168
|
mi@0
|
169 A11 = common * k11
|
mi@0
|
170 A12 = common * k12
|
mi@0
|
171 A13 = common * k13
|
mi@0
|
172 A14 = common * k14
|
mi@0
|
173
|
mi@0
|
174 gain_arg = np.exp(1j * arg - B * T)
|
mi@0
|
175
|
mi@0
|
176 gain = np.abs(
|
mi@0
|
177 (vec - gain_arg * k11)
|
mi@0
|
178 * (vec - gain_arg * k12)
|
mi@0
|
179 * (vec - gain_arg * k13)
|
mi@0
|
180 * (vec - gain_arg * k14)
|
mi@0
|
181 * ( T * np.exp(B*T)
|
mi@0
|
182 / (-1 / np.exp(B*T) + 1 + vec * (1 - np.exp(B*T)))
|
mi@0
|
183 )**4
|
mi@0
|
184 )
|
mi@0
|
185
|
mi@0
|
186 allfilts = np.ones_like(centre_freqs)
|
mi@0
|
187
|
mi@0
|
188 fcoefs = np.column_stack([
|
mi@0
|
189 A0*allfilts, A11, A12, A13, A14, A2*allfilts,
|
mi@0
|
190 B0*allfilts, B1, B2,
|
mi@0
|
191 gain
|
mi@0
|
192 ])
|
mi@0
|
193
|
mi@0
|
194 return fcoefs
|
mi@0
|
195
|
mi@0
|
196
|
mi@0
|
197 def erb_filterbank(wave, coefs):
|
mi@0
|
198 """
|
mi@0
|
199 :param wave: input data (one dimensional sequence)
|
mi@0
|
200 :param coefs: gammatone filter coefficients
|
mi@0
|
201
|
mi@0
|
202 Process an input waveform with a gammatone filter bank. This function takes
|
mi@0
|
203 a single sound vector, and returns an array of filter outputs, one channel
|
mi@0
|
204 per row.
|
mi@0
|
205
|
mi@0
|
206 The fcoefs parameter, which completely specifies the Gammatone filterbank,
|
mi@0
|
207 should be designed with the :func:`make_erb_filters` function.
|
mi@0
|
208
|
mi@0
|
209 | Malcolm Slaney @ Interval, June 11, 1998.
|
mi@0
|
210 | (c) 1998 Interval Research Corporation
|
mi@0
|
211 | Thanks to Alain de Cheveigne' for his suggestions and improvements.
|
mi@0
|
212 |
|
mi@0
|
213 | (c) 2013 Jason Heeris (Python implementation)
|
mi@0
|
214 """
|
mi@0
|
215 output = np.zeros((coefs[:,9].shape[0], wave.shape[0]))
|
mi@0
|
216
|
mi@0
|
217 gain = coefs[:, 9]
|
mi@0
|
218 # A0, A11, A2
|
mi@0
|
219 As1 = coefs[:, (0, 1, 5)]
|
mi@0
|
220 # A0, A12, A2
|
mi@0
|
221 As2 = coefs[:, (0, 2, 5)]
|
mi@0
|
222 # A0, A13, A2
|
mi@0
|
223 As3 = coefs[:, (0, 3, 5)]
|
mi@0
|
224 # A0, A14, A2
|
mi@0
|
225 As4 = coefs[:, (0, 4, 5)]
|
mi@0
|
226 # B0, B1, B2
|
mi@0
|
227 Bs = coefs[:, 6:9]
|
mi@0
|
228
|
mi@0
|
229 # Loop over channels
|
mi@0
|
230 for idx in range(0, coefs.shape[0]):
|
mi@0
|
231 # These seem to be reversed (in the sense of A/B order), but that's what
|
mi@0
|
232 # the original code did...
|
mi@0
|
233 # Replacing these with polynomial multiplications reduces both accuracy
|
mi@0
|
234 # and speed.
|
mi@0
|
235 y1 = sgn.lfilter(As1[idx], Bs[idx], wave)
|
mi@0
|
236 y2 = sgn.lfilter(As2[idx], Bs[idx], y1)
|
mi@0
|
237 y3 = sgn.lfilter(As3[idx], Bs[idx], y2)
|
mi@0
|
238 y4 = sgn.lfilter(As4[idx], Bs[idx], y3)
|
mi@0
|
239 output[idx, :] = y4/gain[idx]
|
mi@0
|
240
|
mi@0
|
241 return output
|