synthio: apply biquad filters during synthesis

This commit is contained in:
Jeff Epler 2023-05-29 10:53:48 -05:00
parent fed8d5825b
commit 51027974e5
No known key found for this signature in database
GPG Key ID: D5BF15AB975AB4DE
11 changed files with 188 additions and 219 deletions

View File

@ -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 } },
@ -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,
@ -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);

View File

@ -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);

View File

@ -26,6 +26,7 @@
#include <math.h>
#include "shared-bindings/synthio/Biquad.h"
#include "shared-module/synthio/Biquad.h"
mp_obj_t common_hal_synthio_new_lpf(mp_float_t w0, mp_float_t Q) {
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) {
return namedtuple_make_new((const mp_obj_type_t *)&synthio_biquad_type_obj, MP_ARRAY_SIZE(out_args), 0, out_args);
}
#define BIQUAD_SHIFT (16)
STATIC int32_t biquad_scale_arg_obj(mp_obj_t arg) {
return (int32_t)MICROPY_FLOAT_C_FUN(round)(MICROPY_FLOAT_C_FUN(ldexp)(mp_obj_get_float(arg), BIQUAD_SHIFT));
}
void synthio_biquad_filter_assign(biquad_filter_state *st, mp_obj_t biquad_obj) {
if (biquad_obj != mp_const_none) {
mp_arg_validate_type(biquad_obj, (const mp_obj_type_t *)&synthio_biquad_type_obj, MP_QSTR_filter);
mp_obj_tuple_t *biquad = (mp_obj_tuple_t *)MP_OBJ_TO_PTR(biquad_obj);
st->a1 = biquad_scale_arg_obj(biquad->items[0]);
st->a2 = biquad_scale_arg_obj(biquad->items[1]);
st->b0 = biquad_scale_arg_obj(biquad->items[2]);
st->b1 = biquad_scale_arg_obj(biquad->items[3]);
st->b2 = biquad_scale_arg_obj(biquad->items[4]);
}
}
void synthio_biquad_filter_samples(biquad_filter_state *st, int32_t *out, const int32_t *in, size_t n, size_t stride) {
int32_t a1 = st->a1;
int32_t a2 = st->a2;
int32_t b0 = st->b0;
int32_t b1 = st->b1;
int32_t b2 = st->b2;
int32_t x0 = st->x[0];
int32_t x1 = st->x[1];
int32_t y0 = st->y[0];
int32_t y1 = st->y[1];
for (; n; --n, in += stride, out += stride) {
int16_t input = *in;
int32_t output = (b0 * input + b1 * x0 + b2 * x1 - a1 * y0 - a2 * y1) >> BIQUAD_SHIFT;
x1 = x0;
x0 = input;
y1 = y0;
y0 = output;
*out = output;
}
st->x[0] = x0;
st->x[1] = x1;
st->y[0] = y0;
st->y[1] = y1;
}

View File

@ -0,0 +1,37 @@
/*
* This file is part of the Micro Python project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2023 Jeff Epler for Adafruit Industries
*
* 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.
*/
#pragma once
#include "py/obj.h"
typedef struct {
int32_t a1, a2, b0, b1, b2;
int32_t x[2], y[2];
} biquad_filter_state;
void synthio_biquad_filter_assign(biquad_filter_state *st, mp_obj_t biquad_obj);
void synthio_biquad_filter_samples(biquad_filter_state *st, int32_t *out, const int32_t *in, size_t n, size_t stride);

View File

@ -40,12 +40,13 @@ void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t
self->frequency_scaled = synthio_frequency_convert_float_to_scaled(val);
}
bool common_hal_synthio_note_get_filter(synthio_note_obj_t *self) {
return self->filter;
mp_obj_t common_hal_synthio_note_get_filter_obj(synthio_note_obj_t *self) {
return self->filter_obj;
}
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, bool value_in) {
self->filter = value_in;
void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, mp_obj_t filter_in) {
synthio_biquad_filter_assign(&self->filter_state, filter_in);
self->filter_obj = filter_in;
}
mp_float_t common_hal_synthio_note_get_ring_frequency(synthio_note_obj_t *self) {

View File

@ -27,6 +27,7 @@
#pragma once
#include "shared-module/synthio/__init__.h"
#include "shared-module/synthio/Biquad.h"
#include "shared-module/synthio/LFO.h"
#include "shared-bindings/synthio/__init__.h"
@ -37,12 +38,14 @@ typedef struct synthio_note_obj {
mp_float_t frequency, ring_frequency;
mp_obj_t waveform_obj, envelope_obj, ring_waveform_obj;
mp_obj_t filter_obj;
biquad_filter_state filter_state;
int32_t sample_rate;
int32_t frequency_scaled;
int32_t ring_frequency_scaled, ring_frequency_bent;
bool filter;
mp_buffer_info_t waveform_buf;
mp_buffer_info_t ring_waveform_buf;

View File

@ -27,6 +27,7 @@
#include "shared-module/synthio/__init__.h"
#include "shared-bindings/synthio/__init__.h"
#include "shared-module/synthio/Biquad.h"
#include "shared-module/synthio/Note.h"
#include "py/runtime.h"
#include <math.h>
@ -309,37 +310,15 @@ static void synth_note_into_buffer(synthio_synth_t *synth, int chan, int32_t *ou
}
}
STATIC void run_fir(synthio_synth_t *synth, int32_t *out_buffer32, uint16_t dur) {
int16_t *coeff = (int16_t *)synth->filter_bufinfo.buf;
size_t fir_len = synth->filter_bufinfo.len;
int32_t *in_buf = synth->filter_buffer;
int synth_chan = synth->channel_count;
// FIR and copy values to output buffer
for (int16_t i = 0; i < dur * synth_chan; i++) {
int32_t acc = 0;
for (size_t j = 0; j < fir_len; j++) {
// shift 5 here is good for up to 32 filtered voices, else might wrap
acc = acc + (in_buf[j * synth_chan] * (coeff[j] >> 5));
}
*out_buffer32++ = acc >> 10;
in_buf++;
}
// Move values down so that they get filtered next time
memmove(synth->filter_buffer, &synth->filter_buffer[dur * synth_chan], fir_len * sizeof(int32_t) * synth_chan);
}
STATIC bool synthio_synth_get_note_filtered(mp_obj_t note_obj) {
STATIC mp_obj_t synthio_synth_get_note_filter(mp_obj_t note_obj) {
if (note_obj == mp_const_none) {
return false;
return mp_const_none;
}
if (!mp_obj_is_small_int(note_obj)) {
synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj);
return note->filter;
return note->filter_obj;
}
return true;
return mp_const_none;
}
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
synth->span.dur -= dur;
int32_t out_buffer32[dur * synth->channel_count];
if (synth->filter_buffer) {
int32_t *filter_start = &synth->filter_buffer[synth->filter_bufinfo.len * synth->channel_count];
memset(filter_start, 0, dur * synth->channel_count * sizeof(int32_t));
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
mp_obj_t note_obj = synth->span.note_obj[chan];
if (!synthio_synth_get_note_filtered(note_obj)) {
continue;
}
synth_note_into_buffer(synth, chan, filter_start, dur);
}
run_fir(synth, out_buffer32, dur);
} else {
memset(out_buffer32, 0, sizeof(out_buffer32));
}
memset(out_buffer32, 0, sizeof(out_buffer32));
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
mp_obj_t note_obj = synth->span.note_obj[chan];
if (synth->filter_buffer && synthio_synth_get_note_filtered(note_obj)) {
continue;
mp_obj_t filter_obj = synthio_synth_get_note_filter(note_obj);
if (filter_obj == mp_const_none) {
synth_note_into_buffer(synth, chan, out_buffer32, dur);
} else {
synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj);
int32_t filter_buffer32[dur * synth->channel_count];
memset(filter_buffer32, 0, sizeof(filter_buffer32));
synth_note_into_buffer(synth, chan, filter_buffer32, dur);
int synth_chan = synth->channel_count;
for (int i = 0; i < synth_chan; i++) {
synthio_biquad_filter_samples(&note->filter_state, &out_buffer32[i], &filter_buffer32[i], dur, i);
}
}
synth_note_into_buffer(synth, chan, out_buffer32, dur);
}
int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index];

View File

@ -0,0 +1,63 @@
import sys
sys.path.insert(
0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave"
)
import random
import audiocore
import synthio
from ulab import numpy as np
import adafruit_wave as wave
random.seed(9)
envelope = synthio.Envelope(
attack_time=0, decay_time=0, release_time=0, attack_level=0.8, sustain_level=1.0
)
SAMPLE_SIZE = 1024
VOLUME = 14700
sine = np.array(
np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME,
dtype=np.int16,
)
noise = np.array([random.randint(-VOLUME, VOLUME) for i in range(SAMPLE_SIZE)], dtype=np.int16)
bend_out = np.linspace(0, 32767, num=SAMPLE_SIZE, endpoint=True, dtype=np.int16)
def synthesize(synth):
for waveform in (sine, None, noise):
for biquad in (
None,
synth.low_pass_filter(330),
synth.low_pass_filter(660),
synth.high_pass_filter(330),
synth.high_pass_filter(660),
synth.band_pass_filter(330),
synth.band_pass_filter(660),
):
n = synthio.Note(
frequency=80,
envelope=envelope,
filter=biquad,
waveform=waveform,
bend=synthio.LFO(bend_out, once=True, rate=1 / 2, scale=5),
)
synth.press(n)
print(synth, n)
yield 2 * 48000 // 256
synth.release_all()
yield 36
with wave.open("biquad.wav", "w") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(48000)
synth = synthio.Synthesizer(sample_rate=48000)
for n in synthesize(synth):
for i in range(n):
result, data = audiocore.get_buffer(synth)
f.writeframes(data)

View File

@ -1,53 +0,0 @@
import sys
sys.path.insert(
0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave"
)
import random
import audiocore
import synthio
from ulab import numpy as np
import adafruit_wave as wave
import mkfilter
random.seed(9)
envelope = synthio.Envelope(
attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=0.8, sustain_level=0.8
)
SAMPLE_SIZE = 1024
bend_out = np.linspace(0, 32767, num=SAMPLE_SIZE, endpoint=True, dtype=np.int16)
filter_rectangular = mkfilter.LPF(48000, 800, 13)
filter_rectangular_big = mkfilter.LPF(48000, 800, 59)
filter_blackman = mkfilter.LPF(48000, 800, 59, win=mkfilter.blackman)
print(filter_blackman)
def synthesize(synth):
n = synthio.Note(
frequency=120,
envelope=envelope,
filter=True,
bend=synthio.LFO(bend_out, once=True, rate=1 / 2, scale=5),
)
synth.press(n)
print(synth, n)
yield 2 * 48000 // 256
synth.release_all()
yield 36
with wave.open("fir.wav", "w") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(48000)
for filter_coeffs in [None, filter_rectangular, filter_rectangular_big, filter_blackman]:
synth = synthio.Synthesizer(sample_rate=48000, filter=filter_coeffs)
for n in synthesize(synth):
for i in range(n):
result, data = audiocore.get_buffer(synth)
f.writeframes(data)

View File

@ -1,105 +0,0 @@
try:
from ulab import numpy as np
except ImportError:
import numpy as np
def lpf(fS, f, N, win=lambda N: 1):
if not (N & 1):
raise ValueError("filter length must be odd")
h = np.sinc(2 * f / fS * (np.arange(N) - (N - 1) / 2))
h = h * win(N)
return h * (1 / np.sum(h))
def hpf(fS, f, N, win=lambda N: 1):
if not (N & 1):
raise ValueError("filter length must be odd")
h = -lpf(fS, f, N)
h = h * win(N)
h[(N - 1) // 2] += 1
return h
def brf(fS, fL, NL, fH, NH, win=lambda N: 1):
hlpf = lpf(fS, fL, NL, win)
hhpf = hpf(fS, fH, NH, win)
if NH > NL:
h = hhpf
h[(NH - NL) // 2 : (NH - NL) // 2 + NL] += hlpf
else:
h = hlpf
h[(NL - NH) // 2 : (NL - NH) // 2 + NH] += hhpf
return h
def bpf(fS, fL, NL, fH, NH, win=lambda N: 1):
hlpf = lpf(fS, fL, NL, win)
hhpf = hpf(fS, fH, NH, win)
return np.convolve(hlpf, hhpf)
def blackman(M):
n = np.arange(1 - M, M, 2)
return 0.42 + 0.5 * np.cos(np.pi * n / (M - 1)) + 0.08 * np.cos(2.0 * np.pi * n / (M - 1))
def tosynthio(coeffs):
result = np.array(coeffs * 32767, dtype=np.int16)
return trim_zeros(result)
def trim_zeros(arr):
i = 0
j = len(arr) - 1
while i < len(arr) and arr[i] == 0:
i += 1
while j > i and arr[j] == 0:
j -= 1
return arr[i : j + 1]
# fiiir.com uses factor 4.6 for blackman window, 0.91 for rectangular
def ntaps(fS, fB, factor=4.6):
b = fB / fS
return round(factor / b) | 1
def LPF(*args, **kw):
return tosynthio(lpf(*args, **kw))
def HPF(*args, **kw):
return tosynthio(hpf(*args, **kw))
def BRF(*args, **kw):
return tosynthio(brf(*args, **kw))
def BPF(*args, **kw):
return tosynthio(bpf(*args, **kw))
if __name__ == "__main__":
print("lpf(24000, 2040, 13) # 1920Hz transition window")
print(list(lpf(24000, 2040, 13)))
print("hpf(24000, 9600, 13) # 960Hz transition window")
print(list(hpf(24000, 9600, 23)))
print("bpf(24000, 1200, 11, 3960, 15) # 2400Hz, 1600Hz transition windows")
print(list(bpf(24000, 1200, 11, 3960, 15)))
print("brf(24000, 960, 19, 2400, 13) # 1200, 1800Hz transition windows")
brf_tst = brf(24000, 960, 19, 2400, 13)
print(brf_tst)
print("brf(24000, 960, 13, 2400, 19) # 1200, 1800Hz transition windows")
brf_tst = brf(24000, 960, 13, 2400, 19)
print(brf_tst)
print("lpf(1, 0.1, 59, blackman) # 1920Hz transition window, blackman")
print(lpf(1, 0.1, 59, blackman))

View File

@ -1,10 +1,10 @@
()
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None),)
(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=None, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None),)
[-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383]
(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None))
(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=None, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=None, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None))
[-1, -1, -1, -1, -1, -1, -1, -1, 28045, -1, -1, -1, -1, -28046, -1, -1, -1, -1, 28045, -1, -1, -1, -1, -28046]
(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None),)
(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, envelope=None, filter=None, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None),)
[-1, -1, -1, 28045, -1, -1, -1, -1, -1, -1, -1, -1, 28045, -1, -1, -1, -1, -28046, -1, -1, -1, -1, 28045, -1]
(-5242, 5241)
(-10484, 10484)