Skip to content

Commit 33a3e2e

Browse files
authored
Merge branch 'master' into add-fast-walsh-hadamard-transform
2 parents c0a429f + f3f599a commit 33a3e2e

20 files changed

Lines changed: 1150 additions & 101 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
cache-dependency-glob: uv.lock
1818
- uses: actions/setup-python@v7
1919
with:
20-
python-version: 3.14
20+
python-version-file: pyproject.toml
2121
allow-prereleases: true
2222
- run: uv sync --group=test
2323
- name: Run tests

.github/workflows/directory_writer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
fetch-depth: 0
1212
- uses: actions/setup-python@v7
1313
with:
14-
python-version: 3.14
14+
python-version-file: pyproject.toml
1515
allow-prereleases: true
1616
- name: Write DIRECTORY.md
1717
run: |

.github/workflows/project_euler.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- uses: astral-sh/setup-uv@v7
2626
- uses: actions/setup-python@v7
2727
with:
28-
python-version: 3.14
28+
python-version-file: pyproject.toml
2929
allow-prereleases: true
3030
- run: uv sync --group=euler-validate --group=test
3131
- run: uv run pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/
@@ -43,7 +43,7 @@ jobs:
4343
- uses: astral-sh/setup-uv@v7
4444
- uses: actions/setup-python@v7
4545
with:
46-
python-version: 3.14
46+
python-version-file: pyproject.toml
4747
allow-prereleases: true
4848
- run: uv sync --group=euler-validate --group=test
4949
- run: uv run pytest scripts/validate_solutions.py

.github/workflows/sphinx.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
- uses: astral-sh/setup-uv@v7
3737
- uses: actions/setup-python@v7
3838
with:
39-
python-version: 3.14
39+
python-version-file: pyproject.toml
4040
allow-prereleases: true
4141
- run: uv sync --group=docs
4242
- uses: actions/configure-pages@v6

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
ci:
22
autoupdate_schedule: monthly
3+
# uv-lock resolves dependencies from PyPI, but pre-commit.ci runs hooks with no
4+
# network access, so it fails there. Skip it on pre-commit.ci; it still runs locally.
5+
skip: [uv-lock]
36

47
repos:
58
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -18,6 +21,11 @@ repos:
1821
hooks:
1922
- id: auto-walrus
2023

24+
- repo: https://github.com/astral-sh/uv-pre-commit
25+
rev: 0.12.7
26+
hooks:
27+
- id: uv-lock
28+
2129
- repo: https://github.com/astral-sh/ruff-pre-commit
2230
rev: v0.16.1
2331
hooks:

audio_filters/README.md

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,58 @@
1-
# Audio Filter
1+
# Audio Filters
22

3-
Audio filters work on the frequency of an audio signal to attenuate unwanted frequency and amplify wanted ones.
4-
They are used within anything related to sound, whether it is radio communication or a hi-fi system.
3+
Audio filters work on the frequency of an audio signal to attenuate unwanted
4+
frequencies and amplify wanted ones. They are used within anything related to
5+
sound, whether it is radio communication or a hi-fi system. If you have ever
6+
turned up the bass or cut the treble on a stereo, tuned a radio to a station, or
7+
removed the background hum from a recording, you have used an audio filter.
8+
9+
Curious to learn more? These are great starting points:
510

611
* <https://www.masteringbox.com/filter-types/>
712
* <http://ethanwiner.com/filters.html>
813
* <https://en.wikipedia.org/wiki/Audio_filter>
914
* <https://en.wikipedia.org/wiki/Electronic_filter>
15+
* <https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html>
16+
17+
## What's in this directory
18+
19+
| File | Description |
20+
| ---- | ----------- |
21+
| [`iir_filter.py`](iir_filter.py) | A generic N-order [Infinite Impulse Response (IIR)](https://en.wikipedia.org/wiki/Infinite_impulse_response) filter. This is the engine every filter below runs on: give it a set of coefficients and it processes a stream of samples one at a time. |
22+
| [`butterworth_filter.py`](butterworth_filter.py) | A collection of second-order [Butterworth](https://en.wikipedia.org/wiki/Butterworth_filter) / biquad filter designs from the RBJ Audio EQ Cookbook. Each function returns a ready-to-use `IIRFilter`. |
23+
| [`equal_loudness_filter.py`](equal_loudness_filter.py) | An [equal-loudness](https://en.wikipedia.org/wiki/Equal-loudness_contour) filter that compensates for the human ear's non-linear response to sound by cascading a Yule-Walker filter and a Butterworth high-pass filter. Includes a dependency-free `yulewalk` implementation. |
24+
| [`show_response.py`](show_response.py) | Helpers to plot the [magnitude and phase response](https://en.wikipedia.org/wiki/Frequency_response) of any filter so you can *see* what it does. |
25+
| [`loudness_curve.json`](loudness_curve.json) | The Robinson-Dadson equal-loudness contour data used by the equal-loudness filter. |
26+
27+
## Filter designs in `butterworth_filter.py`
28+
29+
| Function | Effect |
30+
| -------- | ------ |
31+
| `make_lowpass` | Passes frequencies below the cutoff, attenuates those above it. |
32+
| `make_highpass` | Passes frequencies above the cutoff, attenuates those below it. |
33+
| `make_bandpass` | Passes a band of frequencies around the center (constant skirt gain). |
34+
| `make_bandpass_peak` | Passes a band of frequencies around the center (constant 0 dB peak gain). |
35+
| `make_notch` | Rejects a narrow band around the center — great for removing mains hum. |
36+
| `make_allpass` | Passes all frequencies but changes their phase relationship. |
37+
| `make_peak` | Boosts or cuts a band around the center by a given gain (parametric EQ). |
38+
| `make_lowshelf` | Boosts or cuts everything below the cutoff. |
39+
| `make_highshelf` | Boosts or cuts everything above the cutoff. |
40+
41+
## Try it out
42+
43+
```python
44+
from audio_filters.butterworth_filter import make_lowpass
45+
from audio_filters.show_response import show_frequency_response
46+
47+
# A 5 kHz low-pass filter for CD-quality audio (44.1 kHz sample rate)
48+
filt = make_lowpass(5000, 44100)
49+
50+
# Process samples one at a time...
51+
filtered = [filt.process(sample) for sample in my_audio_samples]
52+
53+
# ...or visualise what the filter does to the spectrum:
54+
show_frequency_response(make_lowpass(5000, 44100), 44100)
55+
```
56+
57+
Every module has runnable doctests — read them for concrete, copy-pasteable
58+
examples of each filter in action.

audio_filters/butterworth_filter.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
88
Code based on https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
99
Alternatively you can use scipy.signal.butter, which should yield the same results.
10+
11+
https://en.wikipedia.org/wiki/Butterworth_filter
12+
13+
Notation used throughout this module (from the RBJ Audio EQ Cookbook):
14+
w0 -- normalised angular frequency, ``2 * pi * frequency / samplerate``
15+
alpha -- bandwidth parameter, ``sin(w0) / (2 * q_factor)``
16+
b0..b2 -- feed-forward (numerator) coefficients of the biquad
17+
a0..a2 -- feed-back (denominator) coefficients of the biquad
18+
The a/b coefficient names match ``IIRFilter.set_coefficients`` and the standard
19+
biquad transfer function, so they are kept consistent across every filter here.
1020
"""
1121

1222

@@ -232,3 +242,81 @@ def make_highshelf(
232242
filt = IIRFilter(2)
233243
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
234244
return filt
245+
246+
247+
def make_notch(
248+
frequency: int,
249+
samplerate: int,
250+
q_factor: float = 1 / sqrt(2),
251+
) -> IIRFilter:
252+
"""
253+
Creates a notch (band-reject) filter that strongly attenuates a narrow band
254+
of frequencies around ``frequency`` while leaving the rest of the spectrum
255+
unchanged. It is the complement of the band-pass filter and is commonly used
256+
to remove a single tone such as 50/60 Hz mains hum.
257+
258+
https://en.wikipedia.org/wiki/Band-stop_filter
259+
260+
>>> filter = make_notch(1000, 48000)
261+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
262+
[1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 1.0,
263+
-1.9828897227476208, 1.0]
264+
"""
265+
w0 = tau * frequency / samplerate # centre frequency, in radians/sample
266+
_sin = sin(w0)
267+
_cos = cos(w0)
268+
alpha = _sin / (2 * q_factor) # controls how narrow the rejected band is
269+
270+
# Feed-forward: a pair of zeros placed exactly on the notch frequency, so
271+
# that frequency is fully cancelled while the rest of the spectrum passes.
272+
b0 = 1.0
273+
b1 = -2 * _cos
274+
b2 = 1.0
275+
276+
# Feed-back: matching poles just inside the unit circle keep the notch
277+
# narrow and the surrounding gain flat.
278+
a0 = 1 + alpha
279+
a1 = -2 * _cos
280+
a2 = 1 - alpha
281+
282+
filt = IIRFilter(2)
283+
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
284+
return filt
285+
286+
287+
def make_bandpass_peak(
288+
frequency: int,
289+
samplerate: int,
290+
q_factor: float = 1 / sqrt(2),
291+
) -> IIRFilter:
292+
"""
293+
Creates a band-pass filter with constant 0 dB peak gain.
294+
295+
Unlike :func:`make_bandpass`, whose skirt (edge) gain is held constant so the
296+
peak gain grows with ``q_factor``, this variant normalises the response so
297+
the peak always reaches 0 dB regardless of the chosen ``q_factor``. Both
298+
forms come from the RBJ Audio EQ Cookbook.
299+
300+
https://en.wikipedia.org/wiki/Band-pass_filter
301+
302+
>>> filter = make_bandpass_peak(1000, 48000)
303+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
304+
[1.0922959556412573, -1.9828897227476208, 0.9077040443587427,
305+
0.09229595564125725, 0, -0.09229595564125725]
306+
"""
307+
w0 = tau * frequency / samplerate
308+
_sin = sin(w0)
309+
_cos = cos(w0)
310+
alpha = _sin / (2 * q_factor)
311+
312+
b0 = alpha
313+
b1 = 0
314+
b2 = -alpha
315+
316+
a0 = 1 + alpha
317+
a1 = -2 * _cos
318+
a2 = 1 - alpha
319+
320+
filt = IIRFilter(2)
321+
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
322+
return filt

0 commit comments

Comments
 (0)