Skip to content

Commit

Permalink
Merge pull request #9776 from relic-se/audiofilters_distortion
Browse files Browse the repository at this point in the history
audiofilters: Add Distortion effect and implement LFO ticking
  • Loading branch information
gamblor21 authored Jan 23, 2025
2 parents 943a2f3 + 815f829 commit 10cd4f0
Show file tree
Hide file tree
Showing 16 changed files with 995 additions and 120 deletions.
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
385 changes: 385 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
117 changes: 53 additions & 64 deletions shared-module/audiodelays/Echo.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <stdint.h>
#include "py/runtime.h"
#include <math.h>

void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_t max_delay_ms,
mp_obj_t delay_ms, mp_obj_t decay, mp_obj_t mix,
Expand Down Expand Up @@ -57,17 +58,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,14 +78,17 @@ 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);
m_malloc_fail(self->max_echo_buffer_len);
}
memset(self->echo_buffer, 0, self->max_echo_buffer_len);

// calculate the length of a single sample in milliseconds
self->sample_ms = MICROPY_FLOAT_CONST(1000.0) / self->sample_rate;

// calculate everything needed for the current delay
mp_float_t f_delay_ms = synthio_block_slot_get(&self->delay_ms);
recalculate_delay(self, f_delay_ms);
Expand Down Expand Up @@ -127,6 +131,9 @@ void common_hal_audiodelays_echo_set_delay_ms(audiodelays_echo_obj_t *self, mp_o
}

void recalculate_delay(audiodelays_echo_obj_t *self, mp_float_t f_delay_ms) {
// Require that delay is at least 1 sample long
f_delay_ms = MAX(f_delay_ms, self->sample_ms);

if (self->freq_shift) {
// Calculate the rate of iteration over the echo buffer with 8 sub-bits
self->echo_buffer_rate = (uint32_t)MAX(self->max_delay_ms / f_delay_ms * MICROPY_FLOAT_CONST(256.0), MICROPY_FLOAT_CONST(1.0));
Expand All @@ -153,7 +160,7 @@ void recalculate_delay(audiodelays_echo_obj_t *self, mp_float_t f_delay_ms) {
memset(self->echo_buffer + self->echo_buffer_len, 0, self->max_echo_buffer_len - self->echo_buffer_len);
}

self->current_delay_ms = (uint32_t)f_delay_ms;
self->current_delay_ms = f_delay_ms;
}

mp_obj_t common_hal_audiodelays_echo_get_decay(audiodelays_echo_obj_t *self) {
Expand Down Expand Up @@ -251,48 +258,13 @@ void common_hal_audiodelays_echo_stop(audiodelays_echo_obj_t *self) {
return;
}

#define RANGE_LOW_16 (-28000)
#define RANGE_HIGH_16 (28000)
#define RANGE_SHIFT_16 (16)
#define RANGE_SCALE_16 (0xfffffff / (32768 * 2 - RANGE_HIGH_16)) // 2 for echo+sample

// dynamic range compression via a downward compressor with hard knee
//
// When the output value is within the range +-28000 (about 85% of full scale),
// it is unchanged. Otherwise, it undergoes a gain reduction so that the
// largest possible values, (+32768,-32767) * 2 (2 for echo and sample),
// still fit within the output range
//
// This produces a much louder overall volume with multiple voices, without
// much additional processing.
//
// https://en.wikipedia.org/wiki/Dynamic_range_compression
static
int16_t mix_down_sample(int32_t sample) {
if (sample < RANGE_LOW_16) {
sample = (((sample - RANGE_LOW_16) * RANGE_SCALE_16) >> RANGE_SHIFT_16) + RANGE_LOW_16;
} else if (sample > RANGE_HIGH_16) {
sample = (((sample - RANGE_HIGH_16) * RANGE_SCALE_16) >> RANGE_SHIFT_16) + RANGE_HIGH_16;
}
return sample;
}

audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *self, bool single_channel_output, uint8_t channel,
uint8_t **buffer, uint32_t *buffer_length) {

if (!single_channel_output) {
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 +275,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 +296,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));

mp_float_t f_delay_ms = synthio_block_slot_get(&self->delay_ms);
if (MICROPY_FLOAT_C_FUN(fabs)(self->current_delay_ms - f_delay_ms) >= self->sample_ms) {
recalculate_delay(self, f_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 +391,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 @@ -440,7 +428,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
}

if (MP_LIKELY(self->bits_per_sample == 16)) {
word = mix_down_sample(word);
word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2));
if (self->freq_shift) {
for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) {
echo_buffer[j % echo_buf_len] = (int16_t)word;
Expand All @@ -465,14 +453,15 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
}

word = echo + sample_word;
word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2));

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 +489,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
4 changes: 3 additions & 1 deletion shared-module/audiodelays/Echo.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "py/obj.h"

#include "shared-module/audiocore/__init__.h"
#include "shared-module/synthio/__init__.h"
#include "shared-module/synthio/block.h"

extern const mp_obj_type_t audiodelays_echo_type;
Expand All @@ -16,7 +17,8 @@ typedef struct {
mp_obj_base_t base;
uint32_t max_delay_ms;
synthio_block_slot_t delay_ms;
uint32_t current_delay_ms;
mp_float_t current_delay_ms;
mp_float_t sample_ms;
synthio_block_slot_t decay;
synthio_block_slot_t mix;

Expand Down
Loading

0 comments on commit 10cd4f0

Please sign in to comment.