diff --git a/ports/unix/variants/coverage/mpconfigvariant.mk b/ports/unix/variants/coverage/mpconfigvariant.mk index 3dd3f334081c..246156f3564a 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.mk +++ b/ports/unix/variants/coverage/mpconfigvariant.mk @@ -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 \ @@ -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 \ diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index dc3d35d8df22..f4335f4b2ae5 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -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 \ diff --git a/shared-bindings/audiofilters/Distortion.c b/shared-bindings/audiofilters/Distortion.c new file mode 100644 index 000000000000..9fb38306e326 --- /dev/null +++ b/shared-bindings/audiofilters/Distortion.c @@ -0,0 +1,385 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared-bindings/audiofilters/Distortion.h" + +#include "shared/runtime/context_manager_helpers.h" +#include "py/binary.h" +#include "py/enum.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/util.h" +#include "shared-module/synthio/block.h" + +//| class DistortionMode: +//| """The method of distortion used by the `audiofilters.Distortion` effect.""" +//| +//| CLIP: DistortionMode +//| """Digital distortion effect which cuts off peaks at the top and bottom of the waveform.""" +//| +//| LOFI: DistortionMode +//| """Low-resolution digital distortion effect (bit depth reduction). You can use it to emulate the sound of early digital audio devices.""" +//| +//| OVERDRIVE: DistortionMode +//| """Emulates the warm distortion produced by a field effect transistor, which is commonly used in solid-state musical instrument amplifiers. The `audiofilters.Distortion.drive` property has no effect in this mode.""" +//| +//| WAVESHAPE: DistortionMode +//| """Waveshaper distortions are used mainly by electronic musicians to achieve an extra-abrasive sound.""" +//| + +MAKE_ENUM_VALUE(audiofilters_distortion_mode_type, distortion_mode, CLIP, DISTORTION_MODE_CLIP); +MAKE_ENUM_VALUE(audiofilters_distortion_mode_type, distortion_mode, LOFI, DISTORTION_MODE_LOFI); +MAKE_ENUM_VALUE(audiofilters_distortion_mode_type, distortion_mode, OVERDRIVE, DISTORTION_MODE_OVERDRIVE); +MAKE_ENUM_VALUE(audiofilters_distortion_mode_type, distortion_mode, WAVESHAPE, DISTORTION_MODE_WAVESHAPE); + +MAKE_ENUM_MAP(audiofilters_distortion_mode) { + MAKE_ENUM_MAP_ENTRY(distortion_mode, CLIP), + MAKE_ENUM_MAP_ENTRY(distortion_mode, LOFI), + MAKE_ENUM_MAP_ENTRY(distortion_mode, OVERDRIVE), + MAKE_ENUM_MAP_ENTRY(distortion_mode, WAVESHAPE), +}; + +static MP_DEFINE_CONST_DICT(audiofilters_distortion_mode_locals_dict, audiofilters_distortion_mode_locals_table); + +MAKE_PRINTER(audiofilters, audiofilters_distortion_mode); + +MAKE_ENUM_TYPE(audiofilters, DistortionMode, audiofilters_distortion_mode); + +static audiofilters_distortion_mode validate_distortion_mode(mp_obj_t obj, qstr arg_name) { + return cp_enum_value(&audiofilters_distortion_mode_type, obj, arg_name); +} + +//| class Distortion: +//| """A Distortion effect""" +//| +//| def __init__( +//| self, +//| drive: synthio.BlockInput = 0.0, +//| pre_gain: synthio.BlockInput = 0.0, +//| post_gain: synthio.BlockInput = 0.0, +//| mode: DistortionMode = DistortionMode.CLIP, +//| soft_clip: bool = False, +//| mix: synthio.BlockInput = 1.0, +//| buffer_size: int = 512, +//| sample_rate: int = 8000, +//| bits_per_sample: int = 16, +//| samples_signed: bool = True, +//| channel_count: int = 1, +//| ) -> None: +//| """Create a Distortion effect where the original sample is manipulated to create a distorted +//| sound according to the DistortionMode. +//| +//| The mix parameter allows you to change how much of the unchanged sample passes through to +//| the output to how much of the effect audio you hear as the output. +//| +//| :param synthio.BlockInput drive: Distortion power. Value can range from 0.0 to 1.0. +//| :param synthio.BlockInput pre_gain: Increases or decreases the volume before the effect, in decibels. Value can range from -60 to 60. +//| :param synthio.BlockInput post_gain: Increases or decreases the volume after the effect, in decibels. Value can range from -80 to 24. +//| :param DistortionMode mode: Distortion type. +//| :param bool soft_clip: Whether or not to soft clip (True) or hard clip (False) the output. +//| :param synthio.BlockInput mix: The mix as a ratio of the sample (0.0) to the effect (1.0). +//| :param int buffer_size: The total size in bytes of each of the two playback buffers to use +//| :param int sample_rate: The sample rate to be used +//| :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) +//| +//| Playing adding a distortion to a synth:: +//| +//| import time +//| import board +//| import audiobusio +//| import synthio +//| import audiofilters +//| +//| audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22) +//| synth = synthio.Synthesizer(channel_count=1, sample_rate=44100) +//| effect = audiofilters.Distortion(drive=0.5, mix=1.0, buffer_size=1024, channel_count=1, sample_rate=44100) +//| effect.play(synth) +//| audio.play(effect) +//| +//| note = synthio.Note(261) +//| while True: +//| synth.press(note) +//| time.sleep(0.25) +//| synth.release(note) +//| time.sleep(5)""" +//| ... + +static mp_obj_t audiofilters_distortion_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_drive, ARG_pre_gain, ARG_post_gain, ARG_mode, ARG_soft_clip, 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_drive, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(0)} }, + { MP_QSTR_pre_gain, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(0)} }, + { MP_QSTR_post_gain, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(0)} }, + { MP_QSTR_mode, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_soft_clip, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + { 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} }, + { 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_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count); + mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate); + mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int; + if (bits_per_sample != 8 && bits_per_sample != 16) { + mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16")); + } + + audiofilters_distortion_mode mode = DISTORTION_MODE_CLIP; + if (args[ARG_mode].u_obj != MP_OBJ_NULL) { + mode = validate_distortion_mode(args[ARG_mode].u_obj, MP_QSTR_mode); + } + + audiofilters_distortion_obj_t *self = mp_obj_malloc(audiofilters_distortion_obj_t, &audiofilters_distortion_type); + common_hal_audiofilters_distortion_construct(self, args[ARG_drive].u_obj, args[ARG_pre_gain].u_obj, args[ARG_post_gain].u_obj, mode, args[ARG_soft_clip].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); + return MP_OBJ_FROM_PTR(self); +} + +//| def deinit(self) -> None: +//| """Deinitialises the Distortion.""" +//| ... +static mp_obj_t audiofilters_distortion_deinit(mp_obj_t self_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_distortion_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_deinit_obj, audiofilters_distortion_deinit); + +static void check_for_deinit(audiofilters_distortion_obj_t *self) { + if (common_hal_audiofilters_distortion_deinited(self)) { + raise_deinited_error(); + } +} + +//| def __enter__(self) -> Distortion: +//| """No-op used by Context Managers.""" +//| ... +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +static mp_obj_t audiofilters_distortion_obj___exit__(size_t n_args, const mp_obj_t *args) { + (void)n_args; + common_hal_audiofilters_distortion_deinit(args[0]); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiofilters_distortion___exit___obj, 4, 4, audiofilters_distortion_obj___exit__); + + +//| drive: synthio.BlockInput +//| """Distortion power. Value can range from 0.0 to 1.0.""" +static mp_obj_t audiofilters_distortion_obj_get_drive(mp_obj_t self_in) { + return common_hal_audiofilters_distortion_get_drive(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_drive_obj, audiofilters_distortion_obj_get_drive); + +static mp_obj_t audiofilters_distortion_obj_set_drive(mp_obj_t self_in, mp_obj_t drive_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_distortion_set_drive(self, drive_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_distortion_set_drive_obj, audiofilters_distortion_obj_set_drive); + +MP_PROPERTY_GETSET(audiofilters_distortion_drive_obj, + (mp_obj_t)&audiofilters_distortion_get_drive_obj, + (mp_obj_t)&audiofilters_distortion_set_drive_obj); + + +//| pre_gain: synthio.BlockInput +//| """Increases or decreases the volume before the effect, in decibels. Value can range from -60 to 60.""" +static mp_obj_t audiofilters_distortion_obj_get_pre_gain(mp_obj_t self_in) { + return common_hal_audiofilters_distortion_get_pre_gain(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_pre_gain_obj, audiofilters_distortion_obj_get_pre_gain); + +static mp_obj_t audiofilters_distortion_obj_set_pre_gain(mp_obj_t self_in, mp_obj_t pre_gain_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_distortion_set_pre_gain(self, pre_gain_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_distortion_set_pre_gain_obj, audiofilters_distortion_obj_set_pre_gain); + +MP_PROPERTY_GETSET(audiofilters_distortion_pre_gain_obj, + (mp_obj_t)&audiofilters_distortion_get_pre_gain_obj, + (mp_obj_t)&audiofilters_distortion_set_pre_gain_obj); + + +//| post_gain: synthio.BlockInput +//| """Increases or decreases the volume after the effect, in decibels. Value can range from -80 to 24.""" +static mp_obj_t audiofilters_distortion_obj_get_post_gain(mp_obj_t self_in) { + return common_hal_audiofilters_distortion_get_post_gain(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_post_gain_obj, audiofilters_distortion_obj_get_post_gain); + +static mp_obj_t audiofilters_distortion_obj_set_post_gain(mp_obj_t self_in, mp_obj_t post_gain_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_distortion_set_post_gain(self, post_gain_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_distortion_set_post_gain_obj, audiofilters_distortion_obj_set_post_gain); + +MP_PROPERTY_GETSET(audiofilters_distortion_post_gain_obj, + (mp_obj_t)&audiofilters_distortion_get_post_gain_obj, + (mp_obj_t)&audiofilters_distortion_set_post_gain_obj); + + +//| mode: DistortionMode +//| """Distortion type.""" +static mp_obj_t audiofilters_distortion_obj_get_mode(mp_obj_t self_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return cp_enum_find(&audiofilters_distortion_mode_type, common_hal_audiofilters_distortion_get_mode(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_mode_obj, audiofilters_distortion_obj_get_mode); + +static mp_obj_t audiofilters_distortion_obj_set_mode(mp_obj_t self_in, mp_obj_t mode_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + audiofilters_distortion_mode mode = validate_distortion_mode(mode_in, MP_QSTR_mode); + common_hal_audiofilters_distortion_set_mode(self, mode); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_distortion_set_mode_obj, audiofilters_distortion_obj_set_mode); + +MP_PROPERTY_GETSET(audiofilters_distortion_mode_obj, + (mp_obj_t)&audiofilters_distortion_get_mode_obj, + (mp_obj_t)&audiofilters_distortion_set_mode_obj); + + +//| soft_clip: bool +//| """Whether or not to soft clip (True) or hard clip (False) the output.""" +static mp_obj_t audiofilters_distortion_obj_get_soft_clip(mp_obj_t self_in) { + return mp_obj_new_bool(common_hal_audiofilters_distortion_get_soft_clip(self_in)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_soft_clip_obj, audiofilters_distortion_obj_get_soft_clip); + +static mp_obj_t audiofilters_distortion_obj_set_soft_clip(mp_obj_t self_in, mp_obj_t soft_clip_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_distortion_set_soft_clip(self, mp_obj_is_true(soft_clip_in)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_distortion_set_soft_clip_obj, audiofilters_distortion_obj_set_soft_clip); + +MP_PROPERTY_GETSET(audiofilters_distortion_soft_clip_obj, + (mp_obj_t)&audiofilters_distortion_get_soft_clip_obj, + (mp_obj_t)&audiofilters_distortion_set_soft_clip_obj); + + +//| mix: synthio.BlockInput +//| """The rate the filtered signal mix between 0 and 1 where 0 is only sample and 1 is all effect.""" +static mp_obj_t audiofilters_distortion_obj_get_mix(mp_obj_t self_in) { + return common_hal_audiofilters_distortion_get_mix(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_mix_obj, audiofilters_distortion_obj_get_mix); + +static mp_obj_t audiofilters_distortion_obj_set_mix(mp_obj_t self_in, mp_obj_t mix_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_distortion_set_mix(self, mix_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_distortion_set_mix_obj, audiofilters_distortion_obj_set_mix); + +MP_PROPERTY_GETSET(audiofilters_distortion_mix_obj, + (mp_obj_t)&audiofilters_distortion_get_mix_obj, + (mp_obj_t)&audiofilters_distortion_set_mix_obj); + + +//| playing: bool +//| """True when the effect is playing a sample. (read-only)""" +static mp_obj_t audiofilters_distortion_obj_get_playing(mp_obj_t self_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiofilters_distortion_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_get_playing_obj, audiofilters_distortion_obj_get_playing); + +MP_PROPERTY_GETTER(audiofilters_distortion_playing_obj, + (mp_obj_t)&audiofilters_distortion_get_playing_obj); + +//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None: +//| """Plays the sample once when loop=False and continuously when loop=True. +//| Does not block. Use `playing` to block. +//| +//| The sample must match the encoding settings given in the constructor.""" +//| ... +static mp_obj_t audiofilters_distortion_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_sample, ARG_loop }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, + { MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + }; + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + 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); + + + mp_obj_t sample = args[ARG_sample].u_obj; + common_hal_audiofilters_distortion_play(self, sample, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_distortion_play_obj, 1, audiofilters_distortion_obj_play); + +//| def stop(self) -> None: +//| """Stops playback of the sample.""" +//| ... +//| +static mp_obj_t audiofilters_distortion_obj_stop(mp_obj_t self_in) { + audiofilters_distortion_obj_t *self = MP_OBJ_TO_PTR(self_in); + + common_hal_audiofilters_distortion_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_distortion_stop_obj, audiofilters_distortion_obj_stop); + +static const mp_rom_map_elem_t audiofilters_distortion_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiofilters_distortion_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&audiofilters_distortion___exit___obj) }, + { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiofilters_distortion_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiofilters_distortion_stop_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofilters_distortion_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_drive), MP_ROM_PTR(&audiofilters_distortion_drive_obj) }, + { MP_ROM_QSTR(MP_QSTR_pre_gain), MP_ROM_PTR(&audiofilters_distortion_pre_gain_obj) }, + { MP_ROM_QSTR(MP_QSTR_post_gain), MP_ROM_PTR(&audiofilters_distortion_post_gain_obj) }, + { MP_ROM_QSTR(MP_QSTR_mode), MP_ROM_PTR(&audiofilters_distortion_mode_obj) }, + { MP_ROM_QSTR(MP_QSTR_soft_clip), MP_ROM_PTR(&audiofilters_distortion_soft_clip_obj) }, + { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofilters_distortion_mix_obj) }, +}; +static MP_DEFINE_CONST_DICT(audiofilters_distortion_locals_dict, audiofilters_distortion_locals_dict_table); + +static const audiosample_p_t audiofilters_distortion_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + .sample_rate = (audiosample_sample_rate_fun)common_hal_audiofilters_distortion_get_sample_rate, + .bits_per_sample = (audiosample_bits_per_sample_fun)common_hal_audiofilters_distortion_get_bits_per_sample, + .channel_count = (audiosample_channel_count_fun)common_hal_audiofilters_distortion_get_channel_count, + .reset_buffer = (audiosample_reset_buffer_fun)audiofilters_distortion_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)audiofilters_distortion_get_buffer, + .get_buffer_structure = (audiosample_get_buffer_structure_fun)audiofilters_distortion_get_buffer_structure, +}; + +MP_DEFINE_CONST_OBJ_TYPE( + audiofilters_distortion_type, + MP_QSTR_Distortion, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiofilters_distortion_make_new, + locals_dict, &audiofilters_distortion_locals_dict, + protocol, &audiofilters_distortion_proto + ); diff --git a/shared-bindings/audiofilters/Distortion.h b/shared-bindings/audiofilters/Distortion.h new file mode 100644 index 000000000000..5e4fadfd28b7 --- /dev/null +++ b/shared-bindings/audiofilters/Distortion.h @@ -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); diff --git a/shared-bindings/audiofilters/Filter.c b/shared-bindings/audiofilters/Filter.c index 34c8cf0c8da9..07a5739dcb27 100644 --- a/shared-bindings/audiofilters/Filter.c +++ b/shared-bindings/audiofilters/Filter.c @@ -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} }, diff --git a/shared-bindings/audiofilters/__init__.c b/shared-bindings/audiofilters/__init__.c index c4124515b7d3..7a17ec655e62 100644 --- a/shared-bindings/audiofilters/__init__.c +++ b/shared-bindings/audiofilters/__init__.c @@ -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 @@ -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); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 207d7afafa89..71b5a8ee732a 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -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; diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c index e4eeff48f4c3..c1a21d898117 100644 --- a/shared-module/audiodelays/Echo.c +++ b/shared-module/audiodelays/Echo.c @@ -7,6 +7,7 @@ #include #include "py/runtime.h" +#include 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, @@ -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); @@ -77,7 +78,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); @@ -85,6 +86,9 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_ } 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); @@ -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)); @@ -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) { @@ -251,32 +258,6 @@ 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) { @@ -284,15 +265,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; @@ -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) { @@ -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 { @@ -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]; @@ -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; @@ -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 { @@ -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; + } } } diff --git a/shared-module/audiodelays/Echo.h b/shared-module/audiodelays/Echo.h index 8742f83898a7..dd0531818624 100644 --- a/shared-module/audiodelays/Echo.h +++ b/shared-module/audiodelays/Echo.h @@ -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; @@ -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; diff --git a/shared-module/audiofilters/Distortion.c b/shared-module/audiofilters/Distortion.c new file mode 100644 index 000000000000..04ce082b9576 --- /dev/null +++ b/shared-module/audiofilters/Distortion.c @@ -0,0 +1,406 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include +#include "py/obj.h" +#include "py/runtime.h" +#include +#include "shared-bindings/audiofilters/Distortion.h" +#include "shared-module/audiofilters/Distortion.h" + +/** + * Based on Godot's AudioEffectDistortion + * - https://docs.godotengine.org/en/stable/classes/class_audioeffectdistortion.html + * - https://github.com/godotengine/godot/blob/master/servers/audio/effects/audio_effect_distortion.cpp + */ + +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) { + + // Basic settings every effect and audio sample has + // These are the effects values, not the source sample(s) + self->bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places + self->samples_signed = samples_signed; // Are the samples we provide signed (common is true) + self->channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo + self->sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects + + // To smooth things out as CircuitPython is doing other tasks most audio objects have a buffer + // A double buffer is set up here so the audio output can use DMA on buffer 1 while we + // write to and create buffer 2. + // This buffer is what is passed to the audio component that plays the effect. + // Samples are set sequentially. For stereo audio they are passed L/R/L/R/... + self->buffer_len = buffer_size; // in bytes + + self->buffer[0] = m_malloc(self->buffer_len); + if (self->buffer[0] == NULL) { + common_hal_audiofilters_distortion_deinit(self); + m_malloc_fail(self->buffer_len); + } + memset(self->buffer[0], 0, self->buffer_len); + + self->buffer[1] = m_malloc(self->buffer_len); + if (self->buffer[1] == NULL) { + common_hal_audiofilters_distortion_deinit(self); + m_malloc_fail(self->buffer_len); + } + memset(self->buffer[1], 0, self->buffer_len); + + self->last_buf_idx = 1; // Which buffer to use first, toggle between 0 and 1 + + // Initialize other values most effects will need. + self->sample = NULL; // The current playing sample + self->sample_remaining_buffer = NULL; // Pointer to the start of the sample buffer we have not played + self->sample_buffer_length = 0; // How many samples do we have left to play (these may be 16 bit!) + self->loop = false; // When the sample is done do we loop to the start again or stop (e.g. in a wav file) + self->more_data = false; // Is there still more data to read from the sample or did we finish + + // The below section sets up the effect's starting values. + + synthio_block_assign_slot(drive, &self->drive, MP_QSTR_drive); + synthio_block_assign_slot(pre_gain, &self->pre_gain, MP_QSTR_pre_gain); + synthio_block_assign_slot(post_gain, &self->post_gain, MP_QSTR_post_gain); + synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix); + + self->mode = mode; + self->soft_clip = soft_clip; +} + +bool common_hal_audiofilters_distortion_deinited(audiofilters_distortion_obj_t *self) { + if (self->buffer[0] == NULL) { + return true; + } + return false; +} + +void common_hal_audiofilters_distortion_deinit(audiofilters_distortion_obj_t *self) { + self->buffer[0] = NULL; + self->buffer[1] = NULL; +} + +mp_obj_t common_hal_audiofilters_distortion_get_drive(audiofilters_distortion_obj_t *self) { + return self->drive.obj; +} + +void common_hal_audiofilters_distortion_set_drive(audiofilters_distortion_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->drive, MP_QSTR_drive); +} + +mp_obj_t common_hal_audiofilters_distortion_get_pre_gain(audiofilters_distortion_obj_t *self) { + return self->pre_gain.obj; +} + +void common_hal_audiofilters_distortion_set_pre_gain(audiofilters_distortion_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->pre_gain, MP_QSTR_pre_gain); +} + +mp_obj_t common_hal_audiofilters_distortion_get_post_gain(audiofilters_distortion_obj_t *self) { + return self->post_gain.obj; +} + +void common_hal_audiofilters_distortion_set_post_gain(audiofilters_distortion_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->post_gain, MP_QSTR_post_gain); +} + +audiofilters_distortion_mode common_hal_audiofilters_distortion_get_mode(audiofilters_distortion_obj_t *self) { + return self->mode; +} + +void common_hal_audiofilters_distortion_set_mode(audiofilters_distortion_obj_t *self, audiofilters_distortion_mode arg) { + self->mode = arg; +} + +bool common_hal_audiofilters_distortion_get_soft_clip(audiofilters_distortion_obj_t *self) { + return self->soft_clip; +} + +void common_hal_audiofilters_distortion_set_soft_clip(audiofilters_distortion_obj_t *self, bool soft_clip) { + self->soft_clip = soft_clip; +} + +mp_obj_t common_hal_audiofilters_distortion_get_mix(audiofilters_distortion_obj_t *self) { + return self->mix.obj; +} + +void common_hal_audiofilters_distortion_set_mix(audiofilters_distortion_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->mix, MP_QSTR_mix); +} + +uint32_t common_hal_audiofilters_distortion_get_sample_rate(audiofilters_distortion_obj_t *self) { + return self->sample_rate; +} + +uint8_t common_hal_audiofilters_distortion_get_channel_count(audiofilters_distortion_obj_t *self) { + return self->channel_count; +} + +uint8_t common_hal_audiofilters_distortion_get_bits_per_sample(audiofilters_distortion_obj_t *self) { + return self->bits_per_sample; +} + +void audiofilters_distortion_reset_buffer(audiofilters_distortion_obj_t *self, + bool single_channel_output, + uint8_t channel) { + + memset(self->buffer[0], 0, self->buffer_len); + memset(self->buffer[1], 0, self->buffer_len); +} + +bool common_hal_audiofilters_distortion_get_playing(audiofilters_distortion_obj_t *self) { + return self->sample != NULL; +} + +void common_hal_audiofilters_distortion_play(audiofilters_distortion_obj_t *self, mp_obj_t sample, bool loop) { + // When a sample is to be played we must ensure the samples values matches what we expect + // Then we reset the sample and get the first buffer to play + // The get_buffer function will actually process that data + + if (audiosample_sample_rate(sample) != self->sample_rate) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_sample_rate); + } + if (audiosample_channel_count(sample) != self->channel_count) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_channel_count); + } + if (audiosample_bits_per_sample(sample) != self->bits_per_sample) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_bits_per_sample); + } + bool single_buffer; + bool samples_signed; + uint32_t max_buffer_length; + uint8_t spacing; + audiosample_get_buffer_structure(sample, false, &single_buffer, &samples_signed, &max_buffer_length, &spacing); + if (samples_signed != self->samples_signed) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_signedness); + } + + self->sample = sample; + self->loop = loop; + + audiosample_reset_buffer(self->sample, false, 0); + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + + // Track remaining sample length in terms of bytes per sample + self->sample_buffer_length /= (self->bits_per_sample / 8); + // Store if we have more data in the sample to retrieve + self->more_data = result == GET_BUFFER_MORE_DATA; + + return; +} + +void common_hal_audiofilters_distortion_stop(audiofilters_distortion_obj_t *self) { + // When the sample is set to stop playing do any cleanup here + self->sample = NULL; + return; +} + +static mp_float_t db_to_linear(mp_float_t value) { + return MICROPY_FLOAT_C_FUN(exp)(value * MICROPY_FLOAT_CONST(0.11512925464970228420089957273422)); +} + +audioio_get_buffer_result_t audiofilters_distortion_get_buffer(audiofilters_distortion_obj_t *self, bool single_channel_output, uint8_t channel, + uint8_t **buffer, uint32_t *buffer_length) { + + // Switch our buffers to the other buffer + self->last_buf_idx = !self->last_buf_idx; + + // If we are using 16 bit samples we need a 16 bit pointer, 8 bit needs an 8 bit pointer + int16_t *word_buffer = (int16_t *)self->buffer[self->last_buf_idx]; + int8_t *hword_buffer = self->buffer[self->last_buf_idx]; + uint32_t length = self->buffer_len / (self->bits_per_sample / 8); + + // 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 + if (self->sample_buffer_length == 0) { + if (!self->more_data) { // The sample has indicated it has no more data to play + if (self->loop && self->sample) { // If we are supposed to loop reset the sample to the start + audiosample_reset_buffer(self->sample, false, 0); + } else { // If we were not supposed to loop the sample, stop playing it + self->sample = NULL; + } + } + if (self->sample) { + // Load another sample buffer to play + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + // Track length in terms of words. + self->sample_buffer_length /= (self->bits_per_sample / 8); + self->more_data = result == GET_BUFFER_MORE_DATA; + } + } + + if (self->sample == NULL) { + if (self->samples_signed) { + memset(word_buffer, 0, length * (self->bits_per_sample / 8)); + } else { + // For unsigned samples set to the middle which is "quiet" + if (MP_LIKELY(self->bits_per_sample == 16)) { + memset(word_buffer, 32768, length * (self->bits_per_sample / 8)); + } else { + memset(hword_buffer, 128, length * (self->bits_per_sample / 8)); + } + } + + // tick all block inputs + shared_bindings_synthio_lfo_tick(self->sample_rate, length / self->channel_count); + (void)synthio_block_slot_get(&self->drive); + (void)synthio_block_slot_get(&self->pre_gain); + (void)synthio_block_slot_get(&self->post_gain); + (void)synthio_block_slot_get(&self->mix); + + length = 0; + } else { + // we have a sample to play and apply effect + // 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(MIN(self->sample_buffer_length, length), SYNTHIO_MAX_DUR * self->channel_count); + + 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 + + // 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 drive = synthio_block_slot_get_limited(&self->drive, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + mp_float_t pre_gain = db_to_linear(synthio_block_slot_get_limited(&self->pre_gain, MICROPY_FLOAT_CONST(-60.0), MICROPY_FLOAT_CONST(60.0))); + mp_float_t post_gain = db_to_linear(synthio_block_slot_get_limited(&self->post_gain, MICROPY_FLOAT_CONST(-80.0), MICROPY_FLOAT_CONST(24.0))); + mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + + // Modify drive value depending on mode + uint32_t word_mask = 0; + if (self->mode == DISTORTION_MODE_CLIP) { + drive = MICROPY_FLOAT_CONST(1.0001) - drive; + } else if (self->mode == DISTORTION_MODE_WAVESHAPE) { + drive = MICROPY_FLOAT_CONST(2.0) * drive / (MICROPY_FLOAT_CONST(1.0001) - drive); + } else if (self->mode == DISTORTION_MODE_LOFI) { + word_mask = 0xFFFFFFFF ^ ((1 << (uint32_t)MICROPY_FLOAT_C_FUN(round)(drive * MICROPY_FLOAT_CONST(14.0))) - 1); + } + + 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]; + } else { + hword_buffer[i] = sample_hsrc[i]; + } + } + } else { + for (uint32_t i = 0; i < n; i++) { + int32_t sample_word = 0; + if (MP_LIKELY(self->bits_per_sample == 16)) { + sample_word = sample_src[i]; + } else { + if (self->samples_signed) { + sample_word = sample_hsrc[i]; + } else { + // Be careful here changing from an 8 bit unsigned to signed into a 32-bit signed + sample_word = (int8_t)(((uint8_t)sample_hsrc[i]) ^ 0x80); + } + } + + // Apply pre-gain + int32_t word = (int32_t)(sample_word * pre_gain); + + // Apply bit mask before converting to float + if (self->mode == DISTORTION_MODE_LOFI) { + word = word & word_mask; + } + + if (self->mode != DISTORTION_MODE_LOFI || self->soft_clip) { + // Convert sample to float + mp_float_t wordf = word / MICROPY_FLOAT_CONST(32768.0); + + switch (self->mode) { + case DISTORTION_MODE_CLIP: { + wordf = MICROPY_FLOAT_C_FUN(pow)(MICROPY_FLOAT_C_FUN(fabs)(wordf), drive); + if (word < 0) { + wordf *= MICROPY_FLOAT_CONST(-1.0); + } + } break; + case DISTORTION_MODE_LOFI: + break; + case DISTORTION_MODE_OVERDRIVE: { + wordf *= MICROPY_FLOAT_CONST(0.686306); + mp_float_t z = MICROPY_FLOAT_CONST(1.0) + MICROPY_FLOAT_C_FUN(exp)(MICROPY_FLOAT_C_FUN(sqrt)(MICROPY_FLOAT_C_FUN(fabs)(wordf)) * MICROPY_FLOAT_CONST(-0.75)); + mp_float_t word_exp = MICROPY_FLOAT_C_FUN(exp)(wordf); + wordf *= MICROPY_FLOAT_CONST(-1.0); + wordf = (word_exp - MICROPY_FLOAT_C_FUN(exp)(wordf * z)) / (word_exp + MICROPY_FLOAT_C_FUN(exp)(wordf)); + } break; + case DISTORTION_MODE_WAVESHAPE: { + wordf = (MICROPY_FLOAT_CONST(1.0) + drive) * wordf / (MICROPY_FLOAT_CONST(1.0) + drive * MICROPY_FLOAT_C_FUN(fabs)(wordf)); + } break; + } + + // Apply post-gain + wordf = wordf * post_gain; + + // Soft clip + if (self->soft_clip) { + if (wordf > 0) { + wordf = MICROPY_FLOAT_CONST(1.0) - MICROPY_FLOAT_C_FUN(exp)(-wordf); + } else { + wordf = MICROPY_FLOAT_CONST(-1.0) + MICROPY_FLOAT_C_FUN(exp)(wordf); + } + } + + // Convert sample back to signed integer + word = (int32_t)(wordf * MICROPY_FLOAT_CONST(32767.0)); + } else { + // Apply post-gain + word = (int32_t)(word * post_gain); + } + + // Hard clip + if (!self->soft_clip) { + word = MIN(MAX(word, -32767), 32768); + } + + if (MP_LIKELY(self->bits_per_sample == 16)) { + 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 = (int8_t)((sample_word * (MICROPY_FLOAT_CONST(1.0) - mix)) + (word * mix)); + if (self->samples_signed) { + hword_buffer[i] = mixed; + } else { + hword_buffer[i] = (uint8_t)mixed ^ 0x80; + } + } + } + } + + // Update the remaining length and the buffer positions based on how much we wrote into our buffer + length -= n; + word_buffer += n; + hword_buffer += n; + self->sample_remaining_buffer += (n * (self->bits_per_sample / 8)); + self->sample_buffer_length -= n; + } + } + + // 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; + + // Distortion always returns more data but some effects may return GET_BUFFER_DONE or GET_BUFFER_ERROR (see audiocore/__init__.h) + return GET_BUFFER_MORE_DATA; +} + +void audiofilters_distortion_get_buffer_structure(audiofilters_distortion_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing) { + + // Return information about the effect's buffer (not the sample's) + // These are used by calling audio objects to determine how to handle the effect's buffer + *single_buffer = false; + *samples_signed = self->samples_signed; + *max_buffer_length = self->buffer_len; + if (single_channel_output) { + *spacing = self->channel_count; + } else { + *spacing = 1; + } +} diff --git a/shared-module/audiofilters/Distortion.h b/shared-module/audiofilters/Distortion.h new file mode 100644 index 000000000000..86f5c71cf42a --- /dev/null +++ b/shared-module/audiofilters/Distortion.h @@ -0,0 +1,60 @@ +// 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 "py/obj.h" + +#include "shared-bindings/audiofilters/Distortion.h" +#include "shared-module/audiocore/__init__.h" +#include "shared-module/synthio/block.h" + +typedef enum { + DISTORTION_MODE_CLIP, + DISTORTION_MODE_LOFI, + DISTORTION_MODE_OVERDRIVE, + DISTORTION_MODE_WAVESHAPE, +} audiofilters_distortion_mode; + +extern const mp_obj_type_t audiofilters_distortion_type; + +typedef struct { + mp_obj_base_t base; + synthio_block_slot_t drive; + synthio_block_slot_t pre_gain; + synthio_block_slot_t post_gain; + audiofilters_distortion_mode mode; + bool soft_clip; + synthio_block_slot_t mix; + + uint8_t bits_per_sample; + bool samples_signed; + uint8_t channel_count; + uint32_t sample_rate; + + int8_t *buffer[2]; + uint8_t last_buf_idx; + uint32_t buffer_len; // max buffer in bytes + + uint8_t *sample_remaining_buffer; + uint32_t sample_buffer_length; + + bool loop; + bool more_data; + + mp_obj_t sample; +} audiofilters_distortion_obj_t; + +void audiofilters_distortion_reset_buffer(audiofilters_distortion_obj_t *self, + bool single_channel_output, + uint8_t channel); + +audioio_get_buffer_result_t audiofilters_distortion_get_buffer(audiofilters_distortion_obj_t *self, + bool single_channel_output, uint8_t channel, + uint8_t **buffer, uint32_t *buffer_length); + +void audiofilters_distortion_get_buffer_structure(audiofilters_distortion_obj_t *self, + bool single_channel_output, bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing); diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index dc3efe18ef49..8e9060551954 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -53,10 +53,6 @@ void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, } common_hal_audiofilters_filter_set_filter(self, filter); - // If we did not receive a BlockInput we need to create a default float value - if (mix == MP_OBJ_NULL) { - mix = mp_obj_new_float(1.0); - } synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix); } @@ -216,32 +212,6 @@ void common_hal_audiofilters_filter_stop(audiofilters_filter_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 audiofilters_filter_get_buffer(audiofilters_filter_obj_t *self, bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { (void)channel; @@ -250,9 +220,6 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o 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)); - // Switch our buffers to the other buffer self->last_buf_idx = !self->last_buf_idx; @@ -282,6 +249,10 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o } if (self->sample == NULL) { + // tick all block inputs + shared_bindings_synthio_lfo_tick(self->sample_rate, length / self->channel_count); + (void)synthio_block_slot_get(&self->mix); + if (self->samples_signed) { memset(word_buffer, 0, length * (self->bits_per_sample / 8)); } else { @@ -300,12 +271,16 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o } else { // we have a sample to play and filter // 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); + uint32_t n = MIN(MIN(self->sample_buffer_length, length), SYNTHIO_MAX_DUR * self->channel_count); 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 || !self->filter_states) { // if mix is zero pure sample only or no biquad filter objects are provided + // 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)); + + if (mix <= MICROPY_FLOAT_CONST(0.01) || !self->filter_states) { // if mix is zero pure sample only or no biquad filter objects are provided for (uint32_t i = 0; i < n; i++) { if (MP_LIKELY(self->bits_per_sample == 16)) { word_buffer[i] = sample_src[i]; @@ -340,13 +315,13 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o // Mix processed signal with original sample and transfer to output buffer for (uint32_t j = 0; j < n_samples; j++) { if (MP_LIKELY(self->bits_per_sample == 16)) { - word_buffer[i + j] = mix_down_sample((int32_t)((sample_src[i + j] * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[j] * mix))); + word_buffer[i + j] = synthio_mix_down_sample((int32_t)((sample_src[i + j] * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[j] * mix)), SYNTHIO_MIX_DOWN_SCALE(2)); if (!self->samples_signed) { word_buffer[i + j] ^= 0x8000; } } else { if (self->samples_signed) { - hword_buffer[i + j] = (int8_t)((sample_hsrc[i + j] * (1.0 - mix)) + (self->filter_buffer[j] * mix)); + hword_buffer[i + j] = (int8_t)((sample_hsrc[i + j] * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[j] * mix)); } else { hword_buffer[i + j] = (uint8_t)(((int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80) * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[j] * mix)) ^ 0x80; } diff --git a/shared-module/audiofilters/Filter.h b/shared-module/audiofilters/Filter.h index e2ac4e5b7276..3110f52b4674 100644 --- a/shared-module/audiofilters/Filter.h +++ b/shared-module/audiofilters/Filter.h @@ -9,6 +9,7 @@ #include "shared-bindings/synthio/Biquad.h" #include "shared-module/audiocore/__init__.h" +#include "shared-module/synthio/__init__.h" #include "shared-module/synthio/block.h" #include "shared-module/synthio/Biquad.h" diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index ce3f1d79f2fb..fdb00ecd363f 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -194,7 +194,7 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, uint32_t n = MIN(MIN(voice->buffer_length, length), SYNTHIO_MAX_DUR * self->channel_count); // Get the current level 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); + shared_bindings_synthio_lfo_tick(self->sample_rate, n / self->channel_count); uint16_t level = (uint16_t)(synthio_block_slot_get_limited(&voice->level, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)) * (1 << 15)); #else uint32_t n = MIN(voice->buffer_length, length); diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index c0fe6b16366b..d3d5b068f57d 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -129,28 +129,24 @@ static synthio_envelope_definition_t *synthio_synth_get_note_envelope(synthio_sy } -#define RANGE_LOW (-28000) -#define RANGE_HIGH (28000) #define RANGE_SHIFT (16) -#define RANGE_SCALE (0xfffffff / (32768 * CIRCUITPY_SYNTHIO_MAX_CHANNELS - RANGE_HIGH)) // 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) * CIRCUITPY_SYNTHIO_MAX_CHANNELS, +// largest possible values, (+32768,-32767) * count, // 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) { - sample = (((sample - RANGE_LOW) * RANGE_SCALE) >> RANGE_SHIFT) + RANGE_LOW; - } else if (sample > RANGE_HIGH) { - sample = (((sample - RANGE_HIGH) * RANGE_SCALE) >> RANGE_SHIFT) + RANGE_HIGH; +int16_t synthio_mix_down_sample(int32_t sample, int32_t scale) { + if (sample < SYNTHIO_MIX_DOWN_RANGE_LOW) { + sample = (((sample - SYNTHIO_MIX_DOWN_RANGE_LOW) * scale) >> RANGE_SHIFT) + SYNTHIO_MIX_DOWN_RANGE_LOW; + } else if (sample > SYNTHIO_MIX_DOWN_RANGE_HIGH) { + sample = (((sample - SYNTHIO_MIX_DOWN_RANGE_HIGH) * scale) >> RANGE_SHIFT) + SYNTHIO_MIX_DOWN_RANGE_HIGH; } return sample; } @@ -294,7 +290,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t return; } - shared_bindings_synthio_lfo_tick(synth->sample_rate); + shared_bindings_synthio_lfo_tick(synth->sample_rate, SYNTHIO_MAX_DUR); synth->buffer_index = !synth->buffer_index; synth->other_channel = 1 - channel; @@ -345,7 +341,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t // mix down audio for (size_t i = 0; i < dur * synth->channel_count; i++) { int32_t sample = out_buffer32[i]; - out_buffer16[i] = mix_down_sample(sample); + out_buffer16[i] = synthio_mix_down_sample(sample, SYNTHIO_MIX_DOWN_SCALE(CIRCUITPY_SYNTHIO_MAX_CHANNELS)); } // advance envelope states @@ -487,9 +483,9 @@ uint32_t synthio_frequency_convert_scaled_to_dds(uint64_t frequency_scaled, int3 return (sample_rate / 2 + frequency_scaled) / sample_rate; } -void shared_bindings_synthio_lfo_tick(uint32_t sample_rate) { +void shared_bindings_synthio_lfo_tick(uint32_t sample_rate, uint16_t num_samples) { mp_float_t recip_sample_rate = MICROPY_FLOAT_CONST(1.) / sample_rate; - synthio_global_rate_scale = SYNTHIO_MAX_DUR * recip_sample_rate; + synthio_global_rate_scale = num_samples * recip_sample_rate; synthio_global_W_scale = (2 * MP_PI) * recip_sample_rate; synthio_global_tick++; } diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index d40e1df86854..686a14447257 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -14,6 +14,10 @@ #define SYNTHIO_NOTE_IS_PLAYING(synth, i) ((synth)->envelope_state[(i)].state != SYNTHIO_ENVELOPE_STATE_RELEASE) #define SYNTHIO_FREQUENCY_SHIFT (16) +#define SYNTHIO_MIX_DOWN_RANGE_LOW (-28000) +#define SYNTHIO_MIX_DOWN_RANGE_HIGH (28000) +#define SYNTHIO_MIX_DOWN_SCALE(x) (0xfffffff / (32768 * x - SYNTHIO_MIX_DOWN_RANGE_HIGH)) + #include "shared-module/audiocore/__init__.h" #include "shared-bindings/synthio/__init__.h" @@ -79,6 +83,8 @@ bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_ void synthio_envelope_step(synthio_envelope_definition_t *definition, synthio_envelope_state_t *state, int n_samples); void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp_obj_t obj, uint32_t sample_rate); +int16_t synthio_mix_down_sample(int32_t sample, int32_t scale); + uint64_t synthio_frequency_convert_float_to_scaled(mp_float_t frequency_hz); uint32_t synthio_frequency_convert_float_to_dds(mp_float_t frequency_hz, int32_t sample_rate); uint32_t synthio_frequency_convert_scaled_to_dds(uint64_t frequency_scaled, int32_t sample_rate); @@ -90,4 +96,4 @@ int synthio_sweep_in_step(synthio_lfo_state_t *state, uint16_t dur); extern mp_float_t synthio_global_rate_scale, synthio_global_W_scale; extern uint8_t synthio_global_tick; -void shared_bindings_synthio_lfo_tick(uint32_t sample_rate); +void shared_bindings_synthio_lfo_tick(uint32_t sample_rate, uint16_t num_samples);