biquad 0.2__tar.gz → 0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- biquad-0.4/PKG-INFO +99 -0
- biquad-0.4/README.md +70 -0
- {biquad-0.2 → biquad-0.4}/setup.cfg +4 -0
- {biquad-0.2 → biquad-0.4}/src/python/biquad/__init__.py +13 -1
- biquad-0.4/src/python/biquad/allpass.py +110 -0
- biquad-0.4/src/python/biquad/bandpass.py +119 -0
- biquad-0.4/src/python/biquad/biquad.py +270 -0
- biquad-0.4/src/python/biquad/highpass.py +110 -0
- biquad-0.4/src/python/biquad/highshelf.py +115 -0
- biquad-0.4/src/python/biquad/lowpass.py +110 -0
- biquad-0.4/src/python/biquad/lowshelf.py +115 -0
- biquad-0.4/src/python/biquad/notch.py +110 -0
- biquad-0.4/src/python/biquad/peak.py +113 -0
- biquad-0.4/src/python/biquad.egg-info/PKG-INFO +99 -0
- {biquad-0.2 → biquad-0.4}/src/python/biquad.egg-info/SOURCES.txt +0 -1
- biquad-0.4/src/python/biquad.egg-info/requires.txt +5 -0
- biquad-0.2/PKG-INFO +0 -48
- biquad-0.2/README.md +0 -20
- biquad-0.2/src/python/biquad/allpass.py +0 -70
- biquad-0.2/src/python/biquad/bandpass.py +0 -77
- biquad-0.2/src/python/biquad/biquad.py +0 -112
- biquad-0.2/src/python/biquad/highpass.py +0 -70
- biquad-0.2/src/python/biquad/highshelf.py +0 -77
- biquad-0.2/src/python/biquad/lowpass.py +0 -70
- biquad-0.2/src/python/biquad/lowshelf.py +0 -77
- biquad-0.2/src/python/biquad/notch.py +0 -70
- biquad-0.2/src/python/biquad/peak.py +0 -77
- biquad-0.2/src/python/biquad/plot.py +0 -88
- biquad-0.2/src/python/biquad.egg-info/PKG-INFO +0 -48
- biquad-0.2/src/python/biquad.egg-info/requires.txt +0 -2
- {biquad-0.2 → biquad-0.4}/LICENSE +0 -0
- {biquad-0.2 → biquad-0.4}/pyproject.toml +0 -0
- {biquad-0.2 → biquad-0.4}/src/python/biquad.egg-info/dependency_links.txt +0 -0
- {biquad-0.2 → biquad-0.4}/src/python/biquad.egg-info/top_level.txt +0 -0
biquad-0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: biquad
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: Collection of alterable digital biquad filters for dynamic audio effect creation
|
|
5
|
+
Home-page: https://github.com/jurihock/biquad
|
|
6
|
+
Author: Juergen Hock
|
|
7
|
+
Author-email: juergen.hock@jurihock.de
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: digital,audio,signal,processing,dasp,dafx,effects,filter,equalizer,eq,biquad,frequency,phase,spectrum,algorithms,analysis,synthesis,c,cpp,python
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Intended Audience :: Other Audience
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: C
|
|
16
|
+
Classifier: Programming Language :: C++
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Artistic Software
|
|
19
|
+
Classifier: Topic :: Education
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Sound Synthesis
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Requires-Python: >=3
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
Provides-Extra: plot
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
|
|
30
|
+
# Alterable biquad filters
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
This is a collection of [digital biquad filters](https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html) whose parameters `f` (frequency in *Hz*), `g` (gain in *dB*) and `q` (quality) can be varied at runtime. Following [DF1](https://ccrma.stanford.edu/~jos/fp/Direct_Form_I.html) filter implementations are available:
|
|
37
|
+
|
|
38
|
+
- Allpass
|
|
39
|
+
- Bandpass
|
|
40
|
+
- Highpass
|
|
41
|
+
- Lowpass
|
|
42
|
+
- Highshelf
|
|
43
|
+
- Lowshelf
|
|
44
|
+
- Notch
|
|
45
|
+
- Peak
|
|
46
|
+
|
|
47
|
+
## Basic usage
|
|
48
|
+
|
|
49
|
+
Filter with static configuration:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import biquad
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
# load audio samples somehow
|
|
56
|
+
x, sr = np.zeros(...), 44100
|
|
57
|
+
|
|
58
|
+
# create a filter of your choice
|
|
59
|
+
f = biquad.bandpass(sr, f=sr/4, q=1)
|
|
60
|
+
|
|
61
|
+
# process all audio samples
|
|
62
|
+
y = f(x)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Filter with dynamic configuration:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import biquad
|
|
69
|
+
import numpy as np
|
|
70
|
+
|
|
71
|
+
# load audio samples somehow
|
|
72
|
+
x, sr = np.zeros(...), 44100
|
|
73
|
+
|
|
74
|
+
# create a filter of your choice
|
|
75
|
+
f = biquad.bandpass(sr)
|
|
76
|
+
|
|
77
|
+
# create parameter modifications as you like
|
|
78
|
+
myf = np.linspace(1, sr/4, len(x))
|
|
79
|
+
myq = np.linspace(2, 1/2, len(x))
|
|
80
|
+
|
|
81
|
+
# process all audio samples
|
|
82
|
+
y = f(x, f=myf, q=myq)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Keep in mind:
|
|
86
|
+
|
|
87
|
+
- All filters have a default value for the persistent parameters `g` and `q`, which is set in the particular `__init__` method.
|
|
88
|
+
- Parameter `f` must be set either in the `__init__` or in the `__call__` method.
|
|
89
|
+
- The optional instantaneous parameters `f`, `g` and `q`, if specified in the `__call__` method, override the persistent ones.
|
|
90
|
+
|
|
91
|
+
## References
|
|
92
|
+
|
|
93
|
+
1. <span id="1">[Cookbook formulae for audio EQ biquad filter coefficients](https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html) by Robert Bristow-Johnson</span>
|
|
94
|
+
2. <span id="2">[Introduction to Digital Filters with Audio Applications](https://ccrma.stanford.edu/~jos/filters/filters.html) by Julius O. Smith III</span>
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[github.com/jurihock/biquad](https://github.com/jurihock/biquad) is licensed under the terms of the MIT license.
|
|
99
|
+
For details please refer to the accompanying [LICENSE](https://github.com/jurihock/biquad/raw/main/LICENSE) file distributed with it.
|
biquad-0.4/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Alterable biquad filters
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
This is a collection of [digital biquad filters](https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html) whose parameters `f` (frequency in *Hz*), `g` (gain in *dB*) and `q` (quality) can be varied at runtime. Following [DF1](https://ccrma.stanford.edu/~jos/fp/Direct_Form_I.html) filter implementations are available:
|
|
8
|
+
|
|
9
|
+
- Allpass
|
|
10
|
+
- Bandpass
|
|
11
|
+
- Highpass
|
|
12
|
+
- Lowpass
|
|
13
|
+
- Highshelf
|
|
14
|
+
- Lowshelf
|
|
15
|
+
- Notch
|
|
16
|
+
- Peak
|
|
17
|
+
|
|
18
|
+
## Basic usage
|
|
19
|
+
|
|
20
|
+
Filter with static configuration:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import biquad
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
# load audio samples somehow
|
|
27
|
+
x, sr = np.zeros(...), 44100
|
|
28
|
+
|
|
29
|
+
# create a filter of your choice
|
|
30
|
+
f = biquad.bandpass(sr, f=sr/4, q=1)
|
|
31
|
+
|
|
32
|
+
# process all audio samples
|
|
33
|
+
y = f(x)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Filter with dynamic configuration:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import biquad
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
# load audio samples somehow
|
|
43
|
+
x, sr = np.zeros(...), 44100
|
|
44
|
+
|
|
45
|
+
# create a filter of your choice
|
|
46
|
+
f = biquad.bandpass(sr)
|
|
47
|
+
|
|
48
|
+
# create parameter modifications as you like
|
|
49
|
+
myf = np.linspace(1, sr/4, len(x))
|
|
50
|
+
myq = np.linspace(2, 1/2, len(x))
|
|
51
|
+
|
|
52
|
+
# process all audio samples
|
|
53
|
+
y = f(x, f=myf, q=myq)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Keep in mind:
|
|
57
|
+
|
|
58
|
+
- All filters have a default value for the persistent parameters `g` and `q`, which is set in the particular `__init__` method.
|
|
59
|
+
- Parameter `f` must be set either in the `__init__` or in the `__call__` method.
|
|
60
|
+
- The optional instantaneous parameters `f`, `g` and `q`, if specified in the `__call__` method, override the persistent ones.
|
|
61
|
+
|
|
62
|
+
## References
|
|
63
|
+
|
|
64
|
+
1. <span id="1">[Cookbook formulae for audio EQ biquad filter coefficients](https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html) by Robert Bristow-Johnson</span>
|
|
65
|
+
2. <span id="2">[Introduction to Digital Filters with Audio Applications](https://ccrma.stanford.edu/~jos/filters/filters.html) by Julius O. Smith III</span>
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
[github.com/jurihock/biquad](https://github.com/jurihock/biquad) is licensed under the terms of the MIT license.
|
|
70
|
+
For details please refer to the accompanying [LICENSE](https://github.com/jurihock/biquad/raw/main/LICENSE) file distributed with it.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.4"
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
from .allpass import allpass
|
|
@@ -13,6 +13,18 @@ from .peak import peak
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def filter(name, sr, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Create a filter instance of the specified filter name.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
sr : int or float
|
|
22
|
+
Sample rate in hertz.
|
|
23
|
+
f : int or float, optional
|
|
24
|
+
Persistent filter frequency parameter in hertz.
|
|
25
|
+
q : int or float, optional
|
|
26
|
+
Persistent filter quality parameter.
|
|
27
|
+
"""
|
|
16
28
|
|
|
17
29
|
name = str(name).lower()
|
|
18
30
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2023 Juergen Hock
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
Source: https://github.com/jurihock/biquad
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .biquad import biquad, __df1__, __gain__, __resize__
|
|
10
|
+
|
|
11
|
+
import numba
|
|
12
|
+
import numpy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class allpass(biquad):
|
|
16
|
+
"""
|
|
17
|
+
Allpass filter (APF).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, sr, f=None, g=0, q=1):
|
|
21
|
+
"""
|
|
22
|
+
Create a new filter instance.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
sr : int or float
|
|
27
|
+
Sample rate in hertz.
|
|
28
|
+
f : int or float, optional
|
|
29
|
+
Persistent filter frequency parameter in hertz.
|
|
30
|
+
g : int or float, optional
|
|
31
|
+
Persistent filter gain parameter in decibel.
|
|
32
|
+
q : int or float, optional
|
|
33
|
+
Persistent filter quality parameter.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
super().__init__(sr=sr, f=f, g=g, q=q)
|
|
37
|
+
|
|
38
|
+
self.__call__(0) # warmup numba
|
|
39
|
+
|
|
40
|
+
def __call__(self, x, f=None, g=None, q=None):
|
|
41
|
+
"""
|
|
42
|
+
Process single or multiple contiguous signal values at once.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
x : scalar or array like
|
|
47
|
+
Filter input data.
|
|
48
|
+
f : scalar or array like, optional
|
|
49
|
+
Instantaneous filter frequency parameter in hertz.
|
|
50
|
+
g : scalar or array like, optional
|
|
51
|
+
Instantaneous filter gain parameter in decibel.
|
|
52
|
+
q : scalar or array like, optional
|
|
53
|
+
Instantaneous filter quality parameter.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
y : scalar or ndarray
|
|
58
|
+
Filter output data of the same shape and dtype as the input x.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
scalar = numpy.isscalar(x)
|
|
62
|
+
|
|
63
|
+
ba = self.ba
|
|
64
|
+
xy = self.xy
|
|
65
|
+
|
|
66
|
+
x = numpy.atleast_1d(x)
|
|
67
|
+
y = numpy.zeros(x.shape, x.dtype)
|
|
68
|
+
|
|
69
|
+
f = __resize__(self.f if f is None else f, x.shape)
|
|
70
|
+
g = __resize__(self.g if g is None else __gain__(g), x.shape)
|
|
71
|
+
q = __resize__(self.q if q is None else q, x.shape)
|
|
72
|
+
|
|
73
|
+
sr = self.sr
|
|
74
|
+
|
|
75
|
+
self.__filter__(ba, xy, x, y, f, g, q, sr)
|
|
76
|
+
|
|
77
|
+
self.f = f[-1]
|
|
78
|
+
self.g = g[-1]
|
|
79
|
+
self.q = q[-1]
|
|
80
|
+
|
|
81
|
+
return y[0] if scalar else y
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
@numba.jit(nopython=True, fastmath=True)
|
|
85
|
+
def __filter__(ba, xy, x, y, f, g, q, sr):
|
|
86
|
+
|
|
87
|
+
rs = 2 * numpy.pi / sr
|
|
88
|
+
|
|
89
|
+
for i in range(x.size):
|
|
90
|
+
|
|
91
|
+
w = f[i] * rs
|
|
92
|
+
|
|
93
|
+
cosw = numpy.cos(w)
|
|
94
|
+
sinw = numpy.sin(w)
|
|
95
|
+
|
|
96
|
+
alpha = sinw / (+2 * q[i])
|
|
97
|
+
beta = cosw * (-2)
|
|
98
|
+
|
|
99
|
+
# update b
|
|
100
|
+
ba[0, 0] = 1 - alpha
|
|
101
|
+
ba[0, 1] = beta
|
|
102
|
+
ba[0, 2] = 1 + alpha
|
|
103
|
+
|
|
104
|
+
# update a
|
|
105
|
+
ba[1, 0] = 1 + alpha
|
|
106
|
+
ba[1, 1] = beta
|
|
107
|
+
ba[1, 2] = 1 - alpha
|
|
108
|
+
|
|
109
|
+
# update y
|
|
110
|
+
__df1__(g[i], ba, xy, x, y, i)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2023 Juergen Hock
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
Source: https://github.com/jurihock/biquad
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .biquad import biquad, __df1__, __gain__, __resize__
|
|
10
|
+
|
|
11
|
+
import numba
|
|
12
|
+
import numpy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class bandpass(biquad):
|
|
16
|
+
"""
|
|
17
|
+
Bandpass filter (BPF).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, sr, f=None, g=0, q=0.7071, *, mode='peak'):
|
|
21
|
+
"""
|
|
22
|
+
Create a new filter instance.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
sr : int or float
|
|
27
|
+
Sample rate in hertz.
|
|
28
|
+
f : int or float, optional
|
|
29
|
+
Persistent filter frequency parameter in hertz.
|
|
30
|
+
g : int or float, optional
|
|
31
|
+
Persistent filter gain parameter in decibel.
|
|
32
|
+
q : int or float, optional
|
|
33
|
+
Persistent filter quality parameter.
|
|
34
|
+
mode : str, peak or skirt, optional
|
|
35
|
+
Choice between constant 0 dB peak gain or constant skirt gain.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
super().__init__(sr=sr, f=f, g=g, q=q)
|
|
39
|
+
|
|
40
|
+
assert mode in ['peak', 'skirt']
|
|
41
|
+
|
|
42
|
+
self.mode = mode
|
|
43
|
+
|
|
44
|
+
self.__call__(0) # warmup numba
|
|
45
|
+
|
|
46
|
+
def __call__(self, x, f=None, g=None, q=None):
|
|
47
|
+
"""
|
|
48
|
+
Process single or multiple contiguous signal values at once.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
x : scalar or array like
|
|
53
|
+
Filter input data.
|
|
54
|
+
f : scalar or array like, optional
|
|
55
|
+
Instantaneous filter frequency parameter in hertz.
|
|
56
|
+
g : scalar or array like, optional
|
|
57
|
+
Instantaneous filter gain parameter in decibel.
|
|
58
|
+
q : scalar or array like, optional
|
|
59
|
+
Instantaneous filter quality parameter.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
y : scalar or ndarray
|
|
64
|
+
Filter output data of the same shape and dtype as the input x.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
scalar = numpy.isscalar(x)
|
|
68
|
+
|
|
69
|
+
ba = self.ba
|
|
70
|
+
xy = self.xy
|
|
71
|
+
|
|
72
|
+
x = numpy.atleast_1d(x)
|
|
73
|
+
y = numpy.zeros(x.shape, x.dtype)
|
|
74
|
+
|
|
75
|
+
f = __resize__(self.f if f is None else f, x.shape)
|
|
76
|
+
g = __resize__(self.g if g is None else __gain__(g), x.shape)
|
|
77
|
+
q = __resize__(self.q if q is None else q, x.shape)
|
|
78
|
+
|
|
79
|
+
sr = self.sr
|
|
80
|
+
mode = self.mode
|
|
81
|
+
|
|
82
|
+
self.__filter__(ba, xy, x, y, f, g, q, sr, mode)
|
|
83
|
+
|
|
84
|
+
self.f = f[-1]
|
|
85
|
+
self.g = g[-1]
|
|
86
|
+
self.q = q[-1]
|
|
87
|
+
|
|
88
|
+
return y[0] if scalar else y
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
@numba.jit(nopython=True, fastmath=True)
|
|
92
|
+
def __filter__(ba, xy, x, y, f, g, q, sr, mode):
|
|
93
|
+
|
|
94
|
+
rs = 2 * numpy.pi / sr
|
|
95
|
+
skirt = mode == 'skirt'
|
|
96
|
+
|
|
97
|
+
for i in range(x.size):
|
|
98
|
+
|
|
99
|
+
w = f[i] * rs
|
|
100
|
+
|
|
101
|
+
cosw = numpy.cos(w)
|
|
102
|
+
sinw = numpy.sin(w)
|
|
103
|
+
|
|
104
|
+
alpha = sinw / (+2 * q[i])
|
|
105
|
+
beta = cosw * (-2)
|
|
106
|
+
gamma = sinw / (+2) if skirt else alpha
|
|
107
|
+
|
|
108
|
+
# update b
|
|
109
|
+
ba[0, 0] = +gamma
|
|
110
|
+
ba[0, 1] = 0
|
|
111
|
+
ba[0, 2] = -gamma
|
|
112
|
+
|
|
113
|
+
# update a
|
|
114
|
+
ba[1, 0] = 1 + alpha
|
|
115
|
+
ba[1, 1] = beta
|
|
116
|
+
ba[1, 2] = 1 - alpha
|
|
117
|
+
|
|
118
|
+
# update y
|
|
119
|
+
__df1__(g[i], ba, xy, x, y, i)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2023 Juergen Hock
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
Source: https://github.com/jurihock/biquad
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numba
|
|
10
|
+
import numpy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@numba.jit(nopython=True, fastmath=True)
|
|
14
|
+
def __df1__(g, ba, xy, x, y, i):
|
|
15
|
+
"""
|
|
16
|
+
Compute filter output y[i] based on filter input x[i]
|
|
17
|
+
as well as specified filter gain g, coeffs ba and delay line xy,
|
|
18
|
+
according to the Direct Form 1.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# roll x
|
|
22
|
+
xy[0, 2] = xy[0, 1]
|
|
23
|
+
xy[0, 1] = xy[0, 0]
|
|
24
|
+
|
|
25
|
+
# roll y
|
|
26
|
+
xy[1, 2] = xy[1, 1]
|
|
27
|
+
xy[1, 1] = xy[1, 0]
|
|
28
|
+
|
|
29
|
+
# update x
|
|
30
|
+
xy[0, 0] = x[i]
|
|
31
|
+
|
|
32
|
+
# compute intermediate results b*x and a*y
|
|
33
|
+
bx = ba[0, 0] * xy[0, 0] + ba[0, 1] * xy[0, 1] + ba[0, 2] * xy[0, 2]
|
|
34
|
+
ay = ba[1, 1] * xy[1, 1] + ba[1, 2] * xy[1, 2]
|
|
35
|
+
|
|
36
|
+
# update y
|
|
37
|
+
xy[1, 0] = (bx * g - ay) / ba[1, 0]
|
|
38
|
+
|
|
39
|
+
# return y
|
|
40
|
+
y[i] = xy[1, 0]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __gain__(x, divisor=20):
|
|
44
|
+
"""
|
|
45
|
+
Convert the specified decibel gain to the linear gain.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
if numpy.isscalar(x):
|
|
49
|
+
|
|
50
|
+
y = x / divisor
|
|
51
|
+
y = 10 ** y
|
|
52
|
+
|
|
53
|
+
else:
|
|
54
|
+
|
|
55
|
+
y = numpy.atleast_1d(x) / divisor
|
|
56
|
+
numpy.power(10, y, out=y)
|
|
57
|
+
|
|
58
|
+
return y
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def __resize__(x, shape):
|
|
62
|
+
"""
|
|
63
|
+
Resize the specified value to the ndarray of desired shape.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if numpy.isscalar(x):
|
|
67
|
+
|
|
68
|
+
y = numpy.full(shape, x)
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
|
|
72
|
+
x = numpy.atleast_1d(x)
|
|
73
|
+
y = numpy.resize(x, shape)
|
|
74
|
+
|
|
75
|
+
return y
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class biquad:
|
|
79
|
+
"""
|
|
80
|
+
Biquad filter base class.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
ba = numpy.array([[1, 0, 0], [1, 0, 0]], float)
|
|
84
|
+
"""
|
|
85
|
+
Biquad filter coefficient matrix of shape (2, 3) excl. gain factor:
|
|
86
|
+
- ba[0] holds b coefficients
|
|
87
|
+
- ba[1] holds a coefficients
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
xy = numpy.array([[0, 0, 0], [0, 0, 0]], float)
|
|
91
|
+
"""
|
|
92
|
+
Biquad filter delay line matrix of shape (2, 3):
|
|
93
|
+
- xy[0] holds input values
|
|
94
|
+
- xy[1] holds output values
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, sr, f=None, g=None, q=None):
|
|
98
|
+
"""
|
|
99
|
+
Create a new filter instance.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
sr : int or float
|
|
104
|
+
Sample rate in hertz.
|
|
105
|
+
f : int or float, optional
|
|
106
|
+
Persistent filter frequency parameter in hertz.
|
|
107
|
+
g : int or float, optional
|
|
108
|
+
Persistent filter gain parameter in decibel.
|
|
109
|
+
q : int or float, optional
|
|
110
|
+
Persistent filter quality parameter.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
assert (sr is not None) and (numpy.isscalar(sr) and numpy.isreal(sr))
|
|
114
|
+
assert (f is None) or (numpy.isscalar(f) and numpy.isreal(f))
|
|
115
|
+
assert (g is None) or (numpy.isscalar(g) and numpy.isreal(g))
|
|
116
|
+
assert (q is None) or (numpy.isscalar(q) and numpy.isreal(q))
|
|
117
|
+
|
|
118
|
+
self.sr = sr
|
|
119
|
+
self.f = (sr / 4) if f is None else f
|
|
120
|
+
self.g = __gain__(0 if g is None else g)
|
|
121
|
+
self.q = 1 if q is None else q
|
|
122
|
+
|
|
123
|
+
# warmup numba
|
|
124
|
+
g = self.g
|
|
125
|
+
ba = self.ba
|
|
126
|
+
xy = self.xy
|
|
127
|
+
x = numpy.zeros(1, float)
|
|
128
|
+
y = numpy.zeros(x.shape, x.dtype)
|
|
129
|
+
__df1__(g, ba, xy, x, y, 0)
|
|
130
|
+
|
|
131
|
+
def __call__(self, x, f=None, g=None, q=None):
|
|
132
|
+
"""
|
|
133
|
+
Process single or multiple contiguous signal values at once.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
x : scalar or array like
|
|
138
|
+
Filter input data.
|
|
139
|
+
f : scalar or array like, optional
|
|
140
|
+
Instantaneous filter frequency parameter in hertz.
|
|
141
|
+
g : scalar or array like, optional
|
|
142
|
+
Instantaneous filter gain parameter in decibel.
|
|
143
|
+
q : scalar or array like, optional
|
|
144
|
+
Instantaneous filter quality parameter.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
y : scalar or ndarray
|
|
149
|
+
Filter output data of the same shape and dtype as the input x.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
scalar = numpy.isscalar(x)
|
|
153
|
+
|
|
154
|
+
ba = self.ba
|
|
155
|
+
xy = self.xy
|
|
156
|
+
|
|
157
|
+
x = numpy.atleast_1d(x)
|
|
158
|
+
y = numpy.zeros(x.shape, x.dtype)
|
|
159
|
+
|
|
160
|
+
f = __resize__(self.f if f is None else f, x.shape)
|
|
161
|
+
g = __resize__(self.g if g is None else __gain__(g), x.shape)
|
|
162
|
+
q = __resize__(self.q if q is None else q, x.shape)
|
|
163
|
+
|
|
164
|
+
self.__filter__(ba, xy, x, y, g)
|
|
165
|
+
|
|
166
|
+
self.f = f[-1]
|
|
167
|
+
self.g = g[-1]
|
|
168
|
+
self.q = q[-1]
|
|
169
|
+
|
|
170
|
+
return y[0] if scalar else y
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
@numba.jit(nopython=True, fastmath=True)
|
|
174
|
+
def __filter__(ba, xy, x, y, g):
|
|
175
|
+
|
|
176
|
+
for i in range(x.size):
|
|
177
|
+
|
|
178
|
+
__df1__(g[i], ba, xy, x, y, i)
|
|
179
|
+
|
|
180
|
+
def response(self, *, norm=False, log=False):
|
|
181
|
+
"""
|
|
182
|
+
Returns frequency and phase response of the transfer function given by the ba coefficients.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
norm : bool, optional
|
|
187
|
+
Option whether to normalize the output frequency response.
|
|
188
|
+
log : bool, optional
|
|
189
|
+
Option whether to express the output frequency values logarithmically.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
w : array
|
|
194
|
+
Corresponding frequency values.
|
|
195
|
+
h : array
|
|
196
|
+
Complex filter response values.
|
|
197
|
+
|
|
198
|
+
See also
|
|
199
|
+
--------
|
|
200
|
+
scipy.signal.freqz
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
(b, a), sr = self.ba, self.sr
|
|
204
|
+
|
|
205
|
+
b *= self.g
|
|
206
|
+
|
|
207
|
+
n = int(sr / 2)
|
|
208
|
+
|
|
209
|
+
# compute frequencies from 0 to pi or sr/2 but excluding the Nyquist frequency
|
|
210
|
+
w = numpy.linspace(0, numpy.pi, n, endpoint=False) \
|
|
211
|
+
if not log else \
|
|
212
|
+
numpy.logspace(numpy.log10(1), numpy.log10(numpy.pi), n, endpoint=False, base=10)
|
|
213
|
+
|
|
214
|
+
# compute the z-domain transfer function
|
|
215
|
+
z = numpy.exp(-1j * w)
|
|
216
|
+
x = numpy.polynomial.polynomial.polyval(z, a, tensor=False)
|
|
217
|
+
y = numpy.polynomial.polynomial.polyval(z, b, tensor=False)
|
|
218
|
+
h = y / x
|
|
219
|
+
|
|
220
|
+
# normalize frequency amplitudes
|
|
221
|
+
h /= len(h) if norm else 1
|
|
222
|
+
|
|
223
|
+
# normalize frequency values according to sr
|
|
224
|
+
w = (w * sr) / (2 * numpy.pi)
|
|
225
|
+
|
|
226
|
+
return w, h
|
|
227
|
+
|
|
228
|
+
def plot(self, *, log=False):
|
|
229
|
+
"""
|
|
230
|
+
Creates filter frequency and phase response plot.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
log : bool, optional
|
|
235
|
+
Option whether to express frequency values logarithmically.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
import matplotlib.pyplot as pyplot
|
|
239
|
+
|
|
240
|
+
w, h = self.response()
|
|
241
|
+
|
|
242
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
|
243
|
+
habs = 20 * numpy.log10(numpy.abs(h))
|
|
244
|
+
|
|
245
|
+
harg = numpy.angle(h)
|
|
246
|
+
|
|
247
|
+
pyplot.plot(w, habs, color='b', alpha=0.9)
|
|
248
|
+
axis1 = pyplot.gca()
|
|
249
|
+
axis2 = axis1.twinx()
|
|
250
|
+
axis2.plot(w, harg, color='g', alpha=0.9)
|
|
251
|
+
|
|
252
|
+
axis1.set_xlabel('Hz')
|
|
253
|
+
axis1.set_ylabel('dB', color='b')
|
|
254
|
+
axis2.set_ylabel('rad', color='g')
|
|
255
|
+
|
|
256
|
+
habsmin = numpy.min(habs[numpy.isfinite(habs)])
|
|
257
|
+
habsmax = numpy.max(habs[numpy.isfinite(habs)])
|
|
258
|
+
|
|
259
|
+
habsmin = numpy.minimum(habsmin, -100)
|
|
260
|
+
habsmax = numpy.maximum(habsmax, +10)
|
|
261
|
+
|
|
262
|
+
hargmin = -numpy.pi
|
|
263
|
+
hargmax = +numpy.pi
|
|
264
|
+
|
|
265
|
+
axis1.set_ylim((habsmin * 1.1, habsmax * 1.1))
|
|
266
|
+
axis2.set_ylim((hargmin * 1.1, hargmax * 1.1))
|
|
267
|
+
|
|
268
|
+
if log: axis1.set_xscale('log')
|
|
269
|
+
|
|
270
|
+
return pyplot
|