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

audiofilters: Add Distortion effect and implement LFO ticking #9776

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
16575fc
Added `Distortion` and `DistortionMode` classes to `audiofilters` mod…
relic-se Oct 23, 2024
46ebae1
Remove separate DistortionMode code files.
relic-se Oct 31, 2024
3a16daf
Remove `audiofilters.DistortionMode.ATAN`
relic-se Oct 31, 2024
1008dd5
Simplify `audiofilters.DistortionMode.LOFI` sample processing with bi…
relic-se Oct 31, 2024
37b6b70
Fix error with null sample handling in `audiofilters.Distortion`.
relic-se Nov 4, 2024
064c3f3
Merge branch 'adafruit:main' into audiofilters_distortion
relic-se Nov 13, 2024
a7060f0
Merge branch 'adafruit:main' into audiofilters_distortion
relic-se Nov 27, 2024
6481b4e
Merge branch 'adafruit:main' into audiofilters_distortion
relic-se Dec 10, 2024
31c9095
Implement `synthio_block_slot_get_limited`.
relic-se Dec 10, 2024
155f197
Convert default float values from null checks to MP_ROM_INT.
relic-se Dec 10, 2024
89f2ae1
Remove unnecessary kwarg setters.
relic-se Dec 10, 2024
5c981f0
Use `MICROPY_FLOAT_CONST` and `MICROPY_FLOAT_C_FUN` within floating p…
relic-se Dec 10, 2024
222ce2c
Apply similar updates to audiofilters.Filter and audiodelays.Echo: MI…
relic-se Dec 10, 2024
0410d22
Added `soft_clip` property to toggle between hard clipping (default) …
relic-se Dec 11, 2024
57022f9
Implemented soft clipping and continued optimization of distortion al…
relic-se Dec 11, 2024
48ca21d
Add Distortion to unix port and make type conversions explicit.
relic-se Dec 11, 2024
4257c62
Variable number of samples within `shared_bindings_synthio_lfo_tick`.
relic-se Dec 12, 2024
0e64e1c
Implement block ticking within audio effects.
relic-se Dec 12, 2024
5fbbeed
Call `shared_bindings_synthio_lfo_tick` on audioeffects in `SYNTHIO_M…
relic-se Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ports/unix/variants/coverage/mpconfigvariant.mk
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ SRC_BITMAP := \
shared-bindings/audiocore/WaveFile.c \
shared-bindings/audiodelays/Echo.c \
shared-bindings/audiodelays/__init__.c \
shared-bindings/audiofilters/Distortion.c \
shared-bindings/audiofilters/Filter.c \
shared-bindings/audiofilters/__init__.c \
shared-bindings/audiomixer/__init__.c \
Expand Down Expand Up @@ -77,6 +78,7 @@ SRC_BITMAP := \
shared-module/audiocore/WaveFile.c \
shared-module/audiodelays/Echo.c \
shared-module/audiodelays/__init__.c \
shared-module/audiofilters/Distortion.c \
shared-module/audiofilters/Filter.c \
shared-module/audiofilters/__init__.c \
shared-module/audiomixer/__init__.c \
Expand Down
1 change: 1 addition & 0 deletions py/circuitpy_defns.mk
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ SRC_SHARED_MODULE_ALL = \
audiocore/__init__.c \
audiodelays/Echo.c \
audiodelays/__init__.c \
audiofilters/Distortion.c \
audiofilters/Filter.c \
audiofilters/__init__.c \
audioio/__init__.c \
Expand Down
386 changes: 386 additions & 0 deletions shared-bindings/audiofilters/Distortion.c

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions shared-bindings/audiofilters/Distortion.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple
//
// SPDX-License-Identifier: MIT

#pragma once

#include "shared-module/audiofilters/Distortion.h"

extern const mp_obj_type_t audiofilters_distortion_type;
extern const mp_obj_type_t audiofilters_distortion_mode_type;

void common_hal_audiofilters_distortion_construct(audiofilters_distortion_obj_t *self,
mp_obj_t drive, mp_obj_t pre_gain, mp_obj_t post_gain,
audiofilters_distortion_mode mode, bool soft_clip, mp_obj_t mix,
uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed,
uint8_t channel_count, uint32_t sample_rate);

void common_hal_audiofilters_distortion_deinit(audiofilters_distortion_obj_t *self);
bool common_hal_audiofilters_distortion_deinited(audiofilters_distortion_obj_t *self);

uint32_t common_hal_audiofilters_distortion_get_sample_rate(audiofilters_distortion_obj_t *self);
uint8_t common_hal_audiofilters_distortion_get_channel_count(audiofilters_distortion_obj_t *self);
uint8_t common_hal_audiofilters_distortion_get_bits_per_sample(audiofilters_distortion_obj_t *self);

mp_obj_t common_hal_audiofilters_distortion_get_drive(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_set_drive(audiofilters_distortion_obj_t *self, mp_obj_t arg);

mp_obj_t common_hal_audiofilters_distortion_get_pre_gain(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_set_pre_gain(audiofilters_distortion_obj_t *self, mp_obj_t arg);

mp_obj_t common_hal_audiofilters_distortion_get_post_gain(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_set_post_gain(audiofilters_distortion_obj_t *self, mp_obj_t arg);

audiofilters_distortion_mode common_hal_audiofilters_distortion_get_mode(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_set_mode(audiofilters_distortion_obj_t *self, audiofilters_distortion_mode mode);

bool common_hal_audiofilters_distortion_get_soft_clip(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_set_soft_clip(audiofilters_distortion_obj_t *self, bool soft_clip);

mp_obj_t common_hal_audiofilters_distortion_get_mix(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_set_mix(audiofilters_distortion_obj_t *self, mp_obj_t arg);

bool common_hal_audiofilters_distortion_get_playing(audiofilters_distortion_obj_t *self);
void common_hal_audiofilters_distortion_play(audiofilters_distortion_obj_t *self, mp_obj_t sample, bool loop);
void common_hal_audiofilters_distortion_stop(audiofilters_distortion_obj_t *self);
2 changes: 1 addition & 1 deletion shared-bindings/audiofilters/Filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ static mp_obj_t audiofilters_filter_make_new(const mp_obj_type_t *type, size_t n
enum { ARG_filter, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1)} },
{ MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} },
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} },
{ MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} },
Expand Down
5 changes: 5 additions & 0 deletions shared-bindings/audiofilters/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "py/runtime.h"

#include "shared-bindings/audiofilters/__init__.h"
#include "shared-bindings/audiofilters/Distortion.h"
#include "shared-bindings/audiofilters/Filter.h"

//| """Support for audio filter effects
Expand All @@ -21,6 +22,10 @@
static const mp_rom_map_elem_t audiofilters_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiofilters) },
{ MP_ROM_QSTR(MP_QSTR_Filter), MP_ROM_PTR(&audiofilters_filter_type) },
{ MP_ROM_QSTR(MP_QSTR_Distortion), MP_ROM_PTR(&audiofilters_distortion_type) },

// Enum-like Classes.
{ MP_ROM_QSTR(MP_QSTR_DistortionMode), MP_ROM_PTR(&audiofilters_distortion_mode_type) },
};

static MP_DEFINE_CONST_DICT(audiofilters_module_globals, audiofilters_module_globals_table);
Expand Down
2 changes: 1 addition & 1 deletion shared-bindings/synthio/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(synthio_voct_to_hz_obj, voct_to_hz);

#if CIRCUITPY_AUDIOCORE_DEBUG
static mp_obj_t synthio_lfo_tick(size_t n, const mp_obj_t *args) {
shared_bindings_synthio_lfo_tick(48000);
shared_bindings_synthio_lfo_tick(48000, SYNTHIO_MAX_DUR);
mp_obj_t result[n];
for (size_t i = 0; i < n; i++) {
synthio_block_slot_t slot;
Expand Down
79 changes: 43 additions & 36 deletions shared-module/audiodelays/Echo.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_

// If we did not receive a BlockInput we need to create a default float value
if (decay == MP_OBJ_NULL) {
decay = mp_obj_new_float(0.7);
decay = mp_obj_new_float(MICROPY_FLOAT_CONST(0.7));
}
synthio_block_assign_slot(decay, &self->decay, MP_QSTR_decay);

if (delay_ms == MP_OBJ_NULL) {
delay_ms = mp_obj_new_float(250.0);
delay_ms = mp_obj_new_float(MICROPY_FLOAT_CONST(250.0));
}
synthio_block_assign_slot(delay_ms, &self->delay_ms, MP_QSTR_delay_ms);

if (mix == MP_OBJ_NULL) {
mix = mp_obj_new_float(0.5);
mix = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
}
synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix);

Expand All @@ -77,7 +77,7 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_

// Allocate the echo buffer for the max possible delay, echo is always 16-bit
self->max_delay_ms = max_delay_ms;
self->max_echo_buffer_len = (uint32_t)(self->sample_rate / 1000.0f * max_delay_ms) * (self->channel_count * sizeof(uint16_t)); // bytes
self->max_echo_buffer_len = (uint32_t)(self->sample_rate / MICROPY_FLOAT_CONST(1000.0) * max_delay_ms) * (self->channel_count * sizeof(uint16_t)); // bytes
self->echo_buffer = m_malloc(self->max_echo_buffer_len);
if (self->echo_buffer == NULL) {
common_hal_audiodelays_echo_deinit(self);
Expand Down Expand Up @@ -284,15 +284,6 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
channel = 0;
}

// get the effect values we need from the BlockInput. These may change at run time so you need to do bounds checking if required
mp_float_t mix = MIN(1.0, MAX(synthio_block_slot_get(&self->mix), 0.0));
mp_float_t decay = MIN(1.0, MAX(synthio_block_slot_get(&self->decay), 0.0));

uint32_t delay_ms = (uint32_t)synthio_block_slot_get(&self->delay_ms);
if (self->current_delay_ms != delay_ms) {
recalculate_delay(self, delay_ms);
}

// Switch our buffers to the other buffer
self->last_buf_idx = !self->last_buf_idx;

Expand All @@ -303,16 +294,6 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *

// The echo buffer is always stored as a 16-bit value internally
int16_t *echo_buffer = (int16_t *)self->echo_buffer;
uint32_t echo_buf_len = self->echo_buffer_len / sizeof(uint16_t);

// Set our echo buffer position accounting for stereo
uint32_t echo_buffer_pos = 0;
if (self->freq_shift) {
echo_buffer_pos = self->echo_buffer_left_pos;
if (channel == 1) {
echo_buffer_pos = self->echo_buffer_right_pos;
}
}

// Loop over the entire length of our buffer to fill it, this may require several calls to get data from the sample
while (length != 0) {
Expand All @@ -334,9 +315,38 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
}
}

// Determine how many bytes we can process to our buffer, the less of the sample we have left and our buffer remaining
uint32_t n;
if (self->sample == NULL) {
n = MIN(length, SYNTHIO_MAX_DUR * self->channel_count);
} else {
n = MIN(MIN(self->sample_buffer_length, length), SYNTHIO_MAX_DUR * self->channel_count);
}

// get the effect values we need from the BlockInput. These may change at run time so you need to do bounds checking if required
shared_bindings_synthio_lfo_tick(self->sample_rate, n / self->channel_count);
mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
mp_float_t decay = synthio_block_slot_get_limited(&self->decay, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));

uint32_t delay_ms = (uint32_t)synthio_block_slot_get(&self->delay_ms);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to check that delay >= 0. Never did in the original code (my bad) but a LFO could go negative and weird things happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting it to an unsigned integer does force it to be >= 0, but a negative result might cause it to wrap around. If it's 0, I believe it will do a minimum of 1 sample of delay. I'll look into it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it should be synthio_block_slot_get_limited(1, some_calculated_maximum) then?

Technically under the C99 standard, conversion from a negative floating point value to an unsigned integer value is undefined behavior.

When a finite value of real floating type is converted to an integer type other than _Bool,
the fractional part is discarded (i.e., the value is truncated toward zero). If the value of
the integral part cannot be represented by the integer type, the behavior is undefined. (6.3.1.4.1)

if (self->current_delay_ms != delay_ms) {
recalculate_delay(self, delay_ms);
}

uint32_t echo_buf_len = self->echo_buffer_len / sizeof(uint16_t);

// Set our echo buffer position accounting for stereo
uint32_t echo_buffer_pos = 0;
if (self->freq_shift) {
echo_buffer_pos = self->echo_buffer_left_pos;
if (channel == 1) {
echo_buffer_pos = self->echo_buffer_right_pos;
}
}

// If we have no sample keep the echo echoing
if (self->sample == NULL) {
if (mix <= 0.01) { // Mix of 0 is pure sample sound. We have no sample so no sound
if (mix <= MICROPY_FLOAT_CONST(0.01)) { // Mix of 0 is pure sample sound. We have no sample so no sound
if (self->samples_signed) {
memset(word_buffer, 0, length * (self->bits_per_sample / 8));
} else {
Expand Down Expand Up @@ -400,13 +410,10 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
length = 0;
} else {
// we have a sample to play and echo
// Determine how many bytes we can process to our buffer, the less of the sample we have left and our buffer remaining
uint32_t n = MIN(self->sample_buffer_length, length);

int16_t *sample_src = (int16_t *)self->sample_remaining_buffer; // for 16-bit samples
int8_t *sample_hsrc = (int8_t *)self->sample_remaining_buffer; // for 8-bit samples

if (mix <= 0.01) { // if mix is zero pure sample only
if (mix <= MICROPY_FLOAT_CONST(0.01)) { // if mix is zero pure sample only
for (uint32_t i = 0; i < n; i++) {
if (MP_LIKELY(self->bits_per_sample == 16)) {
word_buffer[i] = sample_src[i];
Expand Down Expand Up @@ -467,12 +474,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
word = echo + sample_word;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you happen to do a commit before the final review and have a moment after this line could you add:
word = mix_down_sample(word);
I'll try to make it more elegant later but there is an issue and that fixes it.

So the final chunk is:

word = echo + sample_word;
word = mix_down_sample(word);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shared-module/audiodelays/Echo.c:int16_t mix_down_sample(int32_t sample) {
shared-module/audiofilters/Filter.c:int16_t mix_down_sample(int32_t sample) {
shared-module/synthio/__init__.c:int16_t mix_down_sample(int32_t sample) {

we've now got 3 copies of mix_down_sample. If not as part of this PR, please make this have extern linkage somewhere and get rid of the duplicates.


if (MP_LIKELY(self->bits_per_sample == 16)) {
word_buffer[i] = (int16_t)((sample_word * (1.0 - mix)) + (word * mix));
word_buffer[i] = (int16_t)((sample_word * (MICROPY_FLOAT_CONST(1.0) - mix)) + (word * mix));
if (!self->samples_signed) {
word_buffer[i] ^= 0x8000;
}
} else {
int8_t mixed = (int16_t)((sample_word * (1.0 - mix)) + (word * mix));
int8_t mixed = (int16_t)((sample_word * (MICROPY_FLOAT_CONST(1.0) - mix)) + (word * mix));
if (self->samples_signed) {
hword_buffer[i] = mixed;
} else {
Expand Down Expand Up @@ -500,13 +507,13 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
self->sample_remaining_buffer += (n * (self->bits_per_sample / 8));
self->sample_buffer_length -= n;
}
}

if (self->freq_shift) {
if (channel == 0) {
self->echo_buffer_left_pos = echo_buffer_pos;
} else if (channel == 1) {
self->echo_buffer_right_pos = echo_buffer_pos;
if (self->freq_shift) {
if (channel == 0) {
self->echo_buffer_left_pos = echo_buffer_pos;
} else if (channel == 1) {
self->echo_buffer_right_pos = echo_buffer_pos;
}
}
}

Expand Down
Loading