Skip to content

Commit

Permalink
Added frequency shift on echo option from @dcooperdalrymple
Browse files Browse the repository at this point in the history
  • Loading branch information
gamblor21 committed Oct 14, 2024
1 parent 4464531 commit e3228d0
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 42 deletions.
39 changes: 36 additions & 3 deletions shared-bindings/audiodelays/Echo.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
//| :param int channel_count: The number of channels the source samples contain. 1 = mono; 2 = stereo.
//| :param int bits_per_sample: The bits per sample of the effect
//| :param bool samples_signed: Effect is signed (True) or unsigned (False)
//| :param bool freq_shift: Do echos change frequency as the echo delay changes
//|
//| Playing adding an echo to a synth::
//|
Expand All @@ -64,7 +65,7 @@
//|
//| audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22)
//| synth = synthio.Synthesizer(channel_count=1, sample_rate=44100)
//| echo = audiodelays.Echo(max_delay_ms=1000, delay_ms=850, decay=0.65, buffer_size=1024, channel_count=1, sample_rate=44100, mix=0.7)
//| echo = audiodelays.Echo(max_delay_ms=1000, delay_ms=850, decay=0.65, buffer_size=1024, channel_count=1, sample_rate=44100, mix=0.7, freq_shift=False)
//| echo.play(synth)
//| audio.play(echo)
//|
Expand All @@ -76,7 +77,7 @@
//| time.sleep(5)"""
//| ...
static mp_obj_t audiodelays_echo_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
enum { ARG_max_delay_ms, ARG_delay_ms, ARG_decay, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, };
enum { ARG_max_delay_ms, ARG_delay_ms, ARG_decay, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, ARG_freq_shift, };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_max_delay_ms, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 500 } },
{ MP_QSTR_delay_ms, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} },
Expand All @@ -87,6 +88,7 @@ static mp_obj_t audiodelays_echo_make_new(const mp_obj_type_t *type, size_t n_ar
{ MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} },
{ MP_QSTR_samples_signed, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true} },
{ MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } },
{ MP_QSTR_freq_shift, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true } },
};

mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
Expand All @@ -102,7 +104,7 @@ static mp_obj_t audiodelays_echo_make_new(const mp_obj_type_t *type, size_t n_ar
}

audiodelays_echo_obj_t *self = mp_obj_malloc(audiodelays_echo_obj_t, &audiodelays_echo_type);
common_hal_audiodelays_echo_construct(self, max_delay_ms, args[ARG_delay_ms].u_obj, args[ARG_decay].u_obj, args[ARG_mix].u_obj, args[ARG_buffer_size].u_int, bits_per_sample, args[ARG_samples_signed].u_bool, channel_count, sample_rate);
common_hal_audiodelays_echo_construct(self, max_delay_ms, args[ARG_delay_ms].u_obj, args[ARG_decay].u_obj, args[ARG_mix].u_obj, args[ARG_buffer_size].u_int, bits_per_sample, args[ARG_samples_signed].u_bool, channel_count, sample_rate, args[ARG_freq_shift].u_bool);

return MP_OBJ_FROM_PTR(self);
}
Expand Down Expand Up @@ -222,6 +224,36 @@ MP_PROPERTY_GETSET(audiodelays_echo_mix_obj,
(mp_obj_t)&audiodelays_echo_get_mix_obj,
(mp_obj_t)&audiodelays_echo_set_mix_obj);



//| freq_shift: bool
//| """Does the echo change frequencies as the delay changes."""
static mp_obj_t audiodelays_echo_obj_get_freq_shift(mp_obj_t self_in) {
return mp_obj_new_bool(common_hal_audiodelays_echo_get_freq_shift(self_in));
}
MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_echo_get_freq_shift_obj, audiodelays_echo_obj_get_freq_shift);

static mp_obj_t audiodelays_echo_obj_set_freq_shift(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_freq_shift };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_freq_shift, MP_ARG_BOOL | MP_ARG_REQUIRED, {} },
};
audiodelays_echo_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

common_hal_audiodelays_echo_set_freq_shift(self, args[ARG_freq_shift].u_bool);

return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_KW(audiodelays_echo_set_freq_shift_obj, 1, audiodelays_echo_obj_set_freq_shift);

MP_PROPERTY_GETSET(audiodelays_echo_freq_shift_obj,
(mp_obj_t)&audiodelays_echo_get_freq_shift_obj,
(mp_obj_t)&audiodelays_echo_set_freq_shift_obj);



//| playing: bool
//| """True when the effect is playing a sample. (read-only)"""
static mp_obj_t audiodelays_echo_obj_get_playing(mp_obj_t self_in) {
Expand Down Expand Up @@ -284,6 +316,7 @@ static const mp_rom_map_elem_t audiodelays_echo_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_delay_ms), MP_ROM_PTR(&audiodelays_echo_delay_ms_obj) },
{ MP_ROM_QSTR(MP_QSTR_decay), MP_ROM_PTR(&audiodelays_echo_decay_obj) },
{ MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiodelays_echo_mix_obj) },
{ MP_ROM_QSTR(MP_QSTR_freq_shift), MP_ROM_PTR(&audiodelays_echo_freq_shift_obj) },
};
static MP_DEFINE_CONST_DICT(audiodelays_echo_locals_dict, audiodelays_echo_locals_dict_table);

Expand Down
5 changes: 4 additions & 1 deletion shared-bindings/audiodelays/Echo.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extern const mp_obj_type_t audiodelays_echo_type;
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,
uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed,
uint8_t channel_count, uint32_t sample_rate);
uint8_t channel_count, uint32_t sample_rate, bool freq_shift);

void common_hal_audiodelays_echo_deinit(audiodelays_echo_obj_t *self);
bool common_hal_audiodelays_echo_deinited(audiodelays_echo_obj_t *self);
Expand All @@ -25,6 +25,9 @@ uint8_t common_hal_audiodelays_echo_get_bits_per_sample(audiodelays_echo_obj_t *
mp_obj_t common_hal_audiodelays_echo_get_delay_ms(audiodelays_echo_obj_t *self);
void common_hal_audiodelays_echo_set_delay_ms(audiodelays_echo_obj_t *self, mp_obj_t delay_ms);

bool common_hal_audiodelays_echo_get_freq_shift(audiodelays_echo_obj_t *self);
void common_hal_audiodelays_echo_set_freq_shift(audiodelays_echo_obj_t *self, bool freq_shift);

mp_obj_t common_hal_audiodelays_echo_get_decay(audiodelays_echo_obj_t *self);
void common_hal_audiodelays_echo_set_decay(audiodelays_echo_obj_t *self, mp_obj_t decay);

Expand Down
163 changes: 126 additions & 37 deletions shared-module/audiodelays/Echo.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2024 Mark Komus
// SPDX-FileCopyrightText: Copyright (c) 2024 Mark Komus, Cooper Dalrymple
//
// SPDX-License-Identifier: MIT
#include "shared-bindings/audiodelays/Echo.h"
Expand All @@ -11,7 +11,10 @@
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,
uint32_t buffer_size, uint8_t bits_per_sample,
bool samples_signed, uint8_t channel_count, uint32_t sample_rate) {
bool samples_signed, uint8_t channel_count, uint32_t sample_rate, bool freq_shift) {

// Set whether the echo shifts frequencies as the delay changes like a doppler effect
self->freq_shift = freq_shift;

// Basic settings every effect and audio sample has
// These are the effects values, not the source sample(s)
Expand Down Expand Up @@ -82,15 +85,17 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_
}
memset(self->echo_buffer, 0, self->max_echo_buffer_len);

// calculate current echo buffer size we use for the given delay
// calculate everything needed for the current delay
mp_float_t f_delay_ms = synthio_block_slot_get(&self->delay_ms);
self->current_delay_ms = f_delay_ms;
self->echo_buffer_len = self->sample_rate / 1000.0f * f_delay_ms * (self->channel_count * sizeof(uint16_t));
recalculate_delay(self, f_delay_ms);

// read is where we read previous echo from delay_ms ago to play back now
// write is where the store the latest playing sample to echo back later
self->echo_buffer_read_pos = self->buffer_len / sizeof(uint16_t);
self->echo_buffer_write_pos = 0;

// where we read the previous echo from delay_ms ago to play back now (for freq shift)
self->echo_buffer_left_pos = self->echo_buffer_right_pos = 0;
}

bool common_hal_audiodelays_echo_deinited(audiodelays_echo_obj_t *self) {
Expand All @@ -109,7 +114,6 @@ void common_hal_audiodelays_echo_deinit(audiodelays_echo_obj_t *self) {
self->buffer[1] = NULL;
}


mp_obj_t common_hal_audiodelays_echo_get_delay_ms(audiodelays_echo_obj_t *self) {
return self->delay_ms.obj;
}
Expand All @@ -123,23 +127,32 @@ 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) {
// Calculate the current echo buffer length in bytes
if (self->freq_shift) {
// Calculate the rate of iteration over the echo buffer with 8 sub-bits
self->echo_buffer_rate = MAX(self->max_delay_ms / f_delay_ms * 256.0f, 1.0);
self->echo_buffer_len = self->max_echo_buffer_len;
} else {
// Calculate the current echo buffer length in bytes
uint32_t new_echo_buffer_len = self->sample_rate / 1000.0f * f_delay_ms * (self->channel_count * sizeof(uint16_t));

// Check if our new echo is too long for our maximum buffer
if (new_echo_buffer_len > self->max_echo_buffer_len) {
return;
} else if (new_echo_buffer_len < 0.0) { // or too short!
return;
}

uint32_t new_echo_buffer_len = self->sample_rate / 1000.0f * f_delay_ms * (self->channel_count * sizeof(uint16_t));
// If the echo buffer is larger then our audio buffer weird things happen
if (new_echo_buffer_len < self->buffer_len) {
return;
}

// Check if our new echo is too long for our maximum buffer
if (new_echo_buffer_len > self->max_echo_buffer_len) {
return;
} else if (new_echo_buffer_len < 0.0) { // or too short!
return;
}
self->echo_buffer_len = new_echo_buffer_len;

// If the echo buffer is larger then our audio buffer weird things happen
if (new_echo_buffer_len < self->buffer_len) {
return;
// Clear the now unused part of the buffer or some weird artifacts appear
memset(self->echo_buffer + self->echo_buffer_len, 0, self->max_echo_buffer_len - self->echo_buffer_len);
}

self->echo_buffer_len = new_echo_buffer_len;
self->current_delay_ms = f_delay_ms;
}

Expand All @@ -159,6 +172,16 @@ void common_hal_audiodelays_echo_set_mix(audiodelays_echo_obj_t *self, mp_obj_t
synthio_block_assign_slot(arg, &self->mix, MP_QSTR_mix);
}

bool common_hal_audiodelays_echo_get_freq_shift(audiodelays_echo_obj_t *self) {
return self->freq_shift;
}

void common_hal_audiodelays_echo_set_freq_shift(audiodelays_echo_obj_t *self, bool freq_shift) {
self->freq_shift = freq_shift;
uint32_t delay_ms = (uint32_t)synthio_block_slot_get(&self->delay_ms);
recalculate_delay(self, delay_ms);
}

uint32_t common_hal_audiodelays_echo_get_sample_rate(audiodelays_echo_obj_t *self) {
return self->sample_rate;
}
Expand Down Expand Up @@ -257,6 +280,10 @@ int16_t mix_down_sample(int32_t 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));
Expand All @@ -278,6 +305,15 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
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) {
// Check if there is no more sample to play, we will either load more data, reset the sample if loop is on or clear the sample
Expand Down Expand Up @@ -314,27 +350,46 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
} else {
// Since we have no sample we can just iterate over the our entire remaining buffer and finish
for (uint32_t i = 0; i < length; i++) {
int16_t echo = echo_buffer[self->echo_buffer_read_pos++] * decay;
echo_buffer[self->echo_buffer_write_pos++] = echo;
int16_t echo, word = 0;
uint32_t next_buffer_pos = 0;

if (self->freq_shift) {
echo = echo_buffer[echo_buffer_pos >> 8];
next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate;

word = echo * decay;
for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) {
echo_buffer[j % echo_buf_len] = word;
}
} else {
echo = echo_buffer[self->echo_buffer_read_pos++];
word = echo * decay;
echo_buffer[self->echo_buffer_write_pos++] = word;
}

word = echo * mix;

if (MP_LIKELY(self->bits_per_sample == 16)) {
word_buffer[i] = echo * mix;
word_buffer[i] = word;
if (!self->samples_signed) {
word_buffer[i] ^= 0x8000;
}
} else {
echo = echo * mix;
hword_buffer[i] = echo;
hword_buffer[i] = (int8_t)word;
if (!self->samples_signed) {
hword_buffer[i] ^= 0x80;
}
}

if (self->echo_buffer_read_pos >= echo_buf_len) {
self->echo_buffer_read_pos = 0;
}
if (self->echo_buffer_write_pos >= echo_buf_len) {
self->echo_buffer_write_pos = 0;
if (self->freq_shift) {
echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
} else {
if (self->echo_buffer_read_pos >= echo_buf_len) {
self->echo_buffer_read_pos = 0;
}
if (self->echo_buffer_write_pos >= echo_buf_len) {
self->echo_buffer_write_pos = 0;
}
}
}
}
Expand Down Expand Up @@ -370,22 +425,44 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
}
}

int32_t echo = echo_buffer[self->echo_buffer_read_pos++] * decay;
int32_t word = echo + sample_word;
int32_t echo, word = 0;
uint32_t next_buffer_pos = 0;
if (self->freq_shift) {
echo = echo_buffer[echo_buffer_pos >> 8];
next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate;
word = echo * decay + sample_word;
} else {
echo = echo_buffer[self->echo_buffer_read_pos++];
word = echo * decay + sample_word;
}

if (MP_LIKELY(self->bits_per_sample == 16)) {
word = mix_down_sample(word);
echo_buffer[self->echo_buffer_write_pos++] = (int16_t)word;
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;
}
} else {
echo_buffer[self->echo_buffer_write_pos++] = (int16_t)word;
}
} else {
// Do not have mix_down for 8 bit so just hard cap samples into 1 byte
if (word > 127) {
word = 127;
} else if (word < -128) {
word = -128;
}
echo_buffer[self->echo_buffer_write_pos++] = (int8_t)word;
if (self->freq_shift) {
for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) {
echo_buffer[j % echo_buf_len] = (int8_t)word;
}
} else {
echo_buffer[self->echo_buffer_write_pos++] = (int8_t)word;
}
}

word = echo + sample_word;

if (MP_LIKELY(self->bits_per_sample == 16)) {
word_buffer[i] = (sample_word * (1.0 - mix)) + (word * mix);
if (!self->samples_signed) {
Expand All @@ -400,11 +477,15 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
}
}

if (self->echo_buffer_read_pos >= echo_buf_len) {
self->echo_buffer_read_pos = 0;
}
if (self->echo_buffer_write_pos >= echo_buf_len) {
self->echo_buffer_write_pos = 0;
if (self->freq_shift) {
echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
} else {
if (self->echo_buffer_read_pos >= echo_buf_len) {
self->echo_buffer_read_pos = 0;
}
if (self->echo_buffer_write_pos >= echo_buf_len) {
self->echo_buffer_write_pos = 0;
}
}
}
}
Expand All @@ -418,6 +499,14 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
}
}

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;
}
}

// Finally pass our buffer and length to the calling audio function
*buffer = (uint8_t *)self->buffer[self->last_buf_idx];
*buffer_length = self->buffer_len;
Expand Down
Loading

0 comments on commit e3228d0

Please sign in to comment.