Repository: adafruit/circuitpython
Pull request #10196: Audio Effect Reverb
Author: login: gamblor21
Created: 2025-03-28T21:31:29Z | Merged: 2025-04-21T18:19:49Z

Adding a reverb effect for audio effects, based on FreeVerb.

Question:
1. Should this be the final location for the effect (`audiodelays`)? Early on we debated more modules but I'm not sure if there is value to make a Reverb only one.

Time permitting I may still add in a way to control dry and wet mix independently. So mix could either be `mix=0.5` or `mix=(0.3,0.3)` 


Diff:
diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot
index 486d4eb149be3..6beb320527e43 100644
--- a/locale/circuitpython.pot
+++ b/locale/circuitpython.pot
@@ -2595,6 +2595,11 @@ msgstr ""
 #: shared-bindings/audiodelays/MultiTapDelay.c
 #: shared-bindings/audiodelays/PitchShift.c
 #: shared-bindings/audiofilters/Distortion.c
+#: shared-bindings/audiodelays/Reverb.c
+msgid "bits_per_sample must be 16"
+msgstr ""
+
+#: shared-bindings/audiodelays/Echo.c shared-bindings/audiofilters/Distortion.c
 #: shared-bindings/audiofilters/Filter.c shared-bindings/audiomixer/Mixer.c
 msgid "bits_per_sample must be 8 or 16"
 msgstr ""
@@ -4012,6 +4017,10 @@ msgstr ""
 msgid "rsplit(None,n)"
 msgstr ""
 
+#: shared-bindings/audiodelays/Reverb.c
+msgid "samples_signed must be true"
+msgstr ""
+
 #: ports/atmel-samd/common-hal/audiobusio/PDMIn.c
 #: ports/raspberrypi/common-hal/audiobusio/PDMIn.c
 msgid "sampling rate out of range"
diff --git a/ports/unix/variants/coverage/mpconfigvariant.mk b/ports/unix/variants/coverage/mpconfigvariant.mk
index e18645409fdd8..e1924479bbf53 100644
--- a/ports/unix/variants/coverage/mpconfigvariant.mk
+++ b/ports/unix/variants/coverage/mpconfigvariant.mk
@@ -41,6 +41,8 @@ SRC_BITMAP := \
 	shared-bindings/audiofilters/Distortion.c \
 	shared-bindings/audiofilters/Filter.c \
 	shared-bindings/audiofilters/__init__.c \
+	shared-bindings/audiofreeverb/Freeverb.c \
+	shared-bindings/audiofreeverb/__init__.c \
 	shared-bindings/audiomixer/__init__.c \
 	shared-bindings/audiomixer/Mixer.c \
 	shared-bindings/audiomixer/MixerVoice.c \
@@ -86,6 +88,8 @@ SRC_BITMAP := \
 	shared-module/audiofilters/Distortion.c \
 	shared-module/audiofilters/Filter.c \
 	shared-module/audiofilters/__init__.c \
+	shared-module/audiofreeverb/Freeverb.c \
+	shared-module/audiofreeverb/__init__.c \
 	shared-module/audiomixer/__init__.c \
 	shared-module/audiomp3/MP3Decoder.c \
 	shared-module/audiomixer/Mixer.c \
diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
index 624e7e69a3fd1..aecccebb9c850 100644
--- a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
index 07ab1f7f4dfc6..27e4f35f22366 100644
--- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
index b10bbe69a0a10..35e3791507023 100644
--- a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
index fcfeec097d124..4f4ee95bd0296 100644
--- a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
index 69481e904fea4..90bb28c341ef6 100644
--- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
index e7e179e4c2cc9..c6fa66037a9ac 100644
--- a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
index d4fa229b7e67d..9e117262b3f91 100644
--- a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
@@ -19,6 +19,7 @@ audiobusio = false
 audiocore = false
 audiodelays = false
 audiofilters = false
+audiofreeverb = false
 audioio = false
 audiomixer = false
 audiomp3 = false
diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk
index 03431f392f1b0..11999ac515533 100644
--- a/py/circuitpy_defns.mk
+++ b/py/circuitpy_defns.mk
@@ -137,6 +137,9 @@ endif
 ifeq ($(CIRCUITPY_AUDIOFILTERS),1)
 SRC_PATTERNS += audiofilters/%
 endif
+ifeq ($(CIRCUITPY_AUDIOFREEVERB),1)
+SRC_PATTERNS += audiofreeverb/%
+endif
 ifeq ($(CIRCUITPY_AUDIOMIXER),1)
 SRC_PATTERNS += audiomixer/%
 endif
@@ -671,6 +674,8 @@ SRC_SHARED_MODULE_ALL = \
 	audiofilters/Distortion.c \
 	audiofilters/Filter.c \
 	audiofilters/__init__.c \
+	audiofreeverb/__init__.c \
+	audiofreeverb/Freeverb.c \
 	audioio/__init__.c \
 	audiomixer/Mixer.c \
 	audiomixer/MixerVoice.c \
diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk
index c277c04c9d4d5..32ed205ec211b 100644
--- a/py/circuitpy_mpconfig.mk
+++ b/py/circuitpy_mpconfig.mk
@@ -146,6 +146,8 @@ CIRCUITPY_AUDIODELAYS ?= $(CIRCUITPY_AUDIOEFFECTS)
 CFLAGS += -DCIRCUITPY_AUDIODELAYS=$(CIRCUITPY_AUDIODELAYS)
 CIRCUITPY_AUDIOFILTERS ?= $(CIRCUITPY_AUDIOEFFECTS)
 CFLAGS += -DCIRCUITPY_AUDIOFILTERS=$(CIRCUITPY_AUDIOFILTERS)
+CIRCUITPY_AUDIOFREEVERB ?= $(CIRCUITPY_AUDIOEFFECTS)
+CFLAGS += -DCIRCUITPY_AUDIOFREEVERB=$(CIRCUITPY_AUDIOFREEVERB)
 
 CIRCUITPY_AURORA_EPAPER ?= 0
 CFLAGS += -DCIRCUITPY_AURORA_EPAPER=$(CIRCUITPY_AURORA_EPAPER)
diff --git a/shared-bindings/audiofreeverb/Freeverb.c b/shared-bindings/audiofreeverb/Freeverb.c
new file mode 100644
index 0000000000000..62c9237a0d271
--- /dev/null
+++ b/shared-bindings/audiofreeverb/Freeverb.c
@@ -0,0 +1,268 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#include <stdint.h>
+
+#include "shared-bindings/audiofreeverb/Freeverb.h"
+#include "shared-bindings/audiocore/__init__.h"
+#include "shared-module/audiofreeverb/Freeverb.h"
+
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-module/synthio/block.h"
+
+//| class Freeverb:
+//|     """An Freeverb effect"""
+//|
+//|     def __init__(
+//|         self,
+//|         roomsize: synthio.BlockInput = 0.5,
+//|         damp: synthio.BlockInput = 0.5,
+//|         mix: synthio.BlockInput = 0.5,
+//|         buffer_size: int = 512,
+//|         sample_rate: int = 8000,
+//|         bits_per_sample: int = 16,
+//|         samples_signed: bool = True,
+//|         channel_count: int = 1,
+//|     ) -> None:
+//|         """Create a Reverb effect simulating the audio taking place in a large room where you get echos
+//|            off of various surfaces at various times. The size of the room can be adjusted as well as how
+//|            much the higher frequencies get absorbed by the walls.
+//|
+//|            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 roomsize: The size of the room. 0.0 = smallest; 1.0 = largest.
+//|         :param synthio.BlockInput damp: How much the walls absorb. 0.0 = least; 1.0 = most.
+//|         :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. Freeverb requires 16 bits.
+//|         :param bool samples_signed: Effect is signed (True) or unsigned (False). Freeverb requires signed (True).
+//|
+//|         Playing adding reverb to a synth::
+//|
+//|           import time
+//|           import board
+//|           import audiobusio
+//|           import synthio
+//|           import audiofreeverb
+//|
+//|           audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22)
+//|           synth = synthio.Synthesizer(channel_count=1, sample_rate=44100)
+//|           reverb = audiofreeverb.Freeverb(roomsize=0.7, damp=0.3, buffer_size=1024, channel_count=1, sample_rate=44100, mix=0.7)
+//|           reverb.play(synth)
+//|           audio.play(reverb)
+//|
+//|           note = synthio.Note(261)
+//|           while True:
+//|               synth.press(note)
+//|               time.sleep(0.55)
+//|               synth.release(note)
+//|               time.sleep(5)"""
+//|         ...
+//|
+static mp_obj_t audiofreeverb_freeverb_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_roomsize, ARG_damp, 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_roomsize, MP_ARG_OBJ | MP_ARG_KW_ONLY,  {.u_obj = MP_OBJ_NULL} },
+        { MP_QSTR_damp, 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_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);
+    if (args[ARG_samples_signed].u_bool != true) {
+        mp_raise_ValueError(MP_ERROR_TEXT("samples_signed must be true"));
+    }
+    mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int;
+    if (bits_per_sample != 16) {
+        mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 16"));
+    }
+
+    audiofreeverb_freeverb_obj_t *self = mp_obj_malloc(audiofreeverb_freeverb_obj_t, &audiofreeverb_freeverb_type);
+    common_hal_audiofreeverb_freeverb_construct(self, args[ARG_roomsize].u_obj, args[ARG_damp].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 Freeverb."""
+//|         ...
+//|
+static mp_obj_t audiofreeverb_freeverb_deinit(mp_obj_t self_in) {
+    audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiofreeverb_freeverb_deinit(self);
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_deinit_obj, audiofreeverb_freeverb_deinit);
+
+static void check_for_deinit(audiofreeverb_freeverb_obj_t *self) {
+    audiosample_check_for_deinit(&self->base);
+}
+
+//|     def __enter__(self) -> Freeverb:
+//|         """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."""
+//|         ...
+//|
+//  Provided by context manager helper.
+
+//|     roomsize: synthio.BlockInput
+//|     """Apparent size of the room 0.0-1.0"""
+static mp_obj_t audiofreeverb_freeverb_obj_get_roomsize(mp_obj_t self_in) {
+    return common_hal_audiofreeverb_freeverb_get_roomsize(self_in);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_get_roomsize_obj, audiofreeverb_freeverb_obj_get_roomsize);
+
+static mp_obj_t audiofreeverb_freeverb_obj_set_roomsize(mp_obj_t self_in, mp_obj_t roomsize) {
+    audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiofreeverb_freeverb_set_roomsize(self, roomsize);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(audiofreeverb_freeverb_set_roomsize_obj, audiofreeverb_freeverb_obj_set_roomsize);
+
+MP_PROPERTY_GETSET(audiofreeverb_freeverb_roomsize_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_get_roomsize_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_set_roomsize_obj);
+
+//|     damp: synthio.BlockInput
+//|     """How much the high frequencies are dampened in the area. 0.0-1.0"""
+static mp_obj_t audiofreeverb_freeverb_obj_get_damp(mp_obj_t self_in) {
+    return common_hal_audiofreeverb_freeverb_get_damp(self_in);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_get_damp_obj, audiofreeverb_freeverb_obj_get_damp);
+
+static mp_obj_t audiofreeverb_freeverb_obj_set_damp(mp_obj_t self_in, mp_obj_t damp) {
+    audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiofreeverb_freeverb_set_damp(self, damp);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(audiofreeverb_freeverb_set_damp_obj, audiofreeverb_freeverb_obj_set_damp);
+
+MP_PROPERTY_GETSET(audiofreeverb_freeverb_damp_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_get_damp_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_set_damp_obj);
+
+//|     mix: synthio.BlockInput
+//|     """The rate the reverb mix between 0 and 1 where 0 is only sample and 1 is all effect."""
+static mp_obj_t audiofreeverb_freeverb_obj_get_mix(mp_obj_t self_in) {
+    return common_hal_audiofreeverb_freeverb_get_mix(self_in);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_get_mix_obj, audiofreeverb_freeverb_obj_get_mix);
+
+static mp_obj_t audiofreeverb_freeverb_obj_set_mix(mp_obj_t self_in, mp_obj_t mix_in) {
+    audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiofreeverb_freeverb_set_mix(self, mix_in);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(audiofreeverb_freeverb_set_mix_obj, audiofreeverb_freeverb_obj_set_mix);
+
+MP_PROPERTY_GETSET(audiofreeverb_freeverb_mix_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_get_mix_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_set_mix_obj);
+
+//|     playing: bool
+//|     """True when the effect is playing a sample. (read-only)"""
+//|
+static mp_obj_t audiofreeverb_freeverb_obj_get_playing(mp_obj_t self_in) {
+    audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    check_for_deinit(self);
+    return mp_obj_new_bool(common_hal_audiofreeverb_freeverb_get_playing(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_get_playing_obj, audiofreeverb_freeverb_obj_get_playing);
+
+MP_PROPERTY_GETTER(audiofreeverb_freeverb_playing_obj,
+    (mp_obj_t)&audiofreeverb_freeverb_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 audiofreeverb_freeverb_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} },
+    };
+    audiofreeverb_freeverb_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_audiofreeverb_freeverb_play(self, sample, args[ARG_loop].u_bool);
+
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_KW(audiofreeverb_freeverb_play_obj, 1, audiofreeverb_freeverb_obj_play);
+
+//|     def stop(self) -> None:
+//|         """Stops playback of the sample. The reverb continues playing."""
+//|         ...
+//|
+//|
+static mp_obj_t audiofreeverb_freeverb_obj_stop(mp_obj_t self_in) {
+    audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+
+    common_hal_audiofreeverb_freeverb_stop(self);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_stop_obj, audiofreeverb_freeverb_obj_stop);
+
+static const mp_rom_map_elem_t audiofreeverb_freeverb_locals_dict_table[] = {
+    // Methods
+    { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiofreeverb_freeverb_deinit_obj) },
+    { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
+    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
+    { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiofreeverb_freeverb_play_obj) },
+    { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiofreeverb_freeverb_stop_obj) },
+
+    // Properties
+    { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofreeverb_freeverb_playing_obj) },
+    { MP_ROM_QSTR(MP_QSTR_roomsize), MP_ROM_PTR(&audiofreeverb_freeverb_roomsize_obj) },
+    { MP_ROM_QSTR(MP_QSTR_damp), MP_ROM_PTR(&audiofreeverb_freeverb_damp_obj) },
+    { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofreeverb_freeverb_mix_obj) },
+    AUDIOSAMPLE_FIELDS,
+};
+static MP_DEFINE_CONST_DICT(audiofreeverb_freeverb_locals_dict, audiofreeverb_freeverb_locals_dict_table);
+
+static const audiosample_p_t audiofreeverb_freeverb_proto = {
+    MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample)
+    .reset_buffer = (audiosample_reset_buffer_fun)audiofreeverb_freeverb_reset_buffer,
+    .get_buffer = (audiosample_get_buffer_fun)audiofreeverb_freeverb_get_buffer,
+};
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    audiofreeverb_freeverb_type,
+    MP_QSTR_freeverb,
+    MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
+    make_new, audiofreeverb_freeverb_make_new,
+    locals_dict, &audiofreeverb_freeverb_locals_dict,
+    protocol, &audiofreeverb_freeverb_proto
+    );
diff --git a/shared-bindings/audiofreeverb/Freeverb.h b/shared-bindings/audiofreeverb/Freeverb.h
new file mode 100644
index 0000000000000..913953ebecf62
--- /dev/null
+++ b/shared-bindings/audiofreeverb/Freeverb.h
@@ -0,0 +1,36 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "shared-module/audiofreeverb/Freeverb.h"
+
+extern const mp_obj_type_t audiofreeverb_freeverb_type;
+
+void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self,
+    mp_obj_t roomsize, mp_obj_t damp, 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_audiofreeverb_freeverb_deinit(audiofreeverb_freeverb_obj_t *self);
+bool common_hal_audiofreeverb_freeverb_deinited(audiofreeverb_freeverb_obj_t *self);
+
+uint32_t common_hal_audiofreeverb_freeverb_get_sample_rate(audiofreeverb_freeverb_obj_t *self);
+uint8_t common_hal_audiofreeverb_freeverb_get_channel_count(audiofreeverb_freeverb_obj_t *self);
+uint8_t common_hal_audiofreeverb_freeverb_get_bits_per_sample(audiofreeverb_freeverb_obj_t *self);
+
+mp_obj_t common_hal_audiofreeverb_freeverb_get_roomsize(audiofreeverb_freeverb_obj_t *self);
+void common_hal_audiofreeverb_freeverb_set_roomsize(audiofreeverb_freeverb_obj_t *self, mp_obj_t feedback);
+
+mp_obj_t common_hal_audiofreeverb_freeverb_get_damp(audiofreeverb_freeverb_obj_t *self);
+void common_hal_audiofreeverb_freeverb_set_damp(audiofreeverb_freeverb_obj_t *self, mp_obj_t damp);
+
+mp_obj_t common_hal_audiofreeverb_freeverb_get_mix(audiofreeverb_freeverb_obj_t *self);
+void common_hal_audiofreeverb_freeverb_set_mix(audiofreeverb_freeverb_obj_t *self, mp_obj_t mix);
+
+bool common_hal_audiofreeverb_freeverb_get_playing(audiofreeverb_freeverb_obj_t *self);
+void common_hal_audiofreeverb_freeverb_play(audiofreeverb_freeverb_obj_t *self, mp_obj_t sample, bool loop);
+void common_hal_audiofreeverb_freeverb_stop(audiofreeverb_freeverb_obj_t *self);
diff --git a/shared-bindings/audiofreeverb/__init__.c b/shared-bindings/audiofreeverb/__init__.c
new file mode 100644
index 0000000000000..cb8c979c8cfec
--- /dev/null
+++ b/shared-bindings/audiofreeverb/__init__.c
@@ -0,0 +1,34 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#include <stdint.h>
+
+#include "py/obj.h"
+#include "py/runtime.h"
+
+#include "shared-bindings/audiofreeverb/__init__.h"
+#include "shared-bindings/audiofreeverb/Freeverb.h"
+
+
+//| """Support for audio freeverb effect
+//|
+//| The `audiofreeverb` module contains classes to provide access to audio freeverb effects.
+//|
+//| """
+
+static const mp_rom_map_elem_t audiofreeverb_module_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiofreeverb) },
+    { MP_ROM_QSTR(MP_QSTR_Freeverb), MP_ROM_PTR(&audiofreeverb_freeverb_type) },
+};
+
+static MP_DEFINE_CONST_DICT(audiofreeverb_module_globals, audiofreeverb_module_globals_table);
+
+const mp_obj_module_t audiofreeverb_module = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&audiofreeverb_module_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_audiofreeverb, audiofreeverb_module);
diff --git a/shared-bindings/audiofreeverb/__init__.h b/shared-bindings/audiofreeverb/__init__.h
new file mode 100644
index 0000000000000..66463561f5443
--- /dev/null
+++ b/shared-bindings/audiofreeverb/__init__.h
@@ -0,0 +1,7 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
diff --git a/shared-module/audiofreeverb/Freeverb.c b/shared-module/audiofreeverb/Freeverb.c
new file mode 100644
index 0000000000000..d5c4671595e1c
--- /dev/null
+++ b/shared-module/audiofreeverb/Freeverb.c
@@ -0,0 +1,358 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+//
+// Based on FreeVerb - https://github.com/sinshu/freeverb/tree/main
+// Fixed point ideas from - Paul Stoffregen in the Teensy audio library https://github.com/PaulStoffregen/Audio/blob/master/effect_freeverb.cpp
+//
+#include "shared-bindings/audiofreeverb/Freeverb.h"
+
+#include <stdint.h>
+#include "py/runtime.h"
+#include <math.h>
+
+void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self, mp_obj_t roomsize, mp_obj_t damp, 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->base.bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places
+    self->base.samples_signed = samples_signed; // Are the samples we provide signed (common is true)
+    self->base.channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo
+    self->base.sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects
+    self->base.single_buffer = false;
+    self->base.max_buffer_length = buffer_size;
+
+    // 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_maybe(self->buffer_len);
+    if (self->buffer[0] == NULL) {
+        common_hal_audiofreeverb_freeverb_deinit(self);
+        m_malloc_fail(self->buffer_len);
+    }
+    memset(self->buffer[0], 0, self->buffer_len);
+
+    self->buffer[1] = m_malloc_maybe(self->buffer_len);
+    if (self->buffer[1] == NULL) {
+        common_hal_audiofreeverb_freeverb_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 reverb effect's starting values. For a different effect this section will change
+    if (roomsize == MP_OBJ_NULL) {
+        roomsize = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
+    }
+    synthio_block_assign_slot(roomsize, &self->roomsize, MP_QSTR_roomsize);
+    common_hal_audiofreeverb_freeverb_set_roomsize(self, roomsize);
+
+    if (damp == MP_OBJ_NULL) {
+        damp = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
+    }
+    synthio_block_assign_slot(damp, &self->damp, MP_QSTR_damp);
+    common_hal_audiofreeverb_freeverb_set_damp(self, damp);
+
+    if (mix == MP_OBJ_NULL) {
+        mix = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
+    }
+    synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix);
+    common_hal_audiofreeverb_freeverb_set_mix(self, mix);
+
+    // Set up the comb filters
+    // These values come from FreeVerb and are selected for the best reverb sound
+    self->combbuffersizes[0] = self->combbuffersizes[8] = 1116;
+    self->combbuffersizes[1] = self->combbuffersizes[9] = 1188;
+    self->combbuffersizes[2] = self->combbuffersizes[10] = 1277;
+    self->combbuffersizes[3] = self->combbuffersizes[11] = 1356;
+    self->combbuffersizes[4] = self->combbuffersizes[12] = 1422;
+    self->combbuffersizes[5] = self->combbuffersizes[13] = 1491;
+    self->combbuffersizes[6] = self->combbuffersizes[14] = 1557;
+    self->combbuffersizes[7] = self->combbuffersizes[15] = 1617;
+    for (uint32_t i = 0; i < 8 * channel_count; i++) {
+        self->combbuffers[i] = m_malloc_maybe(self->combbuffersizes[i] * sizeof(uint16_t));
+        if (self->combbuffers[i] == NULL) {
+            common_hal_audiofreeverb_freeverb_deinit(self);
+            m_malloc_fail(self->combbuffersizes[i]);
+        }
+        memset(self->combbuffers[i], 0, self->combbuffersizes[i]);
+
+        self->combbufferindex[i] = 0;
+        self->combfitlers[i] = 0;
+    }
+
+    // Set up the allpass filters
+    // These values come from FreeVerb and are selected for the best reverb sound
+    self->allpassbuffersizes[0] = self->allpassbuffersizes[4] = 556;
+    self->allpassbuffersizes[1] = self->allpassbuffersizes[5] = 441;
+    self->allpassbuffersizes[2] = self->allpassbuffersizes[6] = 341;
+    self->allpassbuffersizes[3] = self->allpassbuffersizes[7] = 225;
+    for (uint32_t i = 0; i < 4 * channel_count; i++) {
+        self->allpassbuffers[i] = m_malloc_maybe(self->allpassbuffersizes[i] * sizeof(uint16_t));
+        if (self->allpassbuffers[i] == NULL) {
+            common_hal_audiofreeverb_freeverb_deinit(self);
+            m_malloc_fail(self->allpassbuffersizes[i]);
+        }
+        memset(self->allpassbuffers[i], 0, self->allpassbuffersizes[i]);
+
+        self->allpassbufferindex[i] = 0;
+    }
+}
+
+bool common_hal_audiofreeverb_freeverb_deinited(audiofreeverb_freeverb_obj_t *self) {
+    if (self->buffer[0] == NULL) {
+        return true;
+    }
+    return false;
+}
+
+void common_hal_audiofreeverb_freeverb_deinit(audiofreeverb_freeverb_obj_t *self) {
+    if (common_hal_audiofreeverb_freeverb_deinited(self)) {
+        return;
+    }
+    self->buffer[0] = NULL;
+    self->buffer[1] = NULL;
+}
+
+mp_obj_t common_hal_audiofreeverb_freeverb_get_roomsize(audiofreeverb_freeverb_obj_t *self) {
+    return self->roomsize.obj;
+}
+
+void common_hal_audiofreeverb_freeverb_set_roomsize(audiofreeverb_freeverb_obj_t *self, mp_obj_t roomsize_obj) {
+    synthio_block_assign_slot(roomsize_obj, &self->roomsize, MP_QSTR_roomsize);
+}
+
+int16_t audiofreeverb_freeverb_get_roomsize_fixedpoint(mp_float_t n) {
+    if (n > (mp_float_t)MICROPY_FLOAT_CONST(1.0)) {
+        n = MICROPY_FLOAT_CONST(1.0);
+    } else if (n < (mp_float_t)MICROPY_FLOAT_CONST(0.0)) {
+        n = MICROPY_FLOAT_CONST(0.0);
+    }
+
+    return (int16_t)(n * (mp_float_t)MICROPY_FLOAT_CONST(9175.04)) + 22937; // 9175.04 = 0.28f in fixed point 22937 = 0.7f
+}
+
+mp_obj_t common_hal_audiofreeverb_freeverb_get_damp(audiofreeverb_freeverb_obj_t *self) {
+    return self->damp.obj;
+}
+
+void common_hal_audiofreeverb_freeverb_set_damp(audiofreeverb_freeverb_obj_t *self, mp_obj_t damp) {
+    synthio_block_assign_slot(damp, &self->damp, MP_QSTR_damp);
+}
+
+void audiofreeverb_freeverb_get_damp_fixedpoint(mp_float_t n, int16_t *damp1, int16_t *damp2) {
+    if (n > (mp_float_t)MICROPY_FLOAT_CONST(1.0)) {
+        n = MICROPY_FLOAT_CONST(1.0);
+    } else if (n < (mp_float_t)MICROPY_FLOAT_CONST(0.0)) {
+        n = MICROPY_FLOAT_CONST(0.0);
+    }
+
+    *damp1 = (int16_t)(n * (mp_float_t)MICROPY_FLOAT_CONST(13107.2)); // 13107.2 = 0.4f scaling factor
+    *damp2 = (int16_t)(32768 - *damp1); // inverse of x1 damp2 = 1.0 - damp1
+}
+
+mp_obj_t common_hal_audiofreeverb_freeverb_get_mix(audiofreeverb_freeverb_obj_t *self) {
+    return self->mix.obj;
+}
+
+void common_hal_audiofreeverb_freeverb_set_mix(audiofreeverb_freeverb_obj_t *self, mp_obj_t mix) {
+    synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix);
+}
+
+void audiofreeverb_freeverb_get_mix_fixedpoint(mp_float_t mix, int16_t *mix_sample, int16_t *mix_effect) {
+    mix = mix * (mp_float_t)MICROPY_FLOAT_CONST(2.0);
+    *mix_sample = (int16_t)(MIN((mp_float_t)MICROPY_FLOAT_CONST(2.0) - mix, (mp_float_t)MICROPY_FLOAT_CONST(1.0)) * 32767);
+    *mix_effect = (int16_t)(MIN(mix, (mp_float_t)MICROPY_FLOAT_CONST(1.0)) * 32767);
+}
+
+void audiofreeverb_freeverb_reset_buffer(audiofreeverb_freeverb_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_audiofreeverb_freeverb_get_playing(audiofreeverb_freeverb_obj_t *self) {
+    return self->sample != NULL;
+}
+
+void common_hal_audiofreeverb_freeverb_play(audiofreeverb_freeverb_obj_t *self, mp_obj_t sample, bool loop) {
+    audiosample_must_match(&self->base, sample);
+
+    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->base.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_audiofreeverb_freeverb_stop(audiofreeverb_freeverb_obj_t *self) {
+    // When the sample is set to stop playing do any cleanup here
+    // For reverb we clear the sample but the reverb continues until the object reading our effect stops
+    self->sample = NULL;
+    return;
+}
+
+// cleaner sat16 by http://www.moseleyinstruments.com/
+static int16_t sat16(int32_t n, int rshift) {
+    // we should always round towards 0
+    // to avoid recirculating round-off noise
+    //
+    // a 2s complement positive number is always
+    // rounded down, so we only need to take
+    // care of negative numbers
+    if (n < 0) {
+        n = n + (~(0xFFFFFFFFUL << rshift));
+    }
+    n = n >> rshift;
+    if (n > 32767) {
+        return 32767;
+    }
+    if (n < -32768) {
+        return -32768;
+    }
+    return n;
+}
+
+audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_freeverb_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;
+
+    // 16 bit samples we need a 16 bit pointer
+    int16_t *word_buffer = (int16_t *)self->buffer[self->last_buf_idx];
+    uint32_t length = self->buffer_len / (self->base.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 but we still need to play the reverb
+                    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->base.bits_per_sample / 8);
+                self->more_data = result == GET_BUFFER_MORE_DATA;
+            }
+        }
+
+        // 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->base.channel_count);
+        } else {
+            n = MIN(MIN(self->sample_buffer_length, length), SYNTHIO_MAX_DUR * self->base.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->base.sample_rate, n / self->base.channel_count);
+        mp_float_t damp = synthio_block_slot_get_limited(&self->damp, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
+        int16_t damp1, damp2;
+        audiofreeverb_freeverb_get_damp_fixedpoint(damp, &damp1, &damp2);
+
+        mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
+        int16_t mix_sample, mix_effect;
+        audiofreeverb_freeverb_get_mix_fixedpoint(mix, &mix_sample, &mix_effect);
+
+        mp_float_t roomsize = synthio_block_slot_get_limited(&self->roomsize, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
+        int16_t feedback = audiofreeverb_freeverb_get_roomsize_fixedpoint(roomsize);
+
+        int16_t *sample_src = (int16_t *)self->sample_remaining_buffer;
+
+        for (uint32_t i = 0; i < n; i++) {
+            int32_t sample_word = 0;
+            if (self->sample != NULL) {
+                sample_word = sample_src[i];
+            }
+
+            int32_t word, sum;
+            int16_t input, bufout, output;
+            uint32_t channel_comb_offset = 0, channel_allpass_offset = 0;
+
+            input = sat16(sample_word * 8738, 17); // Initial input scaled down so we can add reverb
+            sum = 0;
+
+            // Calculate each of the 8 comb buffers
+            for (uint32_t j = 0 + channel_comb_offset; j < 8 + channel_comb_offset; j++) {
+                bufout = self->combbuffers[j][self->combbufferindex[j]];
+                sum += bufout;
+                self->combfitlers[j] = sat16(bufout * damp2 + self->combfitlers[j] * damp1, 15);
+                self->combbuffers[j][self->combbufferindex[j]] = sat16(input + sat16(self->combfitlers[j] * feedback, 15), 0);
+                if (++self->combbufferindex[j] >= self->combbuffersizes[j]) {
+                    self->combbufferindex[j] = 0;
+                }
+            }
+
+            output = sat16(sum * 31457, 17); // 31457 = 0.24f with shift of 17
+
+            // Calculate each of the 4 all pass buffers
+            for (uint32_t j = 0 + channel_allpass_offset; j < 4 + channel_allpass_offset; j++) {
+                bufout = self->allpassbuffers[j][self->allpassbufferindex[j]];
+                self->allpassbuffers[j][self->allpassbufferindex[j]] = output + (bufout >> 1); // bufout >> 1 same as bufout*0.5f
+                output = sat16(bufout - output, 1);
+                if (++self->allpassbufferindex[j] >= self->allpassbuffersizes[j]) {
+                    self->allpassbufferindex[j] = 0;
+                }
+            }
+
+            word = output * 30; // Add some volume back don't have to saturate as next step will
+
+            word = sat16(sample_word * mix_sample, 15) + sat16(word * mix_effect, 15);
+            word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2));
+            word_buffer[i] = (int16_t)word;
+
+            if ((self->base.channel_count == 2) && (channel_comb_offset == 0)) {
+                channel_comb_offset = 8;
+                channel_allpass_offset = 4;
+            } else {
+                channel_comb_offset = 0;
+                channel_allpass_offset = 0;
+            }
+        }
+
+        // Update the remaining length and the buffer positions based on how much we wrote into our buffer
+        length -= n;
+        word_buffer += n;
+        self->sample_remaining_buffer += (n * (self->base.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;
+
+    // Reverb 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;
+}
diff --git a/shared-module/audiofreeverb/Freeverb.h b/shared-module/audiofreeverb/Freeverb.h
new file mode 100644
index 0000000000000..44747f0fc951d
--- /dev/null
+++ b/shared-module/audiofreeverb/Freeverb.h
@@ -0,0 +1,56 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+#pragma once
+
+#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 audiofreeverb_freeverb_type;
+
+typedef struct {
+    audiosample_base_t base;
+    synthio_block_slot_t roomsize;
+    synthio_block_slot_t damp;
+    synthio_block_slot_t mix;
+
+    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;
+
+    int16_t combbuffersizes[16];
+    int16_t *combbuffers[16];
+    int16_t combbufferindex[16];
+    int16_t combfitlers[16];
+
+    int16_t allpassbuffersizes[8];
+    int16_t *allpassbuffers[8];
+    int16_t allpassbufferindex[8];
+
+    mp_obj_t sample;
+} audiofreeverb_freeverb_obj_t;
+
+void audiofreeverb_freeverb_reset_buffer(audiofreeverb_freeverb_obj_t *self,
+    bool single_channel_output,
+    uint8_t channel);
+
+audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_freeverb_obj_t *self,
+    bool single_channel_output,
+    uint8_t channel,
+    uint8_t **buffer,
+    uint32_t *buffer_length);  // length in bytes
+
+int16_t audiofreeverb_freeverb_get_roomsize_fixedpoint(mp_float_t n);
+void audiofreeverb_freeverb_get_damp_fixedpoint(mp_float_t n, int16_t *damp1, int16_t *damp2);
+void audiofreeverb_freeverb_get_mix_fixedpoint(mp_float_t mix, int16_t *mix_sample, int16_t *mix_effect);
diff --git a/shared-module/audiofreeverb/__init__.c b/shared-module/audiofreeverb/__init__.c
new file mode 100644
index 0000000000000..94cd4caa3bd1b
--- /dev/null
+++ b/shared-module/audiofreeverb/__init__.c
@@ -0,0 +1,5 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
diff --git a/shared-module/audiofreeverb/__init__.h b/shared-module/audiofreeverb/__init__.h
new file mode 100644
index 0000000000000..66463561f5443
--- /dev/null
+++ b/shared-module/audiofreeverb/__init__.h
@@ -0,0 +1,7 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once


Reviews:
user: login: relic-se
body: Great work so far! I haven't dug into everything quite yet, but the biggest issue at the moment is definitely the mix implementation. <0.5 doesn't seem to work for me.

All my testing was on an RP2040 with mono 22050hz sample audio (StreetChicken.wav).
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-07T20:49:57Z
user: login: gamblor21
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:59:17Z
user: login: tannewt
body: Just a couple small things and a CI to fix. Thanks for the new module!
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-15T16:50:13Z
user: login: gamblor21
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-20T15:59:06Z
user: login: tannewt
body: Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T18:19:42Z

Review comments:
diff_hunk: @@ -0,0 +1,358 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+//
+// Based on FreeVerb - https://github.com/sinshu/freeverb/tree/main
+// Fixed point ideas from - Paul Stoffregen in the Teensy audio library https://github.com/PaulStoffregen/Audio/blob/master/effect_freeverb.cpp
+//
+#include "shared-bindings/audiodelays/Reverb.h"
+
+#include <stdint.h>
+#include "py/runtime.h"
+#include <math.h>
+
+void common_hal_audiodelays_reverb_construct(audiodelays_reverb_obj_t *self, mp_obj_t roomsize, mp_obj_t damp, 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->base.bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places
+    self->base.samples_signed = samples_signed; // Are the samples we provide signed (common is true)
+    self->base.channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo
+    self->base.sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects
+    self->base.single_buffer = false;
+    self->base.max_buffer_length = buffer_size;
+
+    // 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_audiodelays_reverb_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_audiodelays_reverb_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 reverb effect's starting values. For a different effect this section will change
+    if (roomsize == MP_OBJ_NULL) {
+        roomsize = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
+    }
+    synthio_block_assign_slot(roomsize, &self->roomsize, MP_QSTR_roomsize);
+    common_hal_audiodelays_reverb_set_roomsize(self, roomsize);
+
+    if (damp == MP_OBJ_NULL) {
+        damp = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
+    }
+    synthio_block_assign_slot(damp, &self->damp, MP_QSTR_damp);
+    common_hal_audiodelays_reverb_set_damp(self, damp);
+
+    if (mix == MP_OBJ_NULL) {
+        mix = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5));
+    }
+    synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix);
+    common_hal_audiodelays_reverb_set_mix(self, mix);
+
+    // Set up the comb filters
+    // These values come from FreeVerb and are selected for the best reverb sound
+    self->combbuffersizes[0] = self->combbuffersizes[8] = 1116;
+    self->combbuffersizes[1] = self->combbuffersizes[9] = 1188;
+    self->combbuffersizes[2] = self->combbuffersizes[10] = 1277;
+    self->combbuffersizes[3] = self->combbuffersizes[11] = 1356;
+    self->combbuffersizes[4] = self->combbuffersizes[12] = 1422;
+    self->combbuffersizes[5] = self->combbuffersizes[13] = 1491;
+    self->combbuffersizes[6] = self->combbuffersizes[14] = 1557;
+    self->combbuffersizes[7] = self->combbuffersizes[15] = 1617;
+    for (uint32_t i = 0; i < 8 * channel_count; i++) {
+        self->combbuffers[i] = m_malloc(self->combbuffersizes[i] * sizeof(uint16_t));
+        if (self->combbuffers[i] == NULL) {
+            common_hal_audiodelays_reverb_deinit(self);
+            m_malloc_fail(self->combbuffersizes[i]);
+        }
+        memset(self->combbuffers[i], 0, self->combbuffersizes[i]);
+
+        self->combbufferindex[i] = 0;
+        self->combfitlers[i] = 0;
+    }
+
+    // Set up the allpass filters
+    // These values come from FreeVerb and are selected for the best reverb sound
+    self->allpassbuffersizes[0] = self->allpassbuffersizes[4] = 556;
+    self->allpassbuffersizes[1] = self->allpassbuffersizes[5] = 441;
+    self->allpassbuffersizes[2] = self->allpassbuffersizes[6] = 341;
+    self->allpassbuffersizes[3] = self->allpassbuffersizes[7] = 225;
+    for (uint32_t i = 0; i < 4 * channel_count; i++) {
+        self->allpassbuffers[i] = m_malloc(self->allpassbuffersizes[i] * sizeof(uint16_t));
+        if (self->allpassbuffers[i] == NULL) {
+            common_hal_audiodelays_reverb_deinit(self);
+            m_malloc_fail(self->allpassbuffersizes[i]);
+        }
+        memset(self->allpassbuffers[i], 0, self->allpassbuffersizes[i]);
+
+        self->allpassbufferindex[i] = 0;
+    }
+}
+
+bool common_hal_audiodelays_reverb_deinited(audiodelays_reverb_obj_t *self) {
+    if (self->buffer[0] == NULL) {
+        return true;
+    }
+    return false;
+}
+
+void common_hal_audiodelays_reverb_deinit(audiodelays_reverb_obj_t *self) {
+    if (common_hal_audiodelays_reverb_deinited(self)) {
+        return;
+    }
+    self->buffer[0] = NULL;
+    self->buffer[1] = NULL;
+}
+
+mp_obj_t common_hal_audiodelays_reverb_get_roomsize(audiodelays_reverb_obj_t *self) {
+    return self->roomsize.obj;
+}
+
+void common_hal_audiodelays_reverb_set_roomsize(audiodelays_reverb_obj_t *self, mp_obj_t roomsize_obj) {
+    synthio_block_assign_slot(roomsize_obj, &self->roomsize, MP_QSTR_roomsize);
+}
+
+int16_t audiodelays_reverb_get_roomsize_fixedpoint(mp_float_t n) {
+    if (n > (mp_float_t)MICROPY_FLOAT_CONST(1.0)) {
+        n = MICROPY_FLOAT_CONST(1.0);
+    } else if (n < (mp_float_t)MICROPY_FLOAT_CONST(0.0)) {
+        n = MICROPY_FLOAT_CONST(0.0);
+    }
+
+    return (int16_t)(n * (mp_float_t)MICROPY_FLOAT_CONST(9175.04)) + 22937; // 9175.04 = 0.28f in fixed point 22937 = 0.7f
+}
+
+mp_obj_t common_hal_audiodelays_reverb_get_damp(audiodelays_reverb_obj_t *self) {
+    return self->damp.obj;
+}
+
+void common_hal_audiodelays_reverb_set_damp(audiodelays_reverb_obj_t *self, mp_obj_t damp) {
+    synthio_block_assign_slot(damp, &self->damp, MP_QSTR_damp);
+}
+
+void audiodelays_reverb_get_damp_fixedpoint(mp_float_t n, int16_t *damp1, int16_t *damp2) {
+    if (n > (mp_float_t)MICROPY_FLOAT_CONST(1.0)) {
+        n = MICROPY_FLOAT_CONST(1.0);
+    } else if (n < (mp_float_t)MICROPY_FLOAT_CONST(0.0)) {
+        n = MICROPY_FLOAT_CONST(0.0);
+    }
+
+    *damp1 = (int16_t)(n * (mp_float_t)MICROPY_FLOAT_CONST(13107.2)); // 13107.2 = 0.4f scaling factor
+    *damp2 = (int16_t)(32768 - *damp1); // inverse of x1 damp2 = 1.0 - damp1
+}
+
+mp_obj_t common_hal_audiodelays_reverb_get_mix(audiodelays_reverb_obj_t *self) {
+    return self->mix.obj;
+}
+
+void common_hal_audiodelays_reverb_set_mix(audiodelays_reverb_obj_t *self, mp_obj_t mix) {
+    synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix);
+}
+
+void audiodelays_reverb_get_mix_fixedpoint(mp_float_t mix, int16_t *mix_sample, int16_t *mix_effect) {
+    mix = mix * (mp_float_t)MICROPY_FLOAT_CONST(2.0);
+    *mix_sample = (int16_t)MIN((mp_float_t)MICROPY_FLOAT_CONST(2.0) - mix, (mp_float_t)MICROPY_FLOAT_CONST(1.0)) * 32767;
+    *mix_effect = (int16_t)MIN(mix, (mp_float_t)MICROPY_FLOAT_CONST(1.0)) * 32767;
+}
+
+void audiodelays_reverb_reset_buffer(audiodelays_reverb_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_audiodelays_reverb_get_playing(audiodelays_reverb_obj_t *self) {
+    return self->sample != NULL;
+}
+
+void common_hal_audiodelays_reverb_play(audiodelays_reverb_obj_t *self, mp_obj_t sample, bool loop) {
+    audiosample_must_match(&self->base, sample);
+
+    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->base.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_audiodelays_reverb_stop(audiodelays_reverb_obj_t *self) {
+    // When the sample is set to stop playing do any cleanup here
+    // For reverb we clear the sample but the reverb continues until the object reading our effect stops
+    self->sample = NULL;
+    return;
+}
+
+// cleaner sat16 by http://www.moseleyinstruments.com/
+static int16_t sat16(int32_t n, int rshift) {
+    // we should always round towards 0
+    // to avoid recirculating round-off noise
+    //
+    // a 2s complement positive number is always
+    // rounded down, so we only need to take
+    // care of negative numbers
+    if (n < 0) {
+        n = n + (~(0xFFFFFFFFUL << rshift));
+    }
+    n = n >> rshift;
+    if (n > 32767) {
+        return 32767;
+    }
+    if (n < -32768) {
+        return -32768;
+    }
+    return n;
+}
+
+audioio_get_buffer_result_t audiodelays_reverb_get_buffer(audiodelays_reverb_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;
+
+    // 16 bit samples we need a 16 bit pointer
+    int16_t *word_buffer = (int16_t *)self->buffer[self->last_buf_idx];
+    uint32_t length = self->buffer_len / (self->base.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 but we still need to play the reverb
+                    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->base.bits_per_sample / 8);
+                self->more_data = result == GET_BUFFER_MORE_DATA;
+            }
+        }
+
+        // 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->base.channel_count);
+        } else {
+            n = MIN(MIN(self->sample_buffer_length, length), SYNTHIO_MAX_DUR * self->base.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->base.sample_rate, n / self->base.channel_count);
+        mp_float_t damp = synthio_block_slot_get_limited(&self->damp, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
+        int16_t damp1, damp2;
+        audiodelays_reverb_get_damp_fixedpoint(damp, &damp1, &damp2);
+
+        mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
+        int16_t mix_sample, mix_effect;
+        audiodelays_reverb_get_mix_fixedpoint(mix, &mix_sample, &mix_effect);
+
+        mp_float_t roomsize = synthio_block_slot_get_limited(&self->roomsize, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0));
+        int16_t feedback = audiodelays_reverb_get_roomsize_fixedpoint(roomsize);
+
+        int16_t *sample_src = (int16_t *)self->sample_remaining_buffer;
+
+        for (uint32_t i = 0; i < n; i++) {
+            int32_t sample_word = 0;
+            if (self->sample != NULL) {
+                sample_word = sample_src[i];
+            }
+
+            int32_t word, sum;
+            int16_t input, bufout, output;
+            uint32_t channel_comb_offset = 0, channel_allpass_offset = 0;
+
+            input = sat16(sample_word * 8738, 17); // Initial input scaled down so we can add reverb
+            sum = 0;
+
+            // Calculate each of the 8 comb buffers
+            for (uint32_t j = 0 + channel_comb_offset; j < 8 + channel_comb_offset; j++) {
+                bufout = self->combbuffers[j][self->combbufferindex[j]];
+                sum += bufout;
+                self->combfitlers[j] = sat16(bufout * damp2 + self->combfitlers[j] * damp1, 15);
+                self->combbuffers[j][self->combbufferindex[j]] = sat16(input + sat16(self->combfitlers[j] * feedback, 15), 0);
+                if (++self->combbufferindex[j] >= self->combbuffersizes[j]) {
+                    self->combbufferindex[j] = 0;
+                }
+            }
+
+            output = sat16(sum * 31457, 17); // 31457 = 0.24f with shift of 17
+
+            // Calculate each of the 4 all pass buffers
+            for (uint32_t j = 0 + channel_allpass_offset; j < 4 + channel_allpass_offset; j++) {
+                bufout = self->allpassbuffers[j][self->allpassbufferindex[j]];
+                self->allpassbuffers[j][self->allpassbufferindex[j]] = output + (bufout >> 1); // bufout >> 1 same as bufout*0.5f
+                output = sat16(bufout - output, 1);
+                if (++self->allpassbufferindex[j] >= self->allpassbuffersizes[j]) {
+                    self->allpassbufferindex[j] = 0;
+                }
+            }
+
+            word = output * 30; // Add some volume back don't have to saturate as next step will
+
+            word = sat16(sample_word * mix_sample, 15) + sat16(word * mix_effect, 15);
path: shared-module/audiodelays/Reverb.c
user: login: relic-se
body: I haven't quite figured out why, but when mix is <0.5 (ie: 0.499999), there is no effect at all in the output. When mix is 0.5+, then the effect is at full volume and the sample volume decreases as you approach 1.0. The implementation in `audiodelays_reverb_get_mix_fixedpoint` appears correct.
created_at: 2025-04-07T20:35:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,268 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#include <stdint.h>
+
+#include "shared-bindings/audiodelays/Reverb.h"
+#include "shared-bindings/audiocore/__init__.h"
+#include "shared-module/audiodelays/Reverb.h"
+
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-module/synthio/block.h"
+
+//| class Reverb:
+//|     """An Reverb effect"""
+//|
+//|     def __init__(
+//|         self,
+//|         roomsize: synthio.BlockInput = 0.5,
+//|         damp: synthio.BlockInput = 0.5,
+//|         mix: synthio.BlockInput = 0.5,
+//|         buffer_size: int = 512,
+//|         sample_rate: int = 8000,
+//|         bits_per_sample: int = 16,
+//|         samples_signed: bool = True,
+//|         channel_count: int = 1,
+//|     ) -> None:
+//|         """Create a Reverb effect simulating the audio taking place in a large room where you get echos
+//|            off of various surfaces at various times. The size of the room can be adjusted as well as how
+//|            much the higher frequencies get absorbed by the walls.
+//|
+//|            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 roomsize: The size of the room. 0.0 = smallest; 1.0 = largest.
+//|         :param synthio.BlockInput damp: How much the walls absorb. 0.0 = least; 1.0 = most.
+//|         :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. Reverb requires 16 bits.
+//|         :param bool samples_signed: Effect is signed (True) or unsigned (False). Reverb requires signed (True).
+//|
+//|         Playing adding an reverb to a synth::
+//|
+//|           import time
+//|           import board
+//|           import audiobusio
+//|           import synthio
+//|           import audiodelays
+//|
+//|           audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22)
+//|           synth = synthio.Synthesizer(channel_count=1, sample_rate=44100)
+//|           reverb = audiodelays.Reverb(roomsize=0.7, damp=0.3, buffer_size=1024, channel_count=1, sample_rate=44100, mix=0.7)
+//|           reverb.play(synth)
+//|           audio.play(reverb)
+//|
+//|           note = synthio.Note(261)
+//|           while True:
+//|               synth.press(note)
+//|               time.sleep(0.55)
+//|               synth.release(note)
+//|               time.sleep(5)"""
+//|         ...
+//|
+static mp_obj_t audiodelays_reverb_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_roomsize, ARG_damp, 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_roomsize, MP_ARG_OBJ | MP_ARG_KW_ONLY,  {.u_obj = MP_OBJ_NULL} },
+        { MP_QSTR_damp, 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_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);
+    if (args[ARG_samples_signed].u_bool != true) {
+        mp_raise_ValueError(MP_ERROR_TEXT("samples_signed must be true"));
+    }
+    mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int;
+    if (bits_per_sample != 16) {
+        mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 16"));
+    }
+
+    audiodelays_reverb_obj_t *self = mp_obj_malloc(audiodelays_reverb_obj_t, &audiodelays_reverb_type);
+    common_hal_audiodelays_reverb_construct(self, args[ARG_roomsize].u_obj, args[ARG_damp].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 Reverb."""
+//|         ...
+//|
+static mp_obj_t audiodelays_reverb_deinit(mp_obj_t self_in) {
+    audiodelays_reverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiodelays_reverb_deinit(self);
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_reverb_deinit_obj, audiodelays_reverb_deinit);
+
+static void check_for_deinit(audiodelays_reverb_obj_t *self) {
+    audiosample_check_for_deinit(&self->base);
+}
+
+//|     def __enter__(self) -> Reverb:
+//|         """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."""
+//|         ...
+//|
+//  Provided by context manager helper.
+
+//|     roomsize: synthio.BlockInput
+//|     """Apparent size of the room 0.0-1.0"""
+static mp_obj_t audiodelays_reverb_obj_get_roomsize(mp_obj_t self_in) {
+    return common_hal_audiodelays_reverb_get_roomsize(self_in);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_reverb_get_roomsize_obj, audiodelays_reverb_obj_get_roomsize);
+
+static mp_obj_t audiodelays_reverb_obj_set_roomsize(mp_obj_t self_in, mp_obj_t roomsize) {
+    audiodelays_reverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiodelays_reverb_set_roomsize(self, roomsize);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(audiodelays_reverb_set_roomsize_obj, audiodelays_reverb_obj_set_roomsize);
+
+MP_PROPERTY_GETSET(audiodelays_reverb_roomsize_obj,
+    (mp_obj_t)&audiodelays_reverb_get_roomsize_obj,
+    (mp_obj_t)&audiodelays_reverb_set_roomsize_obj);
+
+//|     damp: synthio.BlockInput
+//|     """How reverbrent the area is. 0.0-1.0"""
path: shared-bindings/audiodelays/Reverb.c
user: login: relic-se
body: Should be spelled "reverberant", but also I feel that this description isn't precise. A higher value "dampens" the high frequency response and reduces reverberation among those frequencies (lower frequencies aren't affected as much).
created_at: 2025-04-07T20:37:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,268 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+
+#include <stdint.h>
+
+#include "shared-bindings/audiodelays/Reverb.h"
+#include "shared-bindings/audiocore/__init__.h"
+#include "shared-module/audiodelays/Reverb.h"
+
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-module/synthio/block.h"
+
+//| class Reverb:
+//|     """An Reverb effect"""
+//|
+//|     def __init__(
+//|         self,
+//|         roomsize: synthio.BlockInput = 0.5,
+//|         damp: synthio.BlockInput = 0.5,
+//|         mix: synthio.BlockInput = 0.5,
+//|         buffer_size: int = 512,
+//|         sample_rate: int = 8000,
+//|         bits_per_sample: int = 16,
+//|         samples_signed: bool = True,
+//|         channel_count: int = 1,
+//|     ) -> None:
+//|         """Create a Reverb effect simulating the audio taking place in a large room where you get echos
+//|            off of various surfaces at various times. The size of the room can be adjusted as well as how
+//|            much the higher frequencies get absorbed by the walls.
+//|
+//|            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 roomsize: The size of the room. 0.0 = smallest; 1.0 = largest.
+//|         :param synthio.BlockInput damp: How much the walls absorb. 0.0 = least; 1.0 = most.
+//|         :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. Reverb requires 16 bits.
+//|         :param bool samples_signed: Effect is signed (True) or unsigned (False). Reverb requires signed (True).
+//|
+//|         Playing adding an reverb to a synth::
+//|
+//|           import time
+//|           import board
+//|           import audiobusio
+//|           import synthio
+//|           import audiodelays
+//|
+//|           audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22)
+//|           synth = synthio.Synthesizer(channel_count=1, sample_rate=44100)
+//|           reverb = audiodelays.Reverb(roomsize=0.7, damp=0.3, buffer_size=1024, channel_count=1, sample_rate=44100, mix=0.7)
+//|           reverb.play(synth)
+//|           audio.play(reverb)
+//|
+//|           note = synthio.Note(261)
+//|           while True:
+//|               synth.press(note)
+//|               time.sleep(0.55)
+//|               synth.release(note)
+//|               time.sleep(5)"""
+//|         ...
+//|
+static mp_obj_t audiodelays_reverb_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_roomsize, ARG_damp, 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_roomsize, MP_ARG_OBJ | MP_ARG_KW_ONLY,  {.u_obj = MP_OBJ_NULL} },
+        { MP_QSTR_damp, 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_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);
+    if (args[ARG_samples_signed].u_bool != true) {
+        mp_raise_ValueError(MP_ERROR_TEXT("samples_signed must be true"));
+    }
+    mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int;
+    if (bits_per_sample != 16) {
+        mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 16"));
+    }
+
+    audiodelays_reverb_obj_t *self = mp_obj_malloc(audiodelays_reverb_obj_t, &audiodelays_reverb_type);
+    common_hal_audiodelays_reverb_construct(self, args[ARG_roomsize].u_obj, args[ARG_damp].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 Reverb."""
+//|         ...
+//|
+static mp_obj_t audiodelays_reverb_deinit(mp_obj_t self_in) {
+    audiodelays_reverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiodelays_reverb_deinit(self);
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_reverb_deinit_obj, audiodelays_reverb_deinit);
+
+static void check_for_deinit(audiodelays_reverb_obj_t *self) {
+    audiosample_check_for_deinit(&self->base);
+}
+
+//|     def __enter__(self) -> Reverb:
+//|         """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."""
+//|         ...
+//|
+//  Provided by context manager helper.
+
+//|     roomsize: synthio.BlockInput
+//|     """Apparent size of the room 0.0-1.0"""
+static mp_obj_t audiodelays_reverb_obj_get_roomsize(mp_obj_t self_in) {
+    return common_hal_audiodelays_reverb_get_roomsize(self_in);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_reverb_get_roomsize_obj, audiodelays_reverb_obj_get_roomsize);
+
+static mp_obj_t audiodelays_reverb_obj_set_roomsize(mp_obj_t self_in, mp_obj_t roomsize) {
+    audiodelays_reverb_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_audiodelays_reverb_set_roomsize(self, roomsize);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(audiodelays_reverb_set_roomsize_obj, audiodelays_reverb_obj_set_roomsize);
+
+MP_PROPERTY_GETSET(audiodelays_reverb_roomsize_obj,
+    (mp_obj_t)&audiodelays_reverb_get_roomsize_obj,
+    (mp_obj_t)&audiodelays_reverb_set_roomsize_obj);
+
+//|     damp: synthio.BlockInput
+//|     """How reverbrent the area is. 0.0-1.0"""
path: shared-bindings/audiodelays/Reverb.c
user: login: gamblor21
body: Reworded it.
created_at: 2025-04-09T21:59:17Z
author_association: MEMBER
diff_hunk: @@ -13,6 +13,7 @@
 #include "shared-bindings/audiodelays/Echo.h"
 #include "shared-bindings/audiodelays/PitchShift.h"
 
+
path: shared-bindings/audiodelays/__init__.c
user: login: tannewt
body: ```suggestion
```
created_at: 2025-04-15T16:47:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,358 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+//
+// Based on FreeVerb - https://github.com/sinshu/freeverb/tree/main
+// Fixed point ideas from - Paul Stoffregen in the Teensy audio library https://github.com/PaulStoffregen/Audio/blob/master/effect_freeverb.cpp
+//
+#include "shared-bindings/audiofreeverb/Freeverb.h"
+
+#include <stdint.h>
+#include "py/runtime.h"
+#include <math.h>
+
+void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self, mp_obj_t roomsize, mp_obj_t damp, 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->base.bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places
+    self->base.samples_signed = samples_signed; // Are the samples we provide signed (common is true)
+    self->base.channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo
+    self->base.sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects
+    self->base.single_buffer = false;
+    self->base.max_buffer_length = buffer_size;
+
+    // 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);
path: shared-module/audiofreeverb/Freeverb.c
user: login: tannewt
body: `m_malloc` will raise an exception on its own. If you want null back then use the `_maybe` version.

```suggestion
    self->buffer[0] = m_malloc_maybe(self->buffer_len);
```
created_at: 2025-04-15T16:48:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,358 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
+//
+// SPDX-License-Identifier: MIT
+//
+// Based on FreeVerb - https://github.com/sinshu/freeverb/tree/main
+// Fixed point ideas from - Paul Stoffregen in the Teensy audio library https://github.com/PaulStoffregen/Audio/blob/master/effect_freeverb.cpp
+//
+#include "shared-bindings/audiofreeverb/Freeverb.h"
+
+#include <stdint.h>
+#include "py/runtime.h"
+#include <math.h>
+
+void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self, mp_obj_t roomsize, mp_obj_t damp, 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->base.bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places
+    self->base.samples_signed = samples_signed; // Are the samples we provide signed (common is true)
+    self->base.channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo
+    self->base.sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects
+    self->base.single_buffer = false;
+    self->base.max_buffer_length = buffer_size;
+
+    // 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);
path: shared-module/audiofreeverb/Freeverb.c
user: login: gamblor21
body: Fixed all the times this occurred.
created_at: 2025-04-20T15:59:06Z
author_association: MEMBER

PR comments:
user: login: gamblor21
created_at: 2025-04-05T19:32:15Z
body: @relic-se If you have a moment this is ready for testing. No rush.
author_association: MEMBER
user: login: relic-se
created_at: 2025-04-07T21:05:09Z
body: ~~As for comments on your questions, I think it is fine to keep reverb in `audiodelays`. Although it is filter-based, it is mostly associated with delay, and it is unlikely at the moment for us to add multiple reverb-based effects in to warrant a dedicated `audioreverbs` module (but I am a fan of spring reverb...). It can always be moved around in a future release if need be.~~

Okay, I may have changed my mind. What if we did create a new `audioreverbs` module, renamed this effect to "RoomReverb" or "Freeverb". Then, we could aim for "SpringReverb" and "PlateReverb" additions in the future? I'm not certain exactly how those would work or if they're feasible for the target platforms, but it would definitely give us sufficient variety. Other potential reverb types could be "ReverseReverb" and "Flerb" (has a "flanging" sound).

Unless you want to take a swing at allowing independent dry/wet mix control globally, I vote we keep it standardized for now and then expand all effects in the future to support this functionality. This question might be better poised on a separate issue/PR, but how would you handle `BlockInput` support in that scenario?
author_association: NONE
user: login: gamblor21
created_at: 2025-04-09T22:04:43Z
body: > Okay, I may have changed my mind. What if we did create a new `audioreverbs` module, renamed this effect to "RoomReverb" or "Freeverb". Then, we could aim for "SpringReverb" and "PlateReverb" additions in the future? I'm not certain exactly how those would work or if they're feasible for the target platforms, but it would definitely give us sufficient variety. Other potential reverb types could be "ReverseReverb" and "Flerb" (has a "flanging" sound).

@tannewt @dhalbert Any preference on keeping the reverb in `audiodelays` vs making a new `audioreverbs`? We had talked about a new module for them previously. Or we could always create these effect for now and move it later?
author_association: MEMBER
user: login: dhalbert
created_at: 2025-04-09T22:19:19Z
body: The advantage of a separate module is that we can turn it off if there are space problems. So I don't think it's bad. Didn't we have `audioeffects` for a while but it became too big?

As for further reverbs, how different are they in terms of code? Could you make a parameterized reverb that handles all of those in a single class with parameters, or is there something different in the calcuations?
author_association: COLLABORATOR
user: login: relic-se
created_at: 2025-04-09T23:22:37Z
body: > The advantage of a separate module is that we can turn it off if there are space problems. So I don't think it's bad. Didn't we have `audioeffects` for a while but it became too big?

There was never a singular `audioeffects` module to my knowledge since we moved to separate modules in initial talks. Nonetheless, I am in agreement.

> As for further reverbs, how different are they in terms of code? Could you make a parameterized reverb that handles all of those in a single class with parameters, or is there something different in the calculations?

Unfortunately, I think other reverb types will operate very differently. The reverb algorithm implemented here, "Freeverb", is finely-tuned for its particular operation (a general "room" sound). Further research may dispute this.
author_association: NONE
user: login: tannewt
created_at: 2025-04-10T17:07:13Z
body: I'd go further and put this in a `freeverb` module (check pypi to make sure it doesn't exist). That way each reverb implementation can have its own module and inclusion.
author_association: MEMBER
user: login: gamblor21
created_at: 2025-04-11T21:31:24Z
body: > Unless you want to take a swing at allowing independent dry/wet mix control globally, I vote we keep it standardized for now and then expand all effects in the future to support this functionality. This question might be better poised on a separate issue/PR, but how would you handle `BlockInput` support in that scenario?

I think I may leave it and get this PR in and we can look at how to handle mix better across the board later. I think for `BlockInput` on separate wet/dry I'd just let them vary and and maybe with a caveat if you got 1.0 and 1.0 then yes your audio volume could double. Direct people to the `mix=0.5` but have an advance mode that basically we take the guard rails off.
author_association: MEMBER
user: login: gamblor21
created_at: 2025-04-13T20:37:02Z
body: I think the CI errors may be build errors?
author_association: MEMBER
user: login: relic-se
created_at: 2025-04-14T21:58:22Z
body: @gamblor21 I've tested your updates, and can confirm that the `mix` property is working as intended now. I've also tested `BlockInput` on all 3 properties, `mix`, `roomsize`, and `damp`. Everything works great!
author_association: NONE
user: login: tannewt
created_at: 2025-04-15T16:50:59Z
body: This script will fix the zephyr build for you: https://github.com/adafruit/circuitpython/blob/main/ports/zephyr-cp/cptools/update_board_info.py
author_association: MEMBER
user: login: gamblor21
created_at: 2025-04-20T15:56:24Z
body: > This script will fix the zephyr build for you: https://github.com/adafruit/circuitpython/blob/main/ports/zephyr-cp/cptools/update_board_info.py

Thanks, took a bit to realize how it all worked. Just a note for the future, if someone (me) has stale/old folders in the shared-bindings directory it can cause problems. Wasn't hard to fix after but a small "gotcha".

I think the other issues were mostly the fact I submitted this PR before zephyr, and that should not happen again for others.
author_association: MEMBER