Skip to content

Commit 5102797

Browse files
committed
synthio: apply biquad filters during synthesis
1 parent fed8d58 commit 5102797

File tree

11 files changed

+188
-219
lines changed

11 files changed

+188
-219
lines changed

shared-bindings/synthio/Note.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ static const mp_arg_t note_properties[] = {
4141
{ MP_QSTR_bend, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(0) } },
4242
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
4343
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
44-
{ MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1) } },
44+
{ MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
4545
{ MP_QSTR_ring_frequency, MP_ARG_OBJ, {.u_obj = MP_ROM_INT(0) } },
4646
{ MP_QSTR_ring_bend, MP_ARG_OBJ, {.u_obj = MP_ROM_INT(0) } },
4747
{ MP_QSTR_ring_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
@@ -56,6 +56,7 @@ static const mp_arg_t note_properties[] = {
5656
//| envelope: Optional[Envelope] = None,
5757
//| amplitude: BlockInput = 0.0,
5858
//| bend: BlockInput = 0.0,
59+
//| filter: Optional[Biquad] = None,
5960
//| ring_frequency: float = 0.0,
6061
//| ring_bend: float = 0.0,
6162
//| ring_waveform: Optional[ReadableBuffer] = 0.0,
@@ -97,17 +98,21 @@ MP_PROPERTY_GETSET(synthio_note_frequency_obj,
9798
(mp_obj_t)&synthio_note_get_frequency_obj,
9899
(mp_obj_t)&synthio_note_set_frequency_obj);
99100

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

108113
STATIC mp_obj_t synthio_note_set_filter(mp_obj_t self_in, mp_obj_t arg) {
109114
synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in);
110-
common_hal_synthio_note_set_filter(self, mp_obj_is_true(arg));
115+
common_hal_synthio_note_set_filter(self, arg);
111116
return mp_const_none;
112117
}
113118
MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_filter_obj, synthio_note_set_filter);

shared-bindings/synthio/Note.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ typedef enum synthio_bend_mode_e synthio_bend_mode_t;
99
mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self);
1010
void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value);
1111

12-
bool common_hal_synthio_note_get_filter(synthio_note_obj_t *self);
13-
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, bool value);
12+
mp_obj_t common_hal_synthio_note_get_filter_obj(synthio_note_obj_t *self);
13+
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, mp_obj_t biquad);
1414

1515
mp_obj_t common_hal_synthio_note_get_panning(synthio_note_obj_t *self);
1616
void common_hal_synthio_note_set_panning(synthio_note_obj_t *self, mp_obj_t value);

shared-module/synthio/Biquad.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include <math.h>
2828
#include "shared-bindings/synthio/Biquad.h"
29+
#include "shared-module/synthio/Biquad.h"
2930

3031
mp_obj_t common_hal_synthio_new_lpf(mp_float_t w0, mp_float_t Q) {
3132
mp_float_t s = MICROPY_FLOAT_C_FUN(sin)(w0);
@@ -92,3 +93,47 @@ mp_obj_t common_hal_synthio_new_bpf(mp_float_t w0, mp_float_t Q) {
9293

9394
return namedtuple_make_new((const mp_obj_type_t *)&synthio_biquad_type_obj, MP_ARRAY_SIZE(out_args), 0, out_args);
9495
}
96+
97+
#define BIQUAD_SHIFT (16)
98+
STATIC int32_t biquad_scale_arg_obj(mp_obj_t arg) {
99+
return (int32_t)MICROPY_FLOAT_C_FUN(round)(MICROPY_FLOAT_C_FUN(ldexp)(mp_obj_get_float(arg), BIQUAD_SHIFT));
100+
}
101+
void synthio_biquad_filter_assign(biquad_filter_state *st, mp_obj_t biquad_obj) {
102+
if (biquad_obj != mp_const_none) {
103+
mp_arg_validate_type(biquad_obj, (const mp_obj_type_t *)&synthio_biquad_type_obj, MP_QSTR_filter);
104+
mp_obj_tuple_t *biquad = (mp_obj_tuple_t *)MP_OBJ_TO_PTR(biquad_obj);
105+
st->a1 = biquad_scale_arg_obj(biquad->items[0]);
106+
st->a2 = biquad_scale_arg_obj(biquad->items[1]);
107+
st->b0 = biquad_scale_arg_obj(biquad->items[2]);
108+
st->b1 = biquad_scale_arg_obj(biquad->items[3]);
109+
st->b2 = biquad_scale_arg_obj(biquad->items[4]);
110+
}
111+
}
112+
113+
void synthio_biquad_filter_samples(biquad_filter_state *st, int32_t *out, const int32_t *in, size_t n, size_t stride) {
114+
int32_t a1 = st->a1;
115+
int32_t a2 = st->a2;
116+
int32_t b0 = st->b0;
117+
int32_t b1 = st->b1;
118+
int32_t b2 = st->b2;
119+
120+
int32_t x0 = st->x[0];
121+
int32_t x1 = st->x[1];
122+
int32_t y0 = st->y[0];
123+
int32_t y1 = st->y[1];
124+
125+
for (; n; --n, in += stride, out += stride) {
126+
int16_t input = *in;
127+
int32_t output = (b0 * input + b1 * x0 + b2 * x1 - a1 * y0 - a2 * y1) >> BIQUAD_SHIFT;
128+
129+
x1 = x0;
130+
x0 = input;
131+
y1 = y0;
132+
y0 = output;
133+
*out = output;
134+
}
135+
st->x[0] = x0;
136+
st->x[1] = x1;
137+
st->y[0] = y0;
138+
st->y[1] = y1;
139+
}

shared-module/synthio/Biquad.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
#pragma once
28+
29+
#include "py/obj.h"
30+
31+
typedef struct {
32+
int32_t a1, a2, b0, b1, b2;
33+
int32_t x[2], y[2];
34+
} biquad_filter_state;
35+
36+
void synthio_biquad_filter_assign(biquad_filter_state *st, mp_obj_t biquad_obj);
37+
void synthio_biquad_filter_samples(biquad_filter_state *st, int32_t *out, const int32_t *in, size_t n, size_t stride);

shared-module/synthio/Note.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t
4040
self->frequency_scaled = synthio_frequency_convert_float_to_scaled(val);
4141
}
4242

43-
bool common_hal_synthio_note_get_filter(synthio_note_obj_t *self) {
44-
return self->filter;
43+
mp_obj_t common_hal_synthio_note_get_filter_obj(synthio_note_obj_t *self) {
44+
return self->filter_obj;
4545
}
4646

47-
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, bool value_in) {
48-
self->filter = value_in;
47+
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, mp_obj_t filter_in) {
48+
synthio_biquad_filter_assign(&self->filter_state, filter_in);
49+
self->filter_obj = filter_in;
4950
}
5051

5152
mp_float_t common_hal_synthio_note_get_ring_frequency(synthio_note_obj_t *self) {

shared-module/synthio/Note.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#pragma once
2828

2929
#include "shared-module/synthio/__init__.h"
30+
#include "shared-module/synthio/Biquad.h"
3031
#include "shared-module/synthio/LFO.h"
3132
#include "shared-bindings/synthio/__init__.h"
3233

@@ -37,12 +38,14 @@ typedef struct synthio_note_obj {
3738

3839
mp_float_t frequency, ring_frequency;
3940
mp_obj_t waveform_obj, envelope_obj, ring_waveform_obj;
41+
mp_obj_t filter_obj;
42+
43+
biquad_filter_state filter_state;
4044

4145
int32_t sample_rate;
4246

4347
int32_t frequency_scaled;
4448
int32_t ring_frequency_scaled, ring_frequency_bent;
45-
bool filter;
4649

4750
mp_buffer_info_t waveform_buf;
4851
mp_buffer_info_t ring_waveform_buf;

shared-module/synthio/__init__.c

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
#include "shared-module/synthio/__init__.h"
2929
#include "shared-bindings/synthio/__init__.h"
30+
#include "shared-module/synthio/Biquad.h"
3031
#include "shared-module/synthio/Note.h"
3132
#include "py/runtime.h"
3233
#include <math.h>
@@ -309,37 +310,15 @@ static void synth_note_into_buffer(synthio_synth_t *synth, int chan, int32_t *ou
309310
}
310311
}
311312

312-
STATIC void run_fir(synthio_synth_t *synth, int32_t *out_buffer32, uint16_t dur) {
313-
int16_t *coeff = (int16_t *)synth->filter_bufinfo.buf;
314-
size_t fir_len = synth->filter_bufinfo.len;
315-
int32_t *in_buf = synth->filter_buffer;
316-
317-
318-
int synth_chan = synth->channel_count;
319-
// FIR and copy values to output buffer
320-
for (int16_t i = 0; i < dur * synth_chan; i++) {
321-
int32_t acc = 0;
322-
for (size_t j = 0; j < fir_len; j++) {
323-
// shift 5 here is good for up to 32 filtered voices, else might wrap
324-
acc = acc + (in_buf[j * synth_chan] * (coeff[j] >> 5));
325-
}
326-
*out_buffer32++ = acc >> 10;
327-
in_buf++;
328-
}
329-
330-
// Move values down so that they get filtered next time
331-
memmove(synth->filter_buffer, &synth->filter_buffer[dur * synth_chan], fir_len * sizeof(int32_t) * synth_chan);
332-
}
333-
334-
STATIC bool synthio_synth_get_note_filtered(mp_obj_t note_obj) {
313+
STATIC mp_obj_t synthio_synth_get_note_filter(mp_obj_t note_obj) {
335314
if (note_obj == mp_const_none) {
336-
return false;
315+
return mp_const_none;
337316
}
338317
if (!mp_obj_is_small_int(note_obj)) {
339318
synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj);
340-
return note->filter;
319+
return note->filter_obj;
341320
}
342-
return true;
321+
return mp_const_none;
343322
}
344323

345324
void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) {
@@ -360,30 +339,24 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t
360339
synth->span.dur -= dur;
361340

362341
int32_t out_buffer32[dur * synth->channel_count];
363-
364-
if (synth->filter_buffer) {
365-
int32_t *filter_start = &synth->filter_buffer[synth->filter_bufinfo.len * synth->channel_count];
366-
memset(filter_start, 0, dur * synth->channel_count * sizeof(int32_t));
367-
368-
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
369-
mp_obj_t note_obj = synth->span.note_obj[chan];
370-
if (!synthio_synth_get_note_filtered(note_obj)) {
371-
continue;
372-
}
373-
synth_note_into_buffer(synth, chan, filter_start, dur);
374-
}
375-
376-
run_fir(synth, out_buffer32, dur);
377-
} else {
378-
memset(out_buffer32, 0, sizeof(out_buffer32));
379-
}
342+
memset(out_buffer32, 0, sizeof(out_buffer32));
380343

381344
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
382345
mp_obj_t note_obj = synth->span.note_obj[chan];
383-
if (synth->filter_buffer && synthio_synth_get_note_filtered(note_obj)) {
384-
continue;
346+
mp_obj_t filter_obj = synthio_synth_get_note_filter(note_obj);
347+
if (filter_obj == mp_const_none) {
348+
synth_note_into_buffer(synth, chan, out_buffer32, dur);
349+
} else {
350+
synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj);
351+
int32_t filter_buffer32[dur * synth->channel_count];
352+
memset(filter_buffer32, 0, sizeof(filter_buffer32));
353+
354+
synth_note_into_buffer(synth, chan, filter_buffer32, dur);
355+
int synth_chan = synth->channel_count;
356+
for (int i = 0; i < synth_chan; i++) {
357+
synthio_biquad_filter_samples(&note->filter_state, &out_buffer32[i], &filter_buffer32[i], dur, i);
358+
}
385359
}
386-
synth_note_into_buffer(synth, chan, out_buffer32, dur);
387360
}
388361

389362
int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index];
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import sys
2+
3+
sys.path.insert(
4+
0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave"
5+
)
6+
7+
import random
8+
import audiocore
9+
import synthio
10+
from ulab import numpy as np
11+
import adafruit_wave as wave
12+
13+
random.seed(9)
14+
15+
envelope = synthio.Envelope(
16+
attack_time=0, decay_time=0, release_time=0, attack_level=0.8, sustain_level=1.0
17+
)
18+
19+
SAMPLE_SIZE = 1024
20+
VOLUME = 14700
21+
sine = np.array(
22+
np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME,
23+
dtype=np.int16,
24+
)
25+
noise = np.array([random.randint(-VOLUME, VOLUME) for i in range(SAMPLE_SIZE)], dtype=np.int16)
26+
bend_out = np.linspace(0, 32767, num=SAMPLE_SIZE, endpoint=True, dtype=np.int16)
27+
28+
29+
def synthesize(synth):
30+
for waveform in (sine, None, noise):
31+
for biquad in (
32+
None,
33+
synth.low_pass_filter(330),
34+
synth.low_pass_filter(660),
35+
synth.high_pass_filter(330),
36+
synth.high_pass_filter(660),
37+
synth.band_pass_filter(330),
38+
synth.band_pass_filter(660),
39+
):
40+
n = synthio.Note(
41+
frequency=80,
42+
envelope=envelope,
43+
filter=biquad,
44+
waveform=waveform,
45+
bend=synthio.LFO(bend_out, once=True, rate=1 / 2, scale=5),
46+
)
47+
48+
synth.press(n)
49+
print(synth, n)
50+
yield 2 * 48000 // 256
51+
synth.release_all()
52+
yield 36
53+
54+
55+
with wave.open("biquad.wav", "w") as f:
56+
f.setnchannels(1)
57+
f.setsampwidth(2)
58+
f.setframerate(48000)
59+
synth = synthio.Synthesizer(sample_rate=48000)
60+
for n in synthesize(synth):
61+
for i in range(n):
62+
result, data = audiocore.get_buffer(synth)
63+
f.writeframes(data)

0 commit comments

Comments
 (0)