Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synthio: switch to per-note biquad filtering #8048

Merged
merged 11 commits into from Jun 7, 2023
2 changes: 2 additions & 0 deletions ports/unix/variants/coverage/mpconfigvariant.mk
Expand Up @@ -45,6 +45,7 @@ SRC_BITMAP := \
shared-bindings/synthio/MidiTrack.c \
shared-bindings/synthio/LFO.c \
shared-bindings/synthio/Note.c \
shared-bindings/synthio/Biquad.c \
shared-bindings/synthio/Synthesizer.c \
shared-bindings/traceback/__init__.c \
shared-bindings/util.c \
Expand All @@ -70,6 +71,7 @@ SRC_BITMAP := \
shared-module/synthio/MidiTrack.c \
shared-module/synthio/LFO.c \
shared-module/synthio/Note.c \
shared-module/synthio/Biquad.c \
shared-module/synthio/Synthesizer.c \
shared-module/traceback/__init__.c \
shared-module/zlib/__init__.c \
Expand Down
1 change: 1 addition & 0 deletions py/circuitpy_defns.mk
Expand Up @@ -650,6 +650,7 @@ SRC_SHARED_MODULE_ALL = \
struct/__init__.c \
supervisor/__init__.c \
supervisor/StatusBar.c \
synthio/Biquad.c \
synthio/LFO.c \
synthio/Math.c \
synthio/MidiTrack.c \
Expand Down
110 changes: 110 additions & 0 deletions shared-bindings/synthio/Biquad.c
@@ -0,0 +1,110 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2021 Artyom Skrobov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#include <math.h>
#include <string.h>

#include "py/enum.h"
#include "py/mperrno.h"
#include "py/obj.h"
#include "py/objnamedtuple.h"
#include "py/runtime.h"

#include "shared-bindings/synthio/__init__.h"
#include "shared-bindings/synthio/LFO.h"
#include "shared-bindings/synthio/Math.h"
#include "shared-bindings/synthio/MidiTrack.h"
#include "shared-bindings/synthio/Note.h"
#include "shared-bindings/synthio/Synthesizer.h"

#include "shared-module/synthio/LFO.h"

#define default_attack_time (MICROPY_FLOAT_CONST(0.1))
#define default_decay_time (MICROPY_FLOAT_CONST(0.05))
#define default_release_time (MICROPY_FLOAT_CONST(0.2))
#define default_attack_level (MICROPY_FLOAT_CONST(1.))
#define default_sustain_level (MICROPY_FLOAT_CONST(0.8))

static const mp_arg_t biquad_properties[] = {
{ MP_QSTR_a1, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
{ MP_QSTR_a2, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
{ MP_QSTR_b0, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
{ MP_QSTR_b1, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
{ MP_QSTR_b2, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
};

//| class Biquad:
//| def __init__(self, b0: float, b1: float, b2: float, a1: float, a2: float) -> None:
//| """Construct a normalized biquad filter object.
//|
//| This implements the "direct form 1" biquad filter, where each coefficient
//| has been pre-divided by a0.
//|
//| Biquad objects are usually constructed via one of the related methods on a `Synthesizer` object
//| rather than directly from coefficients.
//|
//| https://github.com/WebAudio/Audio-EQ-Cookbook/blob/main/Audio-EQ-Cookbook.txt
//| """
//|
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) {
mp_arg_val_t args[MP_ARRAY_SIZE(biquad_properties)];
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(biquad_properties), biquad_properties, args);

for (size_t i = 0; i < MP_ARRAY_SIZE(biquad_properties); i++) {
args[i].u_obj = mp_obj_new_float(mp_arg_validate_type_float(args[i].u_obj, biquad_properties[i].qst));
}

MP_STATIC_ASSERT(sizeof(mp_arg_val_t) == sizeof(mp_obj_t));
return namedtuple_make_new(type_in, MP_ARRAY_SIZE(args), 0, &args[0].u_obj);
}

const mp_obj_namedtuple_type_t synthio_biquad_type_obj = {
.base = {
.base = {
.type = &mp_type_type
},
.flags = MP_TYPE_FLAG_EXTENDED,
.name = MP_QSTR_Biquad,
.print = namedtuple_print,
.parent = &mp_type_tuple,
.make_new = synthio_biquad_make_new,
.attr = namedtuple_attr,
MP_TYPE_EXTENDED_FIELDS(
.unary_op = mp_obj_tuple_unary_op,
.binary_op = mp_obj_tuple_binary_op,
.subscr = mp_obj_tuple_subscr,
.getiter = mp_obj_tuple_getiter,
),
},
.n_fields = 5,
.fields = {
MP_QSTR_a1,
MP_QSTR_a2,
MP_QSTR_b0,
MP_QSTR_b1,
MP_QSTR_b2,
},
};
9 changes: 9 additions & 0 deletions shared-bindings/synthio/Biquad.h
@@ -0,0 +1,9 @@
#pragma once

#include "py/obj.h"
#include "py/objnamedtuple.h"

extern const mp_obj_namedtuple_type_t synthio_biquad_type_obj;
mp_obj_t common_hal_synthio_new_lpf(mp_float_t w0, mp_float_t Q);
mp_obj_t common_hal_synthio_new_hpf(mp_float_t w0, mp_float_t Q);
mp_obj_t common_hal_synthio_new_bpf(mp_float_t w0, mp_float_t Q);
15 changes: 10 additions & 5 deletions shared-bindings/synthio/Note.c
Expand Up @@ -41,7 +41,7 @@ static const mp_arg_t note_properties[] = {
{ MP_QSTR_bend, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(0) } },
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
{ MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1) } },
{ MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
{ MP_QSTR_ring_frequency, MP_ARG_OBJ, {.u_obj = MP_ROM_INT(0) } },
{ MP_QSTR_ring_bend, MP_ARG_OBJ, {.u_obj = MP_ROM_INT(0) } },
{ MP_QSTR_ring_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
Expand All @@ -56,6 +56,7 @@ static const mp_arg_t note_properties[] = {
//| envelope: Optional[Envelope] = None,
//| amplitude: BlockInput = 0.0,
//| bend: BlockInput = 0.0,
//| filter: Optional[Biquad] = None,
//| ring_frequency: float = 0.0,
//| ring_bend: float = 0.0,
//| ring_waveform: Optional[ReadableBuffer] = 0.0,
Expand Down Expand Up @@ -97,17 +98,21 @@ MP_PROPERTY_GETSET(synthio_note_frequency_obj,
(mp_obj_t)&synthio_note_get_frequency_obj,
(mp_obj_t)&synthio_note_set_frequency_obj);

//| filter: bool
//| """True if the note should be processed via the synthesizer's FIR filter."""
//| filter: Optional[Biquad]
//| """If not None, the output of this Note is filtered according to the provided coefficients.
//|
//| Construct an appropriate filter by calling a filter-making method on the
//| `Synthesizer` object where you plan to play the note, as filter coefficients depend
//| on the sample rate"""
STATIC mp_obj_t synthio_note_get_filter(mp_obj_t self_in) {
synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_bool(common_hal_synthio_note_get_filter(self));
return common_hal_synthio_note_get_filter_obj(self);
}
MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_filter_obj, synthio_note_get_filter);

STATIC mp_obj_t synthio_note_set_filter(mp_obj_t self_in, mp_obj_t arg) {
synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in);
common_hal_synthio_note_set_filter(self, mp_obj_is_true(arg));
common_hal_synthio_note_set_filter(self, arg);
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_filter_obj, synthio_note_set_filter);
Expand Down
4 changes: 2 additions & 2 deletions shared-bindings/synthio/Note.h
Expand Up @@ -9,8 +9,8 @@ typedef enum synthio_bend_mode_e synthio_bend_mode_t;
mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self);
void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value);

bool common_hal_synthio_note_get_filter(synthio_note_obj_t *self);
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, bool value);
mp_obj_t common_hal_synthio_note_get_filter_obj(synthio_note_obj_t *self);
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, mp_obj_t biquad);

mp_obj_t common_hal_synthio_note_get_panning(synthio_note_obj_t *self);
void common_hal_synthio_note_set_panning(synthio_note_obj_t *self, mp_obj_t value);
Expand Down
118 changes: 113 additions & 5 deletions shared-bindings/synthio/Synthesizer.c
Expand Up @@ -32,6 +32,7 @@
#include "py/objproperty.h"
#include "py/runtime.h"
#include "shared-bindings/util.h"
#include "shared-bindings/synthio/Biquad.h"
#include "shared-bindings/synthio/Synthesizer.h"
#include "shared-bindings/synthio/LFO.h"
#include "shared-bindings/synthio/__init__.h"
Expand All @@ -52,7 +53,6 @@
//| channel_count: int = 1,
//| waveform: Optional[ReadableBuffer] = None,
//| envelope: Optional[Envelope] = None,
//| filter: Optional[ReadableBuffer] = None,
//| ) -> None:
//| """Create a synthesizer object.
//|
Expand All @@ -65,17 +65,15 @@
//| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory
//| :param int channel_count: The number of output channels (1=mono, 2=stereo)
//| :param ReadableBuffer waveform: A single-cycle waveform. Default is a 50% duty cycle square wave. If specified, must be a ReadableBuffer of type 'h' (signed 16 bit)
//| :param ReadableBuffer filter: Coefficients of an FIR filter to apply to notes with ``filter=True``. If specified, must be a ReadableBuffer of type 'h' (signed 16 bit)
//| :param Optional[Envelope] envelope: An object that defines the loudness of a note over time. The default envelope, `None` provides no ramping, voices turn instantly on and off.
//| """
STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
enum { ARG_sample_rate, ARG_channel_count, ARG_waveform, ARG_envelope, ARG_filter };
enum { ARG_sample_rate, ARG_channel_count, ARG_waveform, ARG_envelope };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
{ MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1} },
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
{ MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
Expand All @@ -87,7 +85,6 @@ STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n
args[ARG_sample_rate].u_int,
args[ARG_channel_count].u_int,
args[ARG_waveform].u_obj,
args[ARG_filter].u_obj,
args[ARG_envelope].u_obj);

return MP_OBJ_FROM_PTR(self);
Expand Down Expand Up @@ -292,6 +289,114 @@ MP_PROPERTY_GETTER(synthio_synthesizer_blocks_obj,
//| """Maximum polyphony of the synthesizer (read-only class property)"""
//|

//| def low_pass_filter(cls, frequency: float, q_factor: float = 0.7071067811865475) -> Biquad:
//| """Construct a low-pass filter with the given parameters.
//|
//| ``frequency``, called f0 in the cookbook, is the corner frequency in Hz
//| of the filter.
//|
//| ``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.
//| """

enum passfilter_arg_e { ARG_f0, ARG_Q };

// M_PI is not part of the math.h standard and may not be defined
// And by defining our own we can ensure it uses the correct const format.
#define MP_PI MICROPY_FLOAT_CONST(3.14159265358979323846)

static const mp_arg_t passfilter_properties[] = {
{ MP_QSTR_frequency, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_ROM_NONE} },
{ MP_QSTR_Q, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL } },
};

STATIC mp_obj_t synthio_synthesizer_lpf(size_t n_pos, const mp_obj_t *pos_args, mp_map_t *kw_args) {
mp_arg_val_t args[MP_ARRAY_SIZE(passfilter_properties)];

mp_obj_t self_in = pos_args[0];
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);

mp_arg_parse_all(n_pos - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(passfilter_properties), passfilter_properties, args);

mp_float_t f0 = mp_arg_validate_type_float(args[ARG_f0].u_obj, MP_QSTR_f0);
mp_float_t Q =
args[ARG_Q].u_obj == MP_OBJ_NULL ? MICROPY_FLOAT_CONST(0.7071067811865475) :
mp_arg_validate_type_float(args[ARG_Q].u_obj, MP_QSTR_Q);

mp_float_t w0 = f0 / self->synth.sample_rate * 2 * MP_PI;

return common_hal_synthio_new_lpf(w0, Q);

}

MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_lpf_fun_obj, 1, synthio_synthesizer_lpf);

//| def high_pass_filter(
//| cls, frequency: float, q_factor: float = 0.7071067811865475
//| ) -> Biquad:
//| """Construct a high-pass filter with the given parameters.
//|
//| ``frequency``, called f0 in the cookbook, is the corner frequency in Hz
//| of the filter.
//|
//| ``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.
//| """

STATIC mp_obj_t synthio_synthesizer_hpf(size_t n_pos, const mp_obj_t *pos_args, mp_map_t *kw_args) {
mp_arg_val_t args[MP_ARRAY_SIZE(passfilter_properties)];

mp_obj_t self_in = pos_args[0];
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);

mp_arg_parse_all(n_pos - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(passfilter_properties), passfilter_properties, args);

mp_float_t f0 = mp_arg_validate_type_float(args[ARG_f0].u_obj, MP_QSTR_f0);
mp_float_t Q =
args[ARG_Q].u_obj == MP_OBJ_NULL ? MICROPY_FLOAT_CONST(0.7071067811865475) :
mp_arg_validate_type_float(args[ARG_Q].u_obj, MP_QSTR_Q);

mp_float_t w0 = f0 / self->synth.sample_rate * 2 * MP_PI;

return common_hal_synthio_new_hpf(w0, Q);

}

//| def band_pass_filter(
//| cls, frequency: float, q_factor: float = 0.7071067811865475
//| ) -> Biquad:
//| """Construct a band-pass filter with the given parameters.
//|
//| ``frequency``, called f0 in the cookbook, is the center frequency in Hz
//| of the filter.
//|
//| ``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.
//|
//| The coefficients are scaled such that the filter has a 0dB peak gain.
//| """
//|

MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_hpf_fun_obj, 1, synthio_synthesizer_hpf);

STATIC mp_obj_t synthio_synthesizer_bpf(size_t n_pos, const mp_obj_t *pos_args, mp_map_t *kw_args) {
mp_arg_val_t args[MP_ARRAY_SIZE(passfilter_properties)];

mp_obj_t self_in = pos_args[0];
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);

mp_arg_parse_all(n_pos - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(passfilter_properties), passfilter_properties, args);

mp_float_t f0 = mp_arg_validate_type_float(args[ARG_f0].u_obj, MP_QSTR_f0);
mp_float_t Q =
args[ARG_Q].u_obj == MP_OBJ_NULL ? MICROPY_FLOAT_CONST(0.7071067811865475) :
mp_arg_validate_type_float(args[ARG_Q].u_obj, MP_QSTR_Q);

mp_float_t w0 = f0 / self->synth.sample_rate * 2 * MP_PI;

return common_hal_synthio_new_bpf(w0, Q);

}

MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_bpf_fun_obj, 1, synthio_synthesizer_bpf);

STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
// Methods
{ MP_ROM_QSTR(MP_QSTR_press), MP_ROM_PTR(&synthio_synthesizer_press_obj) },
Expand All @@ -304,6 +409,9 @@ STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&synthio_synthesizer___exit___obj) },

{ MP_ROM_QSTR(MP_QSTR_low_pass_filter), MP_ROM_PTR(&synthio_synthesizer_lpf_fun_obj) },
{ MP_ROM_QSTR(MP_QSTR_high_pass_filter), MP_ROM_PTR(&synthio_synthesizer_hpf_fun_obj) },
{ MP_ROM_QSTR(MP_QSTR_band_pass_filter), MP_ROM_PTR(&synthio_synthesizer_bpf_fun_obj) },
// Properties
{ MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_synthesizer_envelope_obj) },
{ MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_synthesizer_sample_rate_obj) },
Expand Down
2 changes: 1 addition & 1 deletion shared-bindings/synthio/Synthesizer.h
Expand Up @@ -32,7 +32,7 @@
extern const mp_obj_type_t synthio_synthesizer_type;

void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self,
uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj, mp_obj_t filter_obj,
uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj,
mp_obj_t envelope_obj);
void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self);
bool common_hal_synthio_synthesizer_deinited(synthio_synthesizer_obj_t *self);
Expand Down
2 changes: 2 additions & 0 deletions shared-bindings/synthio/__init__.c
Expand Up @@ -36,6 +36,7 @@
#include "extmod/vfs_posix.h"

#include "shared-bindings/synthio/__init__.h"
#include "shared-bindings/synthio/Biquad.h"
#include "shared-bindings/synthio/LFO.h"
#include "shared-bindings/synthio/Math.h"
#include "shared-bindings/synthio/MidiTrack.h"
Expand Down Expand Up @@ -310,6 +311,7 @@ MP_DEFINE_CONST_FUN_OBJ_VAR(synthio_lfo_tick_obj, 1, synthio_lfo_tick);

STATIC const mp_rom_map_elem_t synthio_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_synthio) },
{ MP_ROM_QSTR(MP_QSTR_Biquad), MP_ROM_PTR(&synthio_biquad_type_obj) },
{ MP_ROM_QSTR(MP_QSTR_Math), MP_ROM_PTR(&synthio_math_type) },
{ MP_ROM_QSTR(MP_QSTR_MathOperation), MP_ROM_PTR(&synthio_math_operation_type) },
{ MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) },
Expand Down