From 47c9fc1f356ccf75850d3b983efe1363b3c8a562 Mon Sep 17 00:00:00 2001
From: Cooper Dalrymple <dcooperdalrymple@gmail.com>
Date: Mon, 14 Apr 2025 20:04:58 -0500
Subject: [PATCH 01/10] Always update `echo_buffer_len` during
 `recalculate_delay`.

---
 shared-module/audiodelays/Echo.c | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c
index 968c3bbddb693..2a0eee2ca7b0c 100644
--- a/shared-module/audiodelays/Echo.c
+++ b/shared-module/audiodelays/Echo.c
@@ -136,16 +136,12 @@ void recalculate_delay(audiodelays_echo_obj_t *self, mp_float_t f_delay_ms) {
         // Calculate the current echo buffer length in bytes
         uint32_t new_echo_buffer_len = (uint32_t)(self->base.sample_rate / MICROPY_FLOAT_CONST(1000.0) * f_delay_ms) * (self->base.channel_count * sizeof(uint16_t));
 
-        // Check if our new echo is too long for our maximum buffer
+        // Limit to valid range
         if (new_echo_buffer_len > self->max_echo_buffer_len) {
-            return;
-        } else if (new_echo_buffer_len < 0.0) { // or too short!
-            return;
-        }
-
-        // If the echo buffer is larger then our audio buffer weird things happen
-        if (new_echo_buffer_len < self->buffer_len) {
-            return;
+            new_echo_buffer_len = self->max_echo_buffer_len;
+        } else if (new_echo_buffer_len < self->buffer_len) {
+            // If the echo buffer is smaller than our audio buffer, weird things happen
+            new_echo_buffer_len = self->buffer_len;
         }
 
         self->echo_buffer_len = new_echo_buffer_len;

From 57391fa44b4ff2070eabed9245af18393a928611 Mon Sep 17 00:00:00 2001
From: Cooper Dalrymple <dcooperdalrymple@gmail.com>
Date: Mon, 14 Apr 2025 20:09:35 -0500
Subject: [PATCH 02/10] Unify `echo_buffer_pos` regardless of `freq_shift`,
 remove redundant `echo_buffer_write_pos`, and split buffer for stereo output
 when `freq_shift=True`.

---
 shared-module/audiodelays/Echo.c | 87 +++++++++++++++-----------------
 shared-module/audiodelays/Echo.h |  7 +--
 2 files changed, 43 insertions(+), 51 deletions(-)

diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c
index 2a0eee2ca7b0c..f69b65316a9cc 100644
--- a/shared-module/audiodelays/Echo.c
+++ b/shared-module/audiodelays/Echo.c
@@ -98,11 +98,10 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_
 
     // read is where we read previous echo from delay_ms ago to play back now
     // write is where the store the latest playing sample to echo back later
-    self->echo_buffer_read_pos = self->buffer_len / sizeof(uint16_t);
-    self->echo_buffer_write_pos = 0;
+    self->echo_buffer_pos = 0;
 
-    // where we read the previous echo from delay_ms ago to play back now (for freq shift)
-    self->echo_buffer_left_pos = self->echo_buffer_right_pos = 0;
+    // use a separate buffer position for the right channel when using freq_shift
+    self->echo_buffer_right_pos = 0;
 }
 
 void common_hal_audiodelays_echo_deinit(audiodelays_echo_obj_t *self) {
@@ -131,7 +130,8 @@ void recalculate_delay(audiodelays_echo_obj_t *self, mp_float_t f_delay_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));
-        self->echo_buffer_len = self->max_echo_buffer_len;
+        // Only use half of the buffer per channel if stereo
+        self->echo_buffer_len = self->max_echo_buffer_len >> (self->base.channel_count - 1);
     } else {
         // Calculate the current echo buffer length in bytes
         uint32_t new_echo_buffer_len = (uint32_t)(self->base.sample_rate / MICROPY_FLOAT_CONST(1000.0) * f_delay_ms) * (self->base.channel_count * sizeof(uint16_t));
@@ -174,6 +174,12 @@ bool common_hal_audiodelays_echo_get_freq_shift(audiodelays_echo_obj_t *self) {
 }
 
 void common_hal_audiodelays_echo_set_freq_shift(audiodelays_echo_obj_t *self, bool freq_shift) {
+    // Clear the echo buffer and reset buffer position if changing freq_shift modes
+    if (self->freq_shift != freq_shift) {
+        memset(self->echo_buffer, 0, self->max_echo_buffer_len);
+        self->echo_buffer_pos = 0;
+        self->echo_buffer_right_pos = 0;
+    }
     self->freq_shift = freq_shift;
     uint32_t delay_ms = (uint32_t)synthio_block_slot_get(&self->delay_ms);
     recalculate_delay(self, delay_ms);
@@ -275,12 +281,9 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
         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;
-            }
+        uint32_t echo_buffer_pos = self->echo_buffer_pos;
+        if (self->freq_shift && channel == 1) {
+            echo_buffer_pos = self->echo_buffer_right_pos;
         }
 
         // If we have no sample keep the echo echoing
@@ -304,19 +307,20 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                 for (uint32_t i = 0; i < length; i++) {
                     int16_t echo, word = 0;
                     uint32_t next_buffer_pos = 0;
+                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && (channel == 1 || (i % self->base.channel_count) == 1));
 
                     if (self->freq_shift) {
-                        echo = echo_buffer[echo_buffer_pos >> 8];
+                        echo = echo_buffer[(echo_buffer_pos >> 8) + echo_buffer_offset];
                         next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate;
 
                         for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) {
-                            word = (int16_t)(echo_buffer[j % echo_buf_len] * decay);
-                            echo_buffer[j % echo_buf_len] = word;
+                            word = (int16_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay);
+                            echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = word;
                         }
                     } else {
-                        echo = echo_buffer[self->echo_buffer_read_pos++];
+                        echo = echo_buffer[echo_buffer_pos];
                         word = (int16_t)(echo * decay);
-                        echo_buffer[self->echo_buffer_write_pos++] = word;
+                        echo_buffer[echo_buffer_pos++] = word;
                     }
 
                     word = (int16_t)(echo * mix);
@@ -333,15 +337,10 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         }
                     }
 
-                    if (self->freq_shift) {
+                    if (self->freq_shift && (single_channel_output || echo_buffer_offset)) {
                         echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
-                    } else {
-                        if (self->echo_buffer_read_pos >= echo_buf_len) {
-                            self->echo_buffer_read_pos = 0;
-                        }
-                        if (self->echo_buffer_write_pos >= echo_buf_len) {
-                            self->echo_buffer_write_pos = 0;
-                        }
+                    } else if (!self->freq_shift && echo_buffer_pos >= echo_buf_len) {
+                        echo_buffer_pos = 0;
                     }
                 }
             }
@@ -376,37 +375,39 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
 
                     int32_t echo, word = 0;
                     uint32_t next_buffer_pos = 0;
+                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && (channel == 1 || (i % self->base.channel_count) == 1));
+
                     if (self->freq_shift) {
-                        echo = echo_buffer[echo_buffer_pos >> 8];
+                        echo = echo_buffer[(echo_buffer_pos >> 8) + echo_buffer_offset];
                         next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate;
                     } else {
-                        echo = echo_buffer[self->echo_buffer_read_pos++];
+                        echo = echo_buffer[echo_buffer_pos];
                         word = (int32_t)(echo * decay + sample_word);
                     }
 
                     if (MP_LIKELY(self->base.bits_per_sample == 16)) {
                         if (self->freq_shift) {
                             for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) {
-                                word = (int32_t)(echo_buffer[j % echo_buf_len] * decay + sample_word);
+                                word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word);
                                 word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2));
-                                echo_buffer[j % echo_buf_len] = (int16_t)word;
+                                echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)word;
                             }
                         } else {
                             word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2));
-                            echo_buffer[self->echo_buffer_write_pos++] = (int16_t)word;
+                            echo_buffer[echo_buffer_pos++] = (int16_t)word;
                         }
                     } else {
                         if (self->freq_shift) {
                             for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) {
-                                word = (int32_t)(echo_buffer[j % echo_buf_len] * decay + sample_word);
+                                word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word);
                                 // Do not have mix_down for 8 bit so just hard cap samples into 1 byte
                                 word = MIN(MAX(word, -128), 127);
-                                echo_buffer[j % echo_buf_len] = (int8_t)word;
+                                echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)word;
                             }
                         } else {
                             // Do not have mix_down for 8 bit so just hard cap samples into 1 byte
                             word = MIN(MAX(word, -128), 127);
-                            echo_buffer[self->echo_buffer_write_pos++] = (int8_t)word;
+                            echo_buffer[echo_buffer_pos++] = (int8_t)word;
                         }
                     }
 
@@ -427,15 +428,10 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         }
                     }
 
-                    if (self->freq_shift) {
+                    if (self->freq_shift && (single_channel_output || echo_buffer_offset)) {
                         echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
-                    } else {
-                        if (self->echo_buffer_read_pos >= echo_buf_len) {
-                            self->echo_buffer_read_pos = 0;
-                        }
-                        if (self->echo_buffer_write_pos >= echo_buf_len) {
-                            self->echo_buffer_write_pos = 0;
-                        }
+                    } else if (!self->freq_shift && echo_buffer_pos >= echo_buf_len) {
+                        echo_buffer_pos = 0;
                     }
                 }
             }
@@ -448,12 +444,11 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
             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;
-            }
+        // Update buffer position
+        if (self->freq_shift && channel == 1) {
+            self->echo_buffer_right_pos = echo_buffer_pos;
+        } else {
+            self->echo_buffer_pos = echo_buffer_pos;
         }
     }
 
diff --git a/shared-module/audiodelays/Echo.h b/shared-module/audiodelays/Echo.h
index 7f5dbb69f090a..247a27ce5f23c 100644
--- a/shared-module/audiodelays/Echo.h
+++ b/shared-module/audiodelays/Echo.h
@@ -37,12 +37,9 @@ typedef struct {
     uint32_t echo_buffer_len; // bytes
     uint32_t max_echo_buffer_len; // bytes
 
-    uint32_t echo_buffer_read_pos; // words
-    uint32_t echo_buffer_write_pos; // words
-
-    uint32_t echo_buffer_rate; // words << 8
-    uint32_t echo_buffer_left_pos; // words << 8
+    uint32_t echo_buffer_pos; // words (<< 8 when freq_shift=True)
     uint32_t echo_buffer_right_pos; // words << 8
+    uint32_t echo_buffer_rate; // words << 8
 
     mp_obj_t sample;
 } audiodelays_echo_obj_t;

From feee74a5e2279e3cfd6d17bccf89233ebc50c6ad Mon Sep 17 00:00:00 2001
From: Cooper Dalrymple <dcooperdalrymple@gmail.com>
Date: Mon, 21 Apr 2025 17:31:58 -0500
Subject: [PATCH 03/10] Improve right channel logic.

---
 shared-module/audiodelays/Echo.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c
index f69b65316a9cc..4acb00cfc3baf 100644
--- a/shared-module/audiodelays/Echo.c
+++ b/shared-module/audiodelays/Echo.c
@@ -307,7 +307,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                 for (uint32_t i = 0; i < length; i++) {
                     int16_t echo, word = 0;
                     uint32_t next_buffer_pos = 0;
-                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && (channel == 1 || (i % self->base.channel_count) == 1));
+                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && ((single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1)));
 
                     if (self->freq_shift) {
                         echo = echo_buffer[(echo_buffer_pos >> 8) + echo_buffer_offset];
@@ -337,7 +337,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         }
                     }
 
-                    if (self->freq_shift && (single_channel_output || echo_buffer_offset)) {
+                    if (self->freq_shift && (self->base.channel_count == 1 || single_channel_output || (!single_channel_output && (i % self->base.channel_count) == 1))) {
                         echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
                     } else if (!self->freq_shift && echo_buffer_pos >= echo_buf_len) {
                         echo_buffer_pos = 0;
@@ -375,7 +375,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
 
                     int32_t echo, word = 0;
                     uint32_t next_buffer_pos = 0;
-                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && (channel == 1 || (i % self->base.channel_count) == 1));
+                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && ((single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1)));
 
                     if (self->freq_shift) {
                         echo = echo_buffer[(echo_buffer_pos >> 8) + echo_buffer_offset];
@@ -428,7 +428,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         }
                     }
 
-                    if (self->freq_shift && (single_channel_output || echo_buffer_offset)) {
+                    if (self->freq_shift && (self->base.channel_count == 1 || single_channel_output || (!single_channel_output && (i % self->base.channel_count) == 1))) {
                         echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
                     } else if (!self->freq_shift && echo_buffer_pos >= echo_buf_len) {
                         echo_buffer_pos = 0;

From 7d0e72e5ca03d64bf648a1b65841727f36e56ece Mon Sep 17 00:00:00 2001
From: Cooper Dalrymple <dcooperdalrymple@gmail.com>
Date: Mon, 21 Apr 2025 17:42:39 -0500
Subject: [PATCH 04/10] Unify echo buffer channel splitting.

---
 shared-module/audiodelays/Echo.c | 73 +++++++++++++++++++-------------
 shared-module/audiodelays/Echo.h |  4 +-
 2 files changed, 45 insertions(+), 32 deletions(-)

diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c
index 4acb00cfc3baf..35b3556db35b2 100644
--- a/shared-module/audiodelays/Echo.c
+++ b/shared-module/audiodelays/Echo.c
@@ -98,9 +98,9 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_
 
     // read is where we read previous echo from delay_ms ago to play back now
     // write is where the store the latest playing sample to echo back later
-    self->echo_buffer_pos = 0;
+    self->echo_buffer_left_pos = 0;
 
-    // use a separate buffer position for the right channel when using freq_shift
+    // use a separate buffer position for the right channel
     self->echo_buffer_right_pos = 0;
 }
 
@@ -127,18 +127,21 @@ 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);
 
+    // Calculate the maximum buffer size per channel in bytes
+    uint32_t max_echo_buffer_len = self->max_echo_buffer_len >> (self->base.channel_count - 1);
+
     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));
         // Only use half of the buffer per channel if stereo
-        self->echo_buffer_len = self->max_echo_buffer_len >> (self->base.channel_count - 1);
+        self->echo_buffer_len = max_echo_buffer_len;
     } else {
         // Calculate the current echo buffer length in bytes
-        uint32_t new_echo_buffer_len = (uint32_t)(self->base.sample_rate / MICROPY_FLOAT_CONST(1000.0) * f_delay_ms) * (self->base.channel_count * sizeof(uint16_t));
+        uint32_t new_echo_buffer_len = (uint32_t)(self->base.sample_rate / MICROPY_FLOAT_CONST(1000.0) * f_delay_ms) * sizeof(uint16_t);
 
         // Limit to valid range
-        if (new_echo_buffer_len > self->max_echo_buffer_len) {
-            new_echo_buffer_len = self->max_echo_buffer_len;
+        if (new_echo_buffer_len > max_echo_buffer_len) {
+            new_echo_buffer_len = max_echo_buffer_len;
         } else if (new_echo_buffer_len < self->buffer_len) {
             // If the echo buffer is smaller than our audio buffer, weird things happen
             new_echo_buffer_len = self->buffer_len;
@@ -147,7 +150,9 @@ void recalculate_delay(audiodelays_echo_obj_t *self, mp_float_t f_delay_ms) {
         self->echo_buffer_len = new_echo_buffer_len;
 
         // Clear the now unused part of the buffer or some weird artifacts appear
-        memset(self->echo_buffer + self->echo_buffer_len, 0, self->max_echo_buffer_len - self->echo_buffer_len);
+        for (uint32_t i = 0; i < self->base.channel_count; i++) {
+            memset(self->echo_buffer + (i * max_echo_buffer_len) + self->echo_buffer_len, 0, max_echo_buffer_len - self->echo_buffer_len);
+        }
     }
 
     self->current_delay_ms = f_delay_ms;
@@ -177,7 +182,7 @@ void common_hal_audiodelays_echo_set_freq_shift(audiodelays_echo_obj_t *self, bo
     // Clear the echo buffer and reset buffer position if changing freq_shift modes
     if (self->freq_shift != freq_shift) {
         memset(self->echo_buffer, 0, self->max_echo_buffer_len);
-        self->echo_buffer_pos = 0;
+        self->echo_buffer_left_pos = 0;
         self->echo_buffer_right_pos = 0;
     }
     self->freq_shift = freq_shift;
@@ -279,12 +284,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
         }
 
         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 = self->echo_buffer_pos;
-        if (self->freq_shift && channel == 1) {
-            echo_buffer_pos = self->echo_buffer_right_pos;
-        }
+        uint32_t max_echo_buf_len = (self->max_echo_buffer_len >> (self->base.channel_count - 1)) / sizeof(uint16_t);
 
         // If we have no sample keep the echo echoing
         if (self->sample == NULL) {
@@ -307,7 +307,10 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                 for (uint32_t i = 0; i < length; i++) {
                     int16_t echo, word = 0;
                     uint32_t next_buffer_pos = 0;
-                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && ((single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1)));
+
+                    // Get our echo buffer position and offset depending on current channel
+                    uint32_t echo_buffer_offset = max_echo_buf_len * ((single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1));
+                    uint32_t echo_buffer_pos = echo_buffer_offset ? self->echo_buffer_right_pos : self->echo_buffer_left_pos;
 
                     if (self->freq_shift) {
                         echo = echo_buffer[(echo_buffer_pos >> 8) + echo_buffer_offset];
@@ -318,9 +321,9 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                             echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = word;
                         }
                     } else {
-                        echo = echo_buffer[echo_buffer_pos];
+                        echo = echo_buffer[echo_buffer_pos + echo_buffer_offset];
                         word = (int16_t)(echo * decay);
-                        echo_buffer[echo_buffer_pos++] = word;
+                        echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = word;
                     }
 
                     word = (int16_t)(echo * mix);
@@ -337,11 +340,18 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         }
                     }
 
-                    if (self->freq_shift && (self->base.channel_count == 1 || single_channel_output || (!single_channel_output && (i % self->base.channel_count) == 1))) {
+                    if (self->freq_shift) {
                         echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
                     } else if (!self->freq_shift && echo_buffer_pos >= echo_buf_len) {
                         echo_buffer_pos = 0;
                     }
+
+                    // Update buffer position
+                    if (echo_buffer_offset) {
+                        self->echo_buffer_right_pos = echo_buffer_pos;
+                    } else {
+                        self->echo_buffer_left_pos = echo_buffer_pos;
+                    }
                 }
             }
 
@@ -375,13 +385,16 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
 
                     int32_t echo, word = 0;
                     uint32_t next_buffer_pos = 0;
-                    uint32_t echo_buffer_offset = echo_buf_len * (self->freq_shift && ((single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1)));
+
+                    // Get our echo buffer position and offset depending on current channel
+                    uint32_t echo_buffer_offset = max_echo_buf_len * ((single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1));
+                    uint32_t echo_buffer_pos = echo_buffer_offset ? self->echo_buffer_right_pos : self->echo_buffer_left_pos;
 
                     if (self->freq_shift) {
                         echo = echo_buffer[(echo_buffer_pos >> 8) + echo_buffer_offset];
                         next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate;
                     } else {
-                        echo = echo_buffer[echo_buffer_pos];
+                        echo = echo_buffer[echo_buffer_pos + echo_buffer_offset];
                         word = (int32_t)(echo * decay + sample_word);
                     }
 
@@ -394,7 +407,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                             }
                         } else {
                             word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2));
-                            echo_buffer[echo_buffer_pos++] = (int16_t)word;
+                            echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)word;
                         }
                     } else {
                         if (self->freq_shift) {
@@ -407,7 +420,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         } else {
                             // Do not have mix_down for 8 bit so just hard cap samples into 1 byte
                             word = MIN(MAX(word, -128), 127);
-                            echo_buffer[echo_buffer_pos++] = (int8_t)word;
+                            echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)word;
                         }
                     }
 
@@ -428,11 +441,18 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
                         }
                     }
 
-                    if (self->freq_shift && (self->base.channel_count == 1 || single_channel_output || (!single_channel_output && (i % self->base.channel_count) == 1))) {
+                    if (self->freq_shift) {
                         echo_buffer_pos = next_buffer_pos % (echo_buf_len << 8);
                     } else if (!self->freq_shift && echo_buffer_pos >= echo_buf_len) {
                         echo_buffer_pos = 0;
                     }
+
+                    // Update buffer position
+                    if (echo_buffer_offset) {
+                        self->echo_buffer_right_pos = echo_buffer_pos;
+                    } else {
+                        self->echo_buffer_left_pos = echo_buffer_pos;
+                    }
                 }
             }
 
@@ -443,13 +463,6 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *
             self->sample_remaining_buffer += (n * (self->base.bits_per_sample / 8));
             self->sample_buffer_length -= n;
         }
-
-        // Update buffer position
-        if (self->freq_shift && channel == 1) {
-            self->echo_buffer_right_pos = echo_buffer_pos;
-        } else {
-            self->echo_buffer_pos = echo_buffer_pos;
-        }
     }
 
     // Finally pass our buffer and length to the calling audio function
diff --git a/shared-module/audiodelays/Echo.h b/shared-module/audiodelays/Echo.h
index 247a27ce5f23c..cc37f7030be0e 100644
--- a/shared-module/audiodelays/Echo.h
+++ b/shared-module/audiodelays/Echo.h
@@ -37,8 +37,8 @@ typedef struct {
     uint32_t echo_buffer_len; // bytes
     uint32_t max_echo_buffer_len; // bytes
 
-    uint32_t echo_buffer_pos; // words (<< 8 when freq_shift=True)
-    uint32_t echo_buffer_right_pos; // words << 8
+    uint32_t echo_buffer_left_pos; // words (<< 8 when freq_shift=True)
+    uint32_t echo_buffer_right_pos; // words (<< 8 when freq_shift=True)
     uint32_t echo_buffer_rate; // words << 8
 
     mp_obj_t sample;

From db14b3b7ac34afc9b81bf8141c7ab7138db2d9a6 Mon Sep 17 00:00:00 2001
From: Bernhard Bablok <bablokb@gmx.de>
Date: Tue, 22 Apr 2025 18:00:23 +0200
Subject: [PATCH 05/10] fixed I2C pins

---
 .../boards/pimoroni_inky_frame_5_7/mpconfigboard.h            | 4 ++--
 .../boards/pimoroni_inky_frame_7_3/mpconfigboard.h            | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/mpconfigboard.h b/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/mpconfigboard.h
index d2547cc161568..a8c404c422501 100644
--- a/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/mpconfigboard.h
+++ b/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/mpconfigboard.h
@@ -14,8 +14,8 @@
 
 #define MICROPY_HW_LED_STATUS   (&pin_GPIO6)
 
-#define DEFAULT_I2C_BUS_SCL (&pin_GPIO4)
-#define DEFAULT_I2C_BUS_SDA (&pin_GPIO5)
+#define DEFAULT_I2C_BUS_SDA (&pin_GPIO4)
+#define DEFAULT_I2C_BUS_SCL (&pin_GPIO5)
 
 #define DEFAULT_UART_BUS_TX (&pin_GPIO0)
 #define DEFAULT_UART_BUS_RX (&pin_GPIO1)
diff --git a/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/mpconfigboard.h b/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/mpconfigboard.h
index 42cb1196a5475..32477b12c299f 100644
--- a/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/mpconfigboard.h
+++ b/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/mpconfigboard.h
@@ -14,8 +14,8 @@
 
 #define MICROPY_HW_LED_STATUS   (&pin_GPIO6)
 
-#define DEFAULT_I2C_BUS_SCL (&pin_GPIO4)
-#define DEFAULT_I2C_BUS_SDA (&pin_GPIO5)
+#define DEFAULT_I2C_BUS_SDA (&pin_GPIO4)
+#define DEFAULT_I2C_BUS_SCL (&pin_GPIO5)
 
 #define DEFAULT_UART_BUS_TX (&pin_GPIO0)
 #define DEFAULT_UART_BUS_RX (&pin_GPIO1)

From 66d0f753b152f4cdc958f529d4c7a6de8b89e572 Mon Sep 17 00:00:00 2001
From: Bernhard Bablok <bablokb@gmx.de>
Date: Wed, 23 Apr 2025 08:03:17 +0200
Subject: [PATCH 06/10] fix memory corruption caused by pre-loading

---
 ports/espressif/common-hal/audiobusio/__init__.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ports/espressif/common-hal/audiobusio/__init__.c b/ports/espressif/common-hal/audiobusio/__init__.c
index 226e371c5b0b0..d07a0b521ba9b 100644
--- a/ports/espressif/common-hal/audiobusio/__init__.c
+++ b/ports/espressif/common-hal/audiobusio/__init__.c
@@ -184,7 +184,7 @@ void port_i2s_play(i2s_t *self, mp_obj_t sample, bool loop) {
         self->next_buffer_size = sizeof(starting_frame);
         i2s_fill_buffer(self);
         i2s_channel_preload_data(self->handle, &starting_frame, sizeof(uint32_t), &bytes_loaded);
-        preloaded += 1;
+        preloaded += bytes_loaded;
     }
 
     // enable the channel

From 28ee20abcbf0d5b0e8d8b5e202bf592e2fb9f173 Mon Sep 17 00:00:00 2001
From: Bernhard Bablok <bablokb@gmx.de>
Date: Tue, 6 May 2025 19:20:16 +0200
Subject: [PATCH 07/10] implement spectra6 support

---
 ports/atmel-samd/boards/openbook_m4/board.c   |  1 +
 .../adafruit_magtag_2.9_grayscale/board.c     |  1 +
 .../elecrow_crowpanel_4_2_epaper/board.c      |  1 +
 .../boards/heltec_vision_master_e290/board.c  |  1 +
 .../boards/heltec_wireless_paper/board.c      |  1 +
 .../espressif/boards/m5stack_m5paper/board.c  |  1 +
 ports/espressif/boards/sqfmi_watchy/board.c   |  1 +
 .../bradanlanestudio_explorer_rp2040/board.c  |  2 +
 .../boards/pimoroni_badger2040/board.c        |  1 +
 .../boards/pimoroni_badger2040w/board.c       |  1 +
 .../boards/pimoroni_inky_frame_5_7/board.c    |  1 +
 .../boards/pimoroni_inky_frame_7_3/board.c    |  1 +
 shared-bindings/epaperdisplay/EPaperDisplay.c |  7 +++-
 shared-bindings/epaperdisplay/EPaperDisplay.h |  2 +-
 shared-module/displayio/ColorConverter.c      | 38 ++++++++++++++++++-
 shared-module/displayio/ColorConverter.h      |  1 +
 shared-module/displayio/Palette.h             |  1 +
 shared-module/epaperdisplay/EPaperDisplay.c   |  9 +++--
 18 files changed, 63 insertions(+), 8 deletions(-)

diff --git a/ports/atmel-samd/boards/openbook_m4/board.c b/ports/atmel-samd/boards/openbook_m4/board.c
index 032ebe9f628a7..3ad2e162ecf3c 100644
--- a/ports/atmel-samd/boards/openbook_m4/board.c
+++ b/ports/atmel-samd/boards/openbook_m4/board.c
@@ -85,6 +85,7 @@ void board_init(void) {
         false, // chip_select (don't always toggle chip select)
         false, // grayscale
         false, // acep
+        false, // spectra6
         false, // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/espressif/boards/adafruit_magtag_2.9_grayscale/board.c b/ports/espressif/boards/adafruit_magtag_2.9_grayscale/board.c
index 321a5a1f2c7a0..430079a94dad2 100644
--- a/ports/espressif/boards/adafruit_magtag_2.9_grayscale/board.c
+++ b/ports/espressif/boards/adafruit_magtag_2.9_grayscale/board.c
@@ -141,6 +141,7 @@ void board_init(void) {
         false,  // always_toggle_chip_select
         true, // grayscale
         false, // acep
+        false, // spectra6
         false,  // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/espressif/boards/elecrow_crowpanel_4_2_epaper/board.c b/ports/espressif/boards/elecrow_crowpanel_4_2_epaper/board.c
index 4567e2c595627..88150dd401b97 100644
--- a/ports/espressif/boards/elecrow_crowpanel_4_2_epaper/board.c
+++ b/ports/espressif/boards/elecrow_crowpanel_4_2_epaper/board.c
@@ -96,6 +96,7 @@ void board_init(void) {
         false, // always_toggle_chip_select
         false, // grayscale
         false, // acep
+        false, // spectra6
         false, // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/espressif/boards/heltec_vision_master_e290/board.c b/ports/espressif/boards/heltec_vision_master_e290/board.c
index cc8afe226be95..e392f65a76432 100644
--- a/ports/espressif/boards/heltec_vision_master_e290/board.c
+++ b/ports/espressif/boards/heltec_vision_master_e290/board.c
@@ -94,6 +94,7 @@ void board_init(void) {
         false, // always_toggle_chip_select
         false, // grayscale
         false, // acep
+        false, // spectra6
         false, // two_byte_sequence_length
         true); // address_little_endian
 }
diff --git a/ports/espressif/boards/heltec_wireless_paper/board.c b/ports/espressif/boards/heltec_wireless_paper/board.c
index 2807f2d1a2012..28fb6ae41b9a1 100644
--- a/ports/espressif/boards/heltec_wireless_paper/board.c
+++ b/ports/espressif/boards/heltec_wireless_paper/board.c
@@ -134,6 +134,7 @@ void board_init(void) {
         false, // always_toggle_chip_select
         false, // grayscale
         false, // acep
+        false, // spectra6
         false, // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/espressif/boards/m5stack_m5paper/board.c b/ports/espressif/boards/m5stack_m5paper/board.c
index fbf66fa5ccdf9..db68df79c4291 100644
--- a/ports/espressif/boards/m5stack_m5paper/board.c
+++ b/ports/espressif/boards/m5stack_m5paper/board.c
@@ -74,6 +74,7 @@ void board_init(void) {
     //     false,  // always_toggle_chip_select
     //     false, // grayscale
     //     true, // acep
+    //     false, // spectra6
     //     false,  // two_byte_sequence_length
     //     false  // address_little_endian
     // );
diff --git a/ports/espressif/boards/sqfmi_watchy/board.c b/ports/espressif/boards/sqfmi_watchy/board.c
index 25a7be4613c6e..393b8b759028a 100644
--- a/ports/espressif/boards/sqfmi_watchy/board.c
+++ b/ports/espressif/boards/sqfmi_watchy/board.c
@@ -190,6 +190,7 @@ void board_init(void) {
         false,  // always_toggle_chip_select
         false, // grayscale
         false, // acep
+        false, // spectra6
         false,  // two_byte_sequence_length
         true // address_little_endian
         );
diff --git a/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/board.c b/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/board.c
index a20b16473174c..19e18bd544bee 100644
--- a/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/board.c
+++ b/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/board.c
@@ -281,6 +281,7 @@ void board_init(void) {
             true,                                                                                           // always_toggle_chip_select
             false,                                                                                          // not grayscale
             false,                                                                                          // not acep
+            false,                                                                                          // not spectra6
             false,                                                                                          // not two_byte_sequence_length
             true);                                                                                          // address_little_endian
     } else if (vid_setting == 2) {  // Explorer SSD1608 BW
@@ -314,6 +315,7 @@ void board_init(void) {
             true,                                                                                           // always_toggle_chip_select
             false,                                                                                          // not grayscale
             false,                                                                                          // not acep
+            false,                                                                                          // not spectra6
             false,                                                                                          // not two_byte_sequence_length
             true);                                                                                          // address_little_endian
     } else {
diff --git a/ports/raspberrypi/boards/pimoroni_badger2040/board.c b/ports/raspberrypi/boards/pimoroni_badger2040/board.c
index 457bff33b4433..5950d11da8a11 100644
--- a/ports/raspberrypi/boards/pimoroni_badger2040/board.c
+++ b/ports/raspberrypi/boards/pimoroni_badger2040/board.c
@@ -303,6 +303,7 @@ void board_init(void) {
         false,  // always_toggle_chip_select
         false, // grayscale
         false, // acep
+        false, // spectra6
         false,  // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/raspberrypi/boards/pimoroni_badger2040w/board.c b/ports/raspberrypi/boards/pimoroni_badger2040w/board.c
index 30c05273b9478..7dc11af01eca3 100644
--- a/ports/raspberrypi/boards/pimoroni_badger2040w/board.c
+++ b/ports/raspberrypi/boards/pimoroni_badger2040w/board.c
@@ -303,6 +303,7 @@ void board_init(void) {
         false,  // always_toggle_chip_select
         false, // grayscale
         false, // acep
+        false, // spectra6
         false,  // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/board.c b/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/board.c
index 650d307711e8e..7969143ec6621 100644
--- a/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/board.c
+++ b/ports/raspberrypi/boards/pimoroni_inky_frame_5_7/board.c
@@ -97,6 +97,7 @@ void board_init(void) {
         false,  // always_toggle_chip_select
         false, // grayscale
         true, // acep
+        false, // spectra6
         false,  // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/board.c b/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/board.c
index a24c91e546da9..7c71e03b4281d 100644
--- a/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/board.c
+++ b/ports/raspberrypi/boards/pimoroni_inky_frame_7_3/board.c
@@ -156,6 +156,7 @@ void board_init(void) {
         false,  // always_toggle_chip_select
         false, // grayscale
         true, // acep
+        false, // spectra6
         false,  // two_byte_sequence_length
         false); // address_little_endian
 }
diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c
index 0d558a1319616..4b125370219f6 100644
--- a/shared-bindings/epaperdisplay/EPaperDisplay.c
+++ b/shared-bindings/epaperdisplay/EPaperDisplay.c
@@ -62,6 +62,7 @@
 //|         always_toggle_chip_select: bool = False,
 //|         grayscale: bool = False,
 //|         advanced_color_epaper: bool = False,
+//|         spectra6: bool = False,
 //|         two_byte_sequence_length: bool = False,
 //|         start_up_time: float = 0,
 //|         address_little_endian: bool = False,
@@ -104,6 +105,7 @@
 //|         :param bool always_toggle_chip_select: When True, chip select is toggled every byte
 //|         :param bool grayscale: When true, the color ram is the low bit of 2-bit grayscale
 //|         :param bool advanced_color_epaper: When true, the display is a 7-color advanced color epaper (ACeP)
+//|         :param bool spectra6: When true, the display is a 6-color spectra6 epaper
 //|         :param bool two_byte_sequence_length: When true, use two bytes to define sequence length
 //|         :param float start_up_time: Time to wait after reset before sending commands
 //|         :param bool address_little_endian: Send the least significant byte (not bit) of multi-byte addresses first. Ignored when ram is addressed with one byte
@@ -117,7 +119,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type,
            ARG_set_current_row_command, ARG_write_black_ram_command, ARG_black_bits_inverted,
            ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_highlight_color,
            ARG_refresh_display_command,  ARG_refresh_time, ARG_busy_pin, ARG_busy_state,
-           ARG_seconds_per_frame, ARG_always_toggle_chip_select, ARG_grayscale, ARG_advanced_color_epaper,
+           ARG_seconds_per_frame, ARG_always_toggle_chip_select, ARG_grayscale, ARG_advanced_color_epaper, ARG_spectra6,
            ARG_two_byte_sequence_length, ARG_start_up_time, ARG_address_little_endian };
     static const mp_arg_t allowed_args[] = {
         { MP_QSTR_display_bus, MP_ARG_REQUIRED | MP_ARG_OBJ },
@@ -147,6 +149,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type,
         { MP_QSTR_always_toggle_chip_select, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
         { MP_QSTR_grayscale, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
         { MP_QSTR_advanced_color_epaper, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
+        { MP_QSTR_spectra6, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
         { MP_QSTR_two_byte_sequence_length, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
         { MP_QSTR_start_up_time, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NEW_SMALL_INT(0)} },
         { MP_QSTR_address_little_endian, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
@@ -215,7 +218,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type,
         args[ARG_write_black_ram_command].u_int, args[ARG_black_bits_inverted].u_bool, write_color_ram_command,
         args[ARG_color_bits_inverted].u_bool, highlight_color, refresh_buf, refresh_buf_len, refresh_time,
         busy_pin, args[ARG_busy_state].u_bool, seconds_per_frame,
-        args[ARG_always_toggle_chip_select].u_bool, args[ARG_grayscale].u_bool, args[ARG_advanced_color_epaper].u_bool,
+        args[ARG_always_toggle_chip_select].u_bool, args[ARG_grayscale].u_bool, args[ARG_advanced_color_epaper].u_bool,args[ARG_spectra6].u_bool,
         two_byte_sequence_length, args[ARG_address_little_endian].u_bool
         );
 
diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h
index d4475e0a6aa43..016a07f1490c5 100644
--- a/shared-bindings/epaperdisplay/EPaperDisplay.h
+++ b/shared-bindings/epaperdisplay/EPaperDisplay.h
@@ -26,7 +26,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla
     uint16_t write_color_ram_command, bool color_bits_inverted, uint32_t highlight_color,
     const uint8_t *refresh_sequence, uint16_t refresh_sequence_len, mp_float_t refresh_time,
     const mcu_pin_obj_t *busy_pin, bool busy_state, mp_float_t seconds_per_frame,
-    bool always_toggle_chip_select, bool grayscale, bool acep, bool two_byte_sequence_length,
+    bool always_toggle_chip_select, bool grayscale, bool acep, bool spectra6, bool two_byte_sequence_length,
     bool address_little_endian);
 
 bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_obj_t *self);
diff --git a/shared-module/displayio/ColorConverter.c b/shared-module/displayio/ColorConverter.c
index cf5136f4e6bb6..ae7d3f02d5127 100644
--- a/shared-module/displayio/ColorConverter.c
+++ b/shared-module/displayio/ColorConverter.c
@@ -91,6 +91,40 @@ uint8_t displayio_colorconverter_compute_hue(uint32_t color_rgb888) {
     return hue;
 }
 
+uint8_t displayio_colorconverter_compute_sixcolor(uint32_t color_rgb888) {
+    // This is DDX=1, the default for the displays.
+    uint8_t chroma = displayio_colorconverter_compute_chroma(color_rgb888);
+    if (chroma >= 64) {
+        uint8_t hue = displayio_colorconverter_compute_hue(color_rgb888);
+        // Red 0
+        if (hue < 10) {
+            return 0x3;
+        }
+        // Yellow 42
+        if (hue < 42 + 21) {
+            return 0x2;
+        }
+        // Green 85
+        if (hue < 85 + 42) {
+            return 0x6;
+        }
+        // Blue 170
+        if (hue < 170 + 42) {
+            return 0x5;
+        }
+
+        // The rest is red to 255
+        return 0x3;
+    } else {
+        uint8_t luma = displayio_colorconverter_compute_luma(color_rgb888);
+        if (luma >= 128) {
+            return 0x1; // White
+        } else {
+            return 0x0; // Black
+        }
+    }
+}
+
 uint8_t displayio_colorconverter_compute_sevencolor(uint32_t color_rgb888) {
     // This is DDX=1, the default for the displays.
     uint8_t chroma = displayio_colorconverter_compute_chroma(color_rgb888);
@@ -309,7 +343,9 @@ void displayio_convert_color(const _displayio_colorspace_t *colorspace, bool dit
         return;
     } else if (colorspace->depth == 4) {
         uint8_t packed;
-        if (colorspace->sevencolor) {
+        if (colorspace->sixcolor) {
+            packed = displayio_colorconverter_compute_sixcolor(pixel);
+        } else if (colorspace->sevencolor) {
             packed = displayio_colorconverter_compute_sevencolor(pixel);
         } else {
             packed = displayio_colorconverter_compute_rgbd(pixel);
diff --git a/shared-module/displayio/ColorConverter.h b/shared-module/displayio/ColorConverter.h
index 60efc132f9ae5..d3dbedfe160ab 100644
--- a/shared-module/displayio/ColorConverter.h
+++ b/shared-module/displayio/ColorConverter.h
@@ -41,5 +41,6 @@ uint8_t displayio_colorconverter_compute_rgbd(uint32_t color_rgb888);
 uint8_t displayio_colorconverter_compute_luma(uint32_t color_rgb888);
 uint8_t displayio_colorconverter_compute_chroma(uint32_t color_rgb888);
 uint8_t displayio_colorconverter_compute_hue(uint32_t color_rgb888);
+uint8_t displayio_colorconverter_compute_sixcolor(uint32_t color_rgb888);
 uint8_t displayio_colorconverter_compute_sevencolor(uint32_t color_rgb888);
 void displayio_colorconverter_compute_tricolor(const _displayio_colorspace_t *colorspace, uint8_t pixel_hue, uint32_t *color);
diff --git a/shared-module/displayio/Palette.h b/shared-module/displayio/Palette.h
index 092b934b66a24..bb87a93d98133 100644
--- a/shared-module/displayio/Palette.h
+++ b/shared-module/displayio/Palette.h
@@ -19,6 +19,7 @@ typedef struct {
     uint8_t grayscale_bit; // The lowest grayscale bit. Normally 8 - depth.
     bool grayscale;
     bool tricolor;
+    bool sixcolor; // Spectra6 e-ink screens.
     bool sevencolor; // Acep e-ink screens.
     bool pixels_in_byte_share_row;
     bool reverse_pixels_in_byte;
diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c
index 14fbc3341b5ff..748297cc638de 100644
--- a/shared-module/epaperdisplay/EPaperDisplay.c
+++ b/shared-module/epaperdisplay/EPaperDisplay.c
@@ -36,7 +36,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla
     uint16_t write_color_ram_command, bool color_bits_inverted, uint32_t highlight_color,
     const uint8_t *refresh_sequence, uint16_t refresh_sequence_len, mp_float_t refresh_time,
     const mcu_pin_obj_t *busy_pin, bool busy_state, mp_float_t seconds_per_frame,
-    bool chip_select, bool grayscale, bool acep, bool two_byte_sequence_length, bool address_little_endian) {
+    bool chip_select, bool grayscale, bool acep, bool spectra6, bool two_byte_sequence_length, bool address_little_endian) {
     uint16_t color_depth = 1;
     bool core_grayscale = true;
     if (highlight_color != 0x000000) {
@@ -46,9 +46,10 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla
     } else {
         self->core.colorspace.tricolor = false;
     }
-    self->acep = acep;
+    self->acep = acep || spectra6;
+    self->core.colorspace.sixcolor = spectra6;
     self->core.colorspace.sevencolor = acep;
-    if (acep) {
+    if (self->acep) {
         color_depth = 4; // bits. 7 colors + clean
         grayscale = false;
         core_grayscale = false;
@@ -338,7 +339,7 @@ static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay
                 } else if (self->core.colorspace.tricolor) {
                     self->core.colorspace.grayscale = false;
                     displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer);
-                } else if (self->core.colorspace.sevencolor) {
+                } else if (self->core.colorspace.sixcolor || self->core.colorspace.sevencolor) {
                     displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer);
                 }
             } else {

From f54b0d738445457e84e2ca272bacd7e34d6ba875 Mon Sep 17 00:00:00 2001
From: Bernhard Bablok <bablokb@gmx.de>
Date: Wed, 7 May 2025 08:31:58 +0200
Subject: [PATCH 08/10] added whitespace

---
 shared-bindings/epaperdisplay/EPaperDisplay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c
index 4b125370219f6..40a217a65bd1c 100644
--- a/shared-bindings/epaperdisplay/EPaperDisplay.c
+++ b/shared-bindings/epaperdisplay/EPaperDisplay.c
@@ -218,7 +218,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type,
         args[ARG_write_black_ram_command].u_int, args[ARG_black_bits_inverted].u_bool, write_color_ram_command,
         args[ARG_color_bits_inverted].u_bool, highlight_color, refresh_buf, refresh_buf_len, refresh_time,
         busy_pin, args[ARG_busy_state].u_bool, seconds_per_frame,
-        args[ARG_always_toggle_chip_select].u_bool, args[ARG_grayscale].u_bool, args[ARG_advanced_color_epaper].u_bool,args[ARG_spectra6].u_bool,
+        args[ARG_always_toggle_chip_select].u_bool, args[ARG_grayscale].u_bool, args[ARG_advanced_color_epaper].u_bool, args[ARG_spectra6].u_bool,
         two_byte_sequence_length, args[ARG_address_little_endian].u_bool
         );
 

From 0726b1e4730bfe1cef1fd57c3cab578a30f41346 Mon Sep 17 00:00:00 2001
From: Petr Sedlacek <petr@sedlacek.biz>
Date: Fri, 9 May 2025 16:00:36 +0200
Subject: [PATCH 09/10] Update 42. Keebs Frood to version 9

---
 ports/raspberrypi/boards/42keebs_frood/pins.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/ports/raspberrypi/boards/42keebs_frood/pins.c b/ports/raspberrypi/boards/42keebs_frood/pins.c
index 4543625051869..2e7ac22475b22 100644
--- a/ports/raspberrypi/boards/42keebs_frood/pins.c
+++ b/ports/raspberrypi/boards/42keebs_frood/pins.c
@@ -49,6 +49,9 @@ static const mp_rom_map_elem_t board_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_D15), MP_ROM_PTR(&pin_GPIO15) },
     { MP_ROM_QSTR(MP_QSTR_D16), MP_ROM_PTR(&pin_GPIO16) },
 
+    { MP_ROM_QSTR(MP_QSTR_D10), MP_ROM_PTR(&pin_GPIO10) },
+    { MP_ROM_QSTR(MP_QSTR_D11), MP_ROM_PTR(&pin_GPIO11) },
+
     { MP_ROM_QSTR(MP_QSTR_LED), MP_ROM_PTR(&pin_GPIO17) },
     { MP_ROM_QSTR(MP_QSTR_D17), MP_ROM_PTR(&pin_GPIO17) },
     { MP_ROM_QSTR(MP_QSTR_VBUS_SENSE), MP_ROM_PTR(&pin_GPIO19) },

From f219aada1f8b1ec74d2dc95ca8cece234d4a90f9 Mon Sep 17 00:00:00 2001
From: Dan Halbert <halbert@halwitz.org>
Date: Fri, 23 May 2025 10:26:21 -0400
Subject: [PATCH 10/10] shrink some builds

---
 .../boards/adafruit_magtag_2.9_grayscale/mpconfigboard.mk       | 1 +
 ports/espressif/boards/adafruit_qtpy_esp32c3/mpconfigboard.mk   | 1 +
 .../espressif/boards/makergo_esp32c3_supermini/mpconfigboard.mk | 2 ++
 3 files changed, 4 insertions(+)

diff --git a/ports/espressif/boards/adafruit_magtag_2.9_grayscale/mpconfigboard.mk b/ports/espressif/boards/adafruit_magtag_2.9_grayscale/mpconfigboard.mk
index 01b9ec73e9c34..5ab8c995e331c 100644
--- a/ports/espressif/boards/adafruit_magtag_2.9_grayscale/mpconfigboard.mk
+++ b/ports/espressif/boards/adafruit_magtag_2.9_grayscale/mpconfigboard.mk
@@ -10,6 +10,7 @@ CIRCUITPY_ESP_FLASH_FREQ = 80m
 CIRCUITPY_ESP_FLASH_SIZE = 4MB
 
 CIRCUITPY_ESPCAMERA = 0
+CIRCUITPY_PARALLELDISPLAYBUS = 0
 
 # Include these Python libraries in firmware.
 FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_ConnectionManager
diff --git a/ports/espressif/boards/adafruit_qtpy_esp32c3/mpconfigboard.mk b/ports/espressif/boards/adafruit_qtpy_esp32c3/mpconfigboard.mk
index 1f47795002df9..076c3184e93f2 100644
--- a/ports/espressif/boards/adafruit_qtpy_esp32c3/mpconfigboard.mk
+++ b/ports/espressif/boards/adafruit_qtpy_esp32c3/mpconfigboard.mk
@@ -16,3 +16,4 @@ CIRCUITPY_CODEOP = 0
 
 # Not enough pins.
 CIRCUITPY_PARALLELDISPLAYBUS = 0
+CIRCUITPY_RGBMATRIX = 0
diff --git a/ports/espressif/boards/makergo_esp32c3_supermini/mpconfigboard.mk b/ports/espressif/boards/makergo_esp32c3_supermini/mpconfigboard.mk
index 37c27203af31c..69576369d5faf 100644
--- a/ports/espressif/boards/makergo_esp32c3_supermini/mpconfigboard.mk
+++ b/ports/espressif/boards/makergo_esp32c3_supermini/mpconfigboard.mk
@@ -7,6 +7,8 @@ CIRCUITPY_ESP_FLASH_MODE=dio
 CIRCUITPY_ESP_FLASH_FREQ=80m
 CIRCUITPY_ESP_FLASH_SIZE=4MB
 
+CIRCUITPY_CODEOP = 0
+
 CIRCUITPY_LEGACY_4MB_FLASH_LAYOUT = 1
 
 CIRCUITPY_ESP_USB_SERIAL_JTAG = 1