Skip to content

Commit fed8d58

Browse files
committed
synthio: add biquad filter type & basic filter calculations
the filter cannot be applied as yet.
1 parent 0aaf5a4 commit fed8d58

File tree

9 files changed

+341
-0
lines changed

9 files changed

+341
-0
lines changed

ports/unix/variants/coverage/mpconfigvariant.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ SRC_BITMAP := \
4545
shared-bindings/synthio/MidiTrack.c \
4646
shared-bindings/synthio/LFO.c \
4747
shared-bindings/synthio/Note.c \
48+
shared-bindings/synthio/Biquad.c \
4849
shared-bindings/synthio/Synthesizer.c \
4950
shared-bindings/traceback/__init__.c \
5051
shared-bindings/util.c \
@@ -70,6 +71,7 @@ SRC_BITMAP := \
7071
shared-module/synthio/MidiTrack.c \
7172
shared-module/synthio/LFO.c \
7273
shared-module/synthio/Note.c \
74+
shared-module/synthio/Biquad.c \
7375
shared-module/synthio/Synthesizer.c \
7476
shared-module/traceback/__init__.c \
7577
shared-module/zlib/__init__.c \

py/circuitpy_defns.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ SRC_SHARED_MODULE_ALL = \
650650
struct/__init__.c \
651651
supervisor/__init__.c \
652652
supervisor/StatusBar.c \
653+
synthio/Biquad.c \
653654
synthio/LFO.c \
654655
synthio/Math.c \
655656
synthio/MidiTrack.c \

shared-bindings/synthio/Biquad.c

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* This file is part of the MicroPython project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2021 Artyom Skrobov
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
#include <math.h>
28+
#include <string.h>
29+
30+
#include "py/enum.h"
31+
#include "py/mperrno.h"
32+
#include "py/obj.h"
33+
#include "py/objnamedtuple.h"
34+
#include "py/runtime.h"
35+
36+
#include "shared-bindings/synthio/__init__.h"
37+
#include "shared-bindings/synthio/LFO.h"
38+
#include "shared-bindings/synthio/Math.h"
39+
#include "shared-bindings/synthio/MidiTrack.h"
40+
#include "shared-bindings/synthio/Note.h"
41+
#include "shared-bindings/synthio/Synthesizer.h"
42+
43+
#include "shared-module/synthio/LFO.h"
44+
45+
#define default_attack_time (MICROPY_FLOAT_CONST(0.1))
46+
#define default_decay_time (MICROPY_FLOAT_CONST(0.05))
47+
#define default_release_time (MICROPY_FLOAT_CONST(0.2))
48+
#define default_attack_level (MICROPY_FLOAT_CONST(1.))
49+
#define default_sustain_level (MICROPY_FLOAT_CONST(0.8))
50+
51+
static const mp_arg_t biquad_properties[] = {
52+
{ MP_QSTR_a1, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
53+
{ MP_QSTR_a2, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
54+
{ MP_QSTR_b0, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
55+
{ MP_QSTR_b1, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
56+
{ MP_QSTR_b2, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
57+
};
58+
59+
//| class Biquad:
60+
//| def __init__(self, b0: float, b1: float, b2: float, a1: float, a2: float) -> None:
61+
//| """Construct a normalized biquad filter object.
62+
//|
63+
//| This implements the "direct form 1" biquad filter, where each coefficient
64+
//| has been pre-divided by a0.
65+
//|
66+
//| Biquad objects are usually constructed via one of the related methods on a `Synthesizer` object
67+
//| rather than directly from coefficients.
68+
//|
69+
//| https://github.com/WebAudio/Audio-EQ-Cookbook/blob/main/Audio-EQ-Cookbook.txt
70+
//| """
71+
//|
72+
STATIC mp_obj_t synthio_biquad_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
73+
mp_arg_val_t args[MP_ARRAY_SIZE(biquad_properties)];
74+
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(biquad_properties), biquad_properties, args);
75+
76+
for (size_t i = 0; i < MP_ARRAY_SIZE(biquad_properties); i++) {
77+
args[i].u_obj = mp_obj_new_float(mp_arg_validate_type_float(args[i].u_obj, biquad_properties[i].qst));
78+
}
79+
80+
MP_STATIC_ASSERT(sizeof(mp_arg_val_t) == sizeof(mp_obj_t));
81+
return namedtuple_make_new(type_in, MP_ARRAY_SIZE(args), 0, &args[0].u_obj);
82+
}
83+
84+
const mp_obj_namedtuple_type_t synthio_biquad_type_obj = {
85+
.base = {
86+
.base = {
87+
.type = &mp_type_type
88+
},
89+
.flags = MP_TYPE_FLAG_EXTENDED,
90+
.name = MP_QSTR_Biquad,
91+
.print = namedtuple_print,
92+
.parent = &mp_type_tuple,
93+
.make_new = synthio_biquad_make_new,
94+
.attr = namedtuple_attr,
95+
MP_TYPE_EXTENDED_FIELDS(
96+
.unary_op = mp_obj_tuple_unary_op,
97+
.binary_op = mp_obj_tuple_binary_op,
98+
.subscr = mp_obj_tuple_subscr,
99+
.getiter = mp_obj_tuple_getiter,
100+
),
101+
},
102+
.n_fields = 5,
103+
.fields = {
104+
MP_QSTR_a1,
105+
MP_QSTR_a2,
106+
MP_QSTR_b0,
107+
MP_QSTR_b1,
108+
MP_QSTR_b2,
109+
},
110+
};

shared-bindings/synthio/Biquad.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
#include "py/obj.h"
4+
#include "py/objnamedtuple.h"
5+
6+
extern const mp_obj_namedtuple_type_t synthio_biquad_type_obj;
7+
mp_obj_t common_hal_synthio_new_lpf(mp_float_t w0, mp_float_t Q);
8+
mp_obj_t common_hal_synthio_new_hpf(mp_float_t w0, mp_float_t Q);
9+
mp_obj_t common_hal_synthio_new_bpf(mp_float_t w0, mp_float_t Q);

shared-bindings/synthio/Synthesizer.c

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "py/objproperty.h"
3333
#include "py/runtime.h"
3434
#include "shared-bindings/util.h"
35+
#include "shared-bindings/synthio/Biquad.h"
3536
#include "shared-bindings/synthio/Synthesizer.h"
3637
#include "shared-bindings/synthio/LFO.h"
3738
#include "shared-bindings/synthio/__init__.h"
@@ -292,6 +293,110 @@ MP_PROPERTY_GETTER(synthio_synthesizer_blocks_obj,
292293
//| """Maximum polyphony of the synthesizer (read-only class property)"""
293294
//|
294295

296+
//| def low_pass_filter(cls, cutoff_frequency, q_factor: float = 1 / math.sqrt(2)) -> Biquad:
297+
//| """Construct a low-pass filter with the given parameters.
298+
//|
299+
//| `frequency`, called f0 in the cookbook, is the corner frequency in Hz
300+
//| of the filter.
301+
//|
302+
//| `q_factor`, called `Q` in the cookbook. Controls how peaked the response will be at the cutoff frequency. A large value makes the response more peaked.
303+
//| """
304+
305+
enum passfilter_arg_e { ARG_f0, ARG_Q };
306+
307+
// M_PI is not part of the math.h standard and may not be defined
308+
// And by defining our own we can ensure it uses the correct const format.
309+
#define MP_PI MICROPY_FLOAT_CONST(3.14159265358979323846)
310+
311+
static const mp_arg_t passfilter_properties[] = {
312+
{ MP_QSTR_frequency, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
313+
{ MP_QSTR_Q, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL } },
314+
};
315+
316+
STATIC mp_obj_t synthio_synthesizer_lpf(size_t n_pos, const mp_obj_t *pos_args, mp_map_t *kw_args) {
317+
mp_arg_val_t args[MP_ARRAY_SIZE(passfilter_properties)];
318+
319+
mp_obj_t self_in = pos_args[0];
320+
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
321+
322+
mp_arg_parse_all(n_pos - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(passfilter_properties), passfilter_properties, args);
323+
324+
mp_float_t f0 = mp_arg_validate_type_float(args[ARG_f0].u_obj, MP_QSTR_f0);
325+
mp_float_t Q =
326+
args[ARG_Q].u_obj == MP_OBJ_NULL ? MICROPY_FLOAT_CONST(0.7071067811865475) :
327+
mp_arg_validate_type_float(args[ARG_Q].u_obj, MP_QSTR_Q);
328+
329+
mp_float_t w0 = f0 / self->synth.sample_rate * 2 * MP_PI;
330+
331+
return common_hal_synthio_new_lpf(w0, Q);
332+
333+
}
334+
335+
MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_lpf_fun_obj, 1, synthio_synthesizer_lpf);
336+
337+
//| def high_pass_filter(cls, cutoff_frequency, q_factor: float = 1 / math.sqrt(2)) -> Biquad:
338+
//| """Construct a high-pass filter with the given parameters.
339+
//|
340+
//| `frequency`, called f0 in the cookbook, is the corner frequency in Hz
341+
//| of the filter.
342+
//|
343+
//| `q_factor`, called `Q` in the cookbook. Controls how peaked the response will be at the cutoff frequency. A large value makes the response more peaked.
344+
//| """
345+
346+
STATIC mp_obj_t synthio_synthesizer_hpf(size_t n_pos, const mp_obj_t *pos_args, mp_map_t *kw_args) {
347+
mp_arg_val_t args[MP_ARRAY_SIZE(passfilter_properties)];
348+
349+
mp_obj_t self_in = pos_args[0];
350+
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
351+
352+
mp_arg_parse_all(n_pos - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(passfilter_properties), passfilter_properties, args);
353+
354+
mp_float_t f0 = mp_arg_validate_type_float(args[ARG_f0].u_obj, MP_QSTR_f0);
355+
mp_float_t Q =
356+
args[ARG_Q].u_obj == MP_OBJ_NULL ? MICROPY_FLOAT_CONST(0.7071067811865475) :
357+
mp_arg_validate_type_float(args[ARG_Q].u_obj, MP_QSTR_Q);
358+
359+
mp_float_t w0 = f0 / self->synth.sample_rate * 2 * MP_PI;
360+
361+
return common_hal_synthio_new_hpf(w0, Q);
362+
363+
}
364+
365+
//| def band_pass_filter(cls, frequency, q_factor: float = 1 / math.sqrt(2)) -> Biquad:
366+
//| """Construct a band-pass filter with the given parameters.
367+
//|
368+
//| `frequency`, called f0 in the cookbook, is the center frequency in Hz
369+
//| of the filter.
370+
//|
371+
//| `q_factor`, called `Q` in the cookbook. Controls how peaked the response will be at the cutoff frequency. A large value makes the response more peaked.
372+
//|
373+
//| The coefficients are scaled such that the filter has a 0dB peak gain.
374+
//| """
375+
//|
376+
377+
MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_hpf_fun_obj, 1, synthio_synthesizer_hpf);
378+
379+
STATIC mp_obj_t synthio_synthesizer_bpf(size_t n_pos, const mp_obj_t *pos_args, mp_map_t *kw_args) {
380+
mp_arg_val_t args[MP_ARRAY_SIZE(passfilter_properties)];
381+
382+
mp_obj_t self_in = pos_args[0];
383+
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
384+
385+
mp_arg_parse_all(n_pos - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(passfilter_properties), passfilter_properties, args);
386+
387+
mp_float_t f0 = mp_arg_validate_type_float(args[ARG_f0].u_obj, MP_QSTR_f0);
388+
mp_float_t Q =
389+
args[ARG_Q].u_obj == MP_OBJ_NULL ? MICROPY_FLOAT_CONST(0.7071067811865475) :
390+
mp_arg_validate_type_float(args[ARG_Q].u_obj, MP_QSTR_Q);
391+
392+
mp_float_t w0 = f0 / self->synth.sample_rate * 2 * MP_PI;
393+
394+
return common_hal_synthio_new_bpf(w0, Q);
395+
396+
}
397+
398+
MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_bpf_fun_obj, 1, synthio_synthesizer_bpf);
399+
295400
STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
296401
// Methods
297402
{ MP_ROM_QSTR(MP_QSTR_press), MP_ROM_PTR(&synthio_synthesizer_press_obj) },
@@ -304,6 +409,9 @@ STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
304409
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
305410
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&synthio_synthesizer___exit___obj) },
306411

412+
{ MP_ROM_QSTR(MP_QSTR_low_pass_filter), MP_ROM_PTR(&synthio_synthesizer_lpf_fun_obj) },
413+
{ MP_ROM_QSTR(MP_QSTR_high_pass_filter), MP_ROM_PTR(&synthio_synthesizer_hpf_fun_obj) },
414+
{ MP_ROM_QSTR(MP_QSTR_band_pass_filter), MP_ROM_PTR(&synthio_synthesizer_bpf_fun_obj) },
307415
// Properties
308416
{ MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_synthesizer_envelope_obj) },
309417
{ MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_synthesizer_sample_rate_obj) },

shared-bindings/synthio/__init__.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "extmod/vfs_posix.h"
3737

3838
#include "shared-bindings/synthio/__init__.h"
39+
#include "shared-bindings/synthio/Biquad.h"
3940
#include "shared-bindings/synthio/LFO.h"
4041
#include "shared-bindings/synthio/Math.h"
4142
#include "shared-bindings/synthio/MidiTrack.h"
@@ -310,6 +311,7 @@ MP_DEFINE_CONST_FUN_OBJ_VAR(synthio_lfo_tick_obj, 1, synthio_lfo_tick);
310311

311312
STATIC const mp_rom_map_elem_t synthio_module_globals_table[] = {
312313
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_synthio) },
314+
{ MP_ROM_QSTR(MP_QSTR_Biquad), MP_ROM_PTR(&synthio_biquad_type_obj) },
313315
{ MP_ROM_QSTR(MP_QSTR_Math), MP_ROM_PTR(&synthio_math_type) },
314316
{ MP_ROM_QSTR(MP_QSTR_MathOperation), MP_ROM_PTR(&synthio_math_operation_type) },
315317
{ MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) },

shared-module/synthio/Biquad.c

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* This file is part of the Micro Python project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2023 Jeff Epler for Adafruit Industries
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
#include <math.h>
28+
#include "shared-bindings/synthio/Biquad.h"
29+
30+
mp_obj_t common_hal_synthio_new_lpf(mp_float_t w0, mp_float_t Q) {
31+
mp_float_t s = MICROPY_FLOAT_C_FUN(sin)(w0);
32+
mp_float_t c = MICROPY_FLOAT_C_FUN(cos)(w0);
33+
mp_float_t alpha = s / (2 * Q);
34+
mp_float_t a0 = 1 + alpha;
35+
mp_float_t a1 = -2 * c;
36+
mp_float_t a2 = 1 - alpha;
37+
mp_float_t b0 = (1 - c) / 2;
38+
mp_float_t b1 = 1 - c;
39+
mp_float_t b2 = (1 - c) / 2;
40+
41+
mp_obj_t out_args[] = {
42+
mp_obj_new_float(a1 / a0),
43+
mp_obj_new_float(a2 / a0),
44+
mp_obj_new_float(b0 / a0),
45+
mp_obj_new_float(b1 / a0),
46+
mp_obj_new_float(b2 / a0),
47+
};
48+
49+
return namedtuple_make_new((const mp_obj_type_t *)&synthio_biquad_type_obj, MP_ARRAY_SIZE(out_args), 0, out_args);
50+
}
51+
52+
mp_obj_t common_hal_synthio_new_hpf(mp_float_t w0, mp_float_t Q) {
53+
mp_float_t s = MICROPY_FLOAT_C_FUN(sin)(w0);
54+
mp_float_t c = MICROPY_FLOAT_C_FUN(cos)(w0);
55+
mp_float_t alpha = s / (2 * Q);
56+
mp_float_t a0 = 1 + alpha;
57+
mp_float_t a1 = -2 * c;
58+
mp_float_t a2 = 1 - alpha;
59+
mp_float_t b0 = (1 + c) / 2;
60+
mp_float_t b1 = -(1 + c);
61+
mp_float_t b2 = (1 + c) / 2;
62+
63+
mp_obj_t out_args[] = {
64+
mp_obj_new_float(a1 / a0),
65+
mp_obj_new_float(a2 / a0),
66+
mp_obj_new_float(b0 / a0),
67+
mp_obj_new_float(b1 / a0),
68+
mp_obj_new_float(b2 / a0),
69+
};
70+
71+
return namedtuple_make_new((const mp_obj_type_t *)&synthio_biquad_type_obj, MP_ARRAY_SIZE(out_args), 0, out_args);
72+
}
73+
74+
mp_obj_t common_hal_synthio_new_bpf(mp_float_t w0, mp_float_t Q) {
75+
mp_float_t s = MICROPY_FLOAT_C_FUN(sin)(w0);
76+
mp_float_t c = MICROPY_FLOAT_C_FUN(cos)(w0);
77+
mp_float_t alpha = s / (2 * Q);
78+
mp_float_t a0 = 1 + alpha;
79+
mp_float_t a1 = -2 * c;
80+
mp_float_t a2 = 1 - alpha;
81+
mp_float_t b0 = alpha;
82+
mp_float_t b1 = 0;
83+
mp_float_t b2 = -alpha;
84+
85+
mp_obj_t out_args[] = {
86+
mp_obj_new_float(a1 / a0),
87+
mp_obj_new_float(a2 / a0),
88+
mp_obj_new_float(b0 / a0),
89+
mp_obj_new_float(b1 / a0),
90+
mp_obj_new_float(b2 / a0),
91+
};
92+
93+
return namedtuple_make_new((const mp_obj_type_t *)&synthio_biquad_type_obj, MP_ARRAY_SIZE(out_args), 0, out_args);
94+
}

tests/circuitpython/synthio_biquad.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from synthio import Synthesizer
2+
3+
s = Synthesizer(sample_rate=48000)
4+
5+
6+
def print_filter(x):
7+
print(" ".join(f"{v:.4g}" for v in x))
8+
9+
10+
print_filter(s.low_pass_filter(330))
11+
print_filter(s.high_pass_filter(330))
12+
print_filter(s.band_pass_filter(330))

0 commit comments

Comments
 (0)