diff --git a/components/bl00mbox/.clang-tidy b/components/bl00mbox/.clang-tidy
new file mode 100644
index 0000000000000000000000000000000000000000..8797ef9f00e0c9fa8a20af5f8ee4e1931ea5b1e4
--- /dev/null
+++ b/components/bl00mbox/.clang-tidy
@@ -0,0 +1 @@
+Checks: '-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-diagnostic-deprecated-declarations,-clang-diagnostic-incompatible-pointer-types'
diff --git a/components/bl00mbox/CMakeLists.txt b/components/bl00mbox/CMakeLists.txt
index cd137209936f0037f5ecf4a97efa1794215f064a..4dfae4a138624c6ea6e1afe6772090cd0b7ccce4 100644
--- a/components/bl00mbox/CMakeLists.txt
+++ b/components/bl00mbox/CMakeLists.txt
@@ -1,10 +1,23 @@
+#SPDX-License-Identifier: CC0-1.0
+
 idf_component_register(
     SRCS
         bl00mbox.c
-        buds/tinysynth/tinysynth.c
+        bl00mbox_audio.c
+        bl00mbox_user.c
+        bl00mbox_plugin_registry.c
+        bl00mbox_radspa_requirements.c
+        plugins/trad_synth/trad_synth.c
+        plugins/ampliverter.c
+        plugins/sampler.c
+        plugins/delay.c
+        plugins/sequencer.c
+        radspa/radspa_helpers.c
     INCLUDE_DIRS
         include
-        buds/tinysynth
+        plugins/trad_synth
+        plugins
+        radspa
     REQUIRES
         st3m
 )
diff --git a/components/bl00mbox/LICENSE b/components/bl00mbox/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..0e259d42c996742e9e3cba14c677129b2c1b6311
--- /dev/null
+++ b/components/bl00mbox/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/components/bl00mbox/README.md b/components/bl00mbox/README.md
index 9e4c8e90abb37fa1a4f8000210878f8e3be80d13..03ae5d16b18a4a80836ee12994b019501c63f4a4 100644
--- a/components/bl00mbox/README.md
+++ b/components/bl00mbox/README.md
@@ -1,7 +1,118 @@
-Welcome to bl00mbox!
+## welcome to bl00mbox!
 
-Plugins are called buds and provided in the radspa (wip) file format.
-To add your own plugin to the compiler add the .c files and also the
-directory of the .h files to ./CMakeLists.txt/.
+bl00mbox is a radspa plugin host that manages rendering as well as routing. it provides several high-level
+features to reduce computational load of inactive sources in complex systems.
 
+## naming conventions
 
+bl00mbox_audio_* functions are to be called only from within the audio task. There are no thread safety
+features.
+
+bl00mbox_tween_* functions are to be called only when the audio task idles. Since the audio task can block
+on them briefly they should not be run at high priority.
+
+## rendering
+The rendering engine produces samples as requested by a host task.
+
+### channels
+On the topmost layer, all plugins are assigned to a channel, representing different patches, applications and
+similar. There is a limited amount of channels as defined by BL00MBOX_CHANNELS where channel 0 is reserved
+for system sounds. The host UI provides applications with a channel number that they can use to freely
+spawn plugin instances. This allows bl00mbox to not render a channel if the application is suspended, or
+free it entirely if the application is closed while allowing for fast application switching without having
+to rebuild plugins and routing structure.
+
+The default policy for the time being is that only the last channel that has received data input as well as
+the system channel 0 are rendered, but different policies such as individual background overrides may be
+implemented.
+
+### buds
+All plugins are wrapped into buds which provide additional information, such as the assigned channel.
+
+### radspa
+Plugins are provided in the custom radspa format as defined in radspa.h. For the current scope of bl00mbox
+all signals such as audio data, pitch or events is represented as int16_t values. While this allows for high
+flexibility with signal routing, this means common signals such as note start/end are encoded. These encodings
+are visible to bl00mbox via radspa signal hints. A basic radspa plugin descriptor with name, description and
+an instance constructor function is provided to the host.
+
+The specific number of signals is dynamic and thus is not part of the descriptor. Each plugin may take one
+initialization variable (for example for numbers of channels in a polysynth or length of a delay buffer).
+Support for multiple initialization variables is planned for the future.
+
+### routing
+Routing is generally slow and is performed in-between buffer generation.
+
+### the render tree
+Each channel has its own render tree. Each node on the tree is a bud. A single bud, often a mixer, is attached
+to the tree and always rendered if the tree is active. Each bud may request signals from other buds during
+rendering, in which case that bud is rendered. For example, the root bud could be an envelope generator that
+receives audio from an oscillator child bud. If the envelope generator is in a "mute" state, the child bud
+is never evaluated, thus providing an early return.
+
+## modifying routing and parameters
+bl00mbox provides a thread safe API for routing and parameter changes.
+
+## known weaknesses
+Due to the nested evaluation of the render tree the stack size may grow large.
+
+### examples
+
+
+run these in ur repl :D!
+
+## bass line
+
+```
+import bl00mbox
+
+c = bl00mbox.Channel()
+c.volume = 10000
+osc1 = c.new_bud(420)
+env1 = c.new_bud(42)
+env1.signals.output.value = c.output
+env1.signals.input.value = osc1.signals.output
+
+osc2 = c.new_bud(420)
+env2 = c.new_bud(42)
+env2.signals.input.value = osc2.signals.output
+
+amp1 = c.new_bud(69)
+amp1.signals.input.value = env2.signals.output
+amp1.signals.output.value = osc1.signals.lin_fm
+
+env1.signals.sustain.value = 0
+env2.signals.sustain.value = 0
+env1.signals.attack.value = 10
+env2.signals.attack.value = 100
+env1.signals.decay.value = 800
+env2.signals.decay.value = 800
+
+osc1.signals.pitch.tone = -12
+osc2.signals.pitch.tone = -24
+
+osc3 = c.new_bud(420)
+osc3.signals.waveform.value = 0
+osc3.signals.pitch.tone = -100
+osc3.signals.output.value = env1.signals.trigger
+osc3.signals.output.value = env2.signals.trigger
+
+osc4 = c.new_bud(420)
+osc4.signals.waveform.value = 32767
+osc4.signals.pitch.tone = -124
+
+amp2 = c.new_bud(69)
+amp2.signals.input.value = osc4.signals.output
+amp2.signals.bias.value = 18376 - 2400 
+amp2.signals.gain.value = 300
+
+amp2.signals.output.value = osc1.signals.pitch
+
+amp3 = c.new_bud(69)
+amp3.signals.input.value = amp2.signals.output
+amp3.signals.bias.value = - 2400
+amp3.signals.gain.value = 31000
+
+amp3.signals.output.value = osc2.signals.pitch
+osc2.signals.output.value = c.output
+```
diff --git a/components/bl00mbox/bl00mbox.c b/components/bl00mbox/bl00mbox.c
index e7d9e2ec60cb1efe56f045409221cfbf66afee61..ade8e30295642a7b1d6ad5bc34a333924b62683b 100644
--- a/components/bl00mbox/bl00mbox.c
+++ b/components/bl00mbox/bl00mbox.c
@@ -1,120 +1,11 @@
-#include <freertos/FreeRTOS.h>
-#include <freertos/queue.h>
-#include <freertos/task.h>
+// SPDX-License-Identifier: CC0-1.0
+#include "bl00mbox.h"
+#include "bl00mbox_audio.h"
+#include "bl00mbox_plugin_registry.h"
 #include "st3m_audio.h"
-#include "st3m_scope.h"
-
-#include <math.h>
-#include <stdio.h>
-#include <string.h>
-
-typedef struct _audio_source_t {
-    void *render_data;
-    int16_t (*render_function)(void *);
-    uint16_t index;
-    struct _audio_source_t *next;
-} audio_source_t;
-
-static audio_source_t *_audio_sources = NULL;
-
-uint16_t bl00mbox_source_add(void *render_data, void *render_function) {
-    // construct audio source struct
-    audio_source_t *src = malloc(sizeof(audio_source_t));
-    if (src == NULL) return 0;
-    src->render_data = render_data;
-    src->render_function = render_function;
-    src->next = NULL;
-    src->index = 0;
-
-    // handle empty list special case
-    if (_audio_sources == NULL) {
-        _audio_sources = src;
-        return 0;  // only nonempty lists from here on out!
-    }
-
-    // searching for lowest unused index
-    audio_source_t *index_source = _audio_sources;
-    while (1) {
-        if (src->index == (index_source->index)) {
-            src->index++;  // someone else has index already, try next
-            index_source = _audio_sources;  // start whole list for new index
-        } else {
-            index_source = index_source->next;
-        }
-        if (index_source == NULL) {  // traversed the entire list
-            break;
-        }
-    }
-
-    audio_source_t *audio_source = _audio_sources;
-    // append new source to linked list
-    while (audio_source != NULL) {
-        if (audio_source->next == NULL) {
-            audio_source->next = src;
-            break;
-        } else {
-            audio_source = audio_source->next;
-        }
-    }
-    return src->index;
-}
-
-void bl00mbox_source_remove(uint16_t index) {
-    audio_source_t *audio_source = _audio_sources;
-    audio_source_t *start_gap = NULL;
-
-    while (audio_source != NULL) {
-        if (index == audio_source->index) {
-            if (start_gap == NULL) {
-                _audio_sources = audio_source->next;
-            } else {
-                start_gap->next = audio_source->next;
-            }
-            vTaskDelay(
-                20 /
-                portTICK_PERIOD_MS);  // give other tasks time to stop using
-            free(audio_source);       // terrible hack tbh
-            break;
-        }
-        start_gap = audio_source;
-        audio_source = audio_source->next;
-    }
-}
-
-uint16_t bl00mbox_sources_count() {
-    uint16_t ret = 0;
-    audio_source_t *audio_source = _audio_sources;
-    while (audio_source != NULL) {
-        audio_source = audio_source->next;
-        ret++;
-    }
-    return ret;
-}
-
-void bl00mbox_player_function(int16_t *rx, int16_t *tx, uint16_t len) {
-    int32_t acc[len];
-    memset(acc, 0, len * sizeof(float));
-    audio_source_t *audio_source = _audio_sources;
-    while (audio_source != NULL) {
-        for (uint16_t i = 0; i < len; i += 2) {
-            acc[i] +=
-                (*(audio_source->render_function))(audio_source->render_data);
-        }
-        audio_source = audio_source->next;
-    }
-
-    for (uint16_t i = 0; i < len; i += 2) {
-        st3m_scope_write((acc[i]) >> 4);
-
-        acc[i] = acc[i] >> 3;
-
-        if (acc[i] > 32767) acc[i] = 32767;
-        if (acc[i] < -32767) acc[i] = -32767;
-        tx[i] = acc[i];
-        tx[i + 1] = acc[i];
-    }
-}
 
 void bl00mbox_init() {
-    st3m_audio_set_player_function(bl00mbox_player_function);
+    bl00mbox_plugin_registry_init();
+    st3m_audio_set_player_function(bl00mbox_audio_render);
+    bl00mbox_channels_init();
 }
diff --git a/components/bl00mbox/bl00mbox_audio.c b/components/bl00mbox/bl00mbox_audio.c
new file mode 100644
index 0000000000000000000000000000000000000000..9cf62fd5207e87a965780a1947fec17e50d142dc
--- /dev/null
+++ b/components/bl00mbox/bl00mbox_audio.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: CC0-1.0
+#include "bl00mbox_audio.h"
+
+static bool is_initialized = false;
+static bool bl00mbox_audio_run = true;
+void bl00mbox_audio_enable() { bl00mbox_audio_run = true; }
+void bl00mbox_audio_disable() { bl00mbox_audio_run = false; }
+
+static uint32_t render_pass_id;
+
+// fixed-length list of channels
+static bl00mbox_channel_t channels[BL00MBOX_CHANNELS];
+static int8_t last_chan_event = 0;
+// foregrounded channel is always rendered
+// note: regardless of settings the system channel 0 is always rendered
+static uint8_t bl00mbox_channel_foreground = 0;
+// channels may request being active while not being in foreground
+static bool bl00mbox_channel_background_mute_override[BL00MBOX_CHANNELS] = {
+    false,
+};
+
+bool bl00mbox_channel_set_background_mute_override(uint8_t channel_index,
+                                                   bool enable) {
+#ifdef BL00MBOX_BACKGROUND_MUTE_OVERRIDE_ENABLE
+    if (channel_index >= BL00MBOX_CHANNELS) return false;
+    bl00mbox_channel_background_mute_override[channel_index] = enable;
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool bl00mbox_channel_get_background_mute_override(uint8_t channel_index) {
+    if (channel_index >= BL00MBOX_CHANNELS) return false;
+    return bl00mbox_channel_background_mute_override[channel_index];
+}
+
+static void** ptr_to_be_set_by_audio_task = NULL;
+static void* ptr_to_be_set_by_audio_task_target = NULL;
+// TODO: multicore thread safety on this boi
+static volatile bool ptr_set_request_pending = false;
+static uint64_t bl00mbox_audio_waitfor_timeout = 0ULL;
+
+bool bl00mbox_audio_waitfor_pointer_change(void** ptr, void* new_val) {
+    if (!is_initialized) return false;
+    /// takes pointer to pointer that is to be set null
+    ptr_to_be_set_by_audio_task = ptr;
+    ptr_to_be_set_by_audio_task_target = new_val;
+    ptr_set_request_pending = true;
+
+    volatile uint64_t timeout = 0;  // cute
+    while (ptr_set_request_pending) {
+        timeout++;
+        // TODO: nop
+        if (bl00mbox_audio_waitfor_timeout &&
+            (timeout = bl00mbox_audio_waitfor_timeout)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool bl00mbox_audio_do_pointer_change() {
+    if (ptr_set_request_pending) {
+        (*ptr_to_be_set_by_audio_task) = ptr_to_be_set_by_audio_task_target;
+        ptr_to_be_set_by_audio_task_target = NULL;
+        ptr_to_be_set_by_audio_task = NULL;
+        ptr_set_request_pending = false;
+        return true;
+    }
+    return false;
+}
+
+void bl00mbox_channel_event(uint8_t chan) { last_chan_event = chan; }
+
+bl00mbox_channel_t* bl00mbox_get_channel(uint8_t index) {
+    if (index >= BL00MBOX_CHANNELS) return NULL;
+    return &(channels[index]);
+}
+
+uint8_t bl00mbox_channel_get_foreground_index() {
+    return bl00mbox_channel_foreground;
+}
+
+void bl00mbox_channel_set_foreground_index(uint8_t index) {
+    if (index >= BL00MBOX_CHANNELS) return;
+    bl00mbox_channel_foreground = index;
+}
+
+uint8_t bl00mbox_channel_get_free_index() {
+    uint8_t ret = 1;
+    for (; ret < BL00MBOX_CHANNELS; ret++) {
+        if (bl00mbox_get_channel(ret)->is_free) {
+            bl00mbox_get_channel(ret)->is_free = false;
+            break;
+        }
+    }
+    last_chan_event = ret;
+    return ret;
+}
+
+void bl00mbox_channels_init() {
+    for (uint8_t i = 0; i < BL00MBOX_CHANNELS; i++) {
+        bl00mbox_channel_t* chan = bl00mbox_get_channel(i);
+        chan->volume = BL00MBOX_DEFAULT_CHANNEL_VOLUME;
+        chan->root_list = NULL;
+        chan->buds = NULL;
+        chan->connections = NULL;
+        chan->is_active = true;
+        chan->is_free = true;
+    }
+    is_initialized = true;
+}
+
+void bl00mbox_channel_enable(uint8_t chan) {
+    if (chan >= (BL00MBOX_CHANNELS)) return;
+    bl00mbox_channel_t* ch = bl00mbox_get_channel(chan);
+    ch->is_active = true;
+}
+
+void bl00mbox_channel_disable(uint8_t chan) {
+    if (chan >= (BL00MBOX_CHANNELS)) return;
+    bl00mbox_channel_t* ch = bl00mbox_get_channel(chan);
+    ch->is_active = false;
+}
+
+void bl00mbox_channel_set_volume(uint8_t chan, uint16_t volume) {
+    if (chan >= (BL00MBOX_CHANNELS)) return;
+    bl00mbox_channel_t* ch = bl00mbox_get_channel(chan);
+    ch->volume = volume < 32767 ? volume : 32767;
+}
+
+int16_t bl00mbox_channel_get_volume(uint8_t chan) {
+    if (chan >= (BL00MBOX_CHANNELS)) return 0;
+    bl00mbox_channel_t* ch = bl00mbox_get_channel(chan);
+    return ch->volume;
+}
+
+void bl00mbox_audio_bud_render(bl00mbox_bud_t* bud, uint16_t num_samples) {
+    if (bud->render_pass_id == render_pass_id) return;
+    bud->plugin->render(bud->plugin, num_samples, render_pass_id);
+    bud->render_pass_id = render_pass_id;
+}
+
+static void bl00mbox_audio_channel_render(bl00mbox_channel_t* chan,
+                                          int16_t* out, uint16_t len,
+                                          bool adding) {
+    if (render_pass_id == chan->render_pass_id) return;
+    chan->render_pass_id = render_pass_id;
+
+    bl00mbox_channel_root_t* root = chan->root_list;
+
+    // early exit when no sources:
+    if ((root == NULL) || (!chan->is_active)) {
+        if (adding) return;                     // nothing to do
+        memset(out, 0, len * sizeof(int16_t));  // mute
+        return;
+    }
+
+    int32_t acc[256];
+    bool first = true;
+
+    while (root != NULL) {
+        bl00mbox_audio_bud_render(root->con->source_bud, len);
+        if (first) {
+            for (uint16_t i = 0; i < len; i++) {
+                acc[i] = root->con->buffer[i];
+            }
+        } else {
+            for (uint16_t i = 0; i < len;
+                 i++) {  // replace this with proper ladspa-style adding
+                         // function someday
+                acc[i] += root->con->buffer[i];
+            }
+        }
+        first = false;
+        root = root->next;
+    }
+
+    if (root == NULL) return;
+    for (uint16_t i = 0; i < len; i++) {
+        if (adding) {
+            out[i] =
+                radspa_add_sat(radspa_mult_shift(acc[i], chan->volume), out[i]);
+        } else {
+            out[i] = radspa_mult_shift(acc[i], chan->volume);
+        }
+    }
+}
+
+void bl00mbox_audio_render(int16_t* rx, int16_t* tx, uint16_t len) {
+    if (!is_initialized) {
+        memset(tx, 0, len * sizeof(int16_t));  // mute
+        return;
+    }
+
+    bl00mbox_audio_do_pointer_change();
+    bl00mbox_channel_foreground = last_chan_event;
+
+    if (!bl00mbox_audio_run) {
+        memset(tx, 0, len * sizeof(int16_t));  // mute
+        return;
+    }
+
+    render_pass_id++;  // fresh pass, all relevant sources must be recomputed
+    uint16_t mono_len = len / 2;
+    int16_t acc[mono_len];
+    // system channel always runs non-adding
+    bl00mbox_audio_channel_render(&(channels[0]), acc, mono_len, 0);
+
+    // re-rendering channels is ok, if render_pass_id didn't change it will just
+    // exit
+    bl00mbox_audio_channel_render(&(channels[bl00mbox_channel_foreground]), acc,
+                                  mono_len, 1);
+
+    // TODO: scales poorly if there's many channels
+#ifdef BL00MBOX_BACKGROUND_MUTE_OVERRIDE_ENABLE
+    for (uint8_t i = 1; i < (BL00MBOX_CHANNELS); i++) {
+        if (bl00mbox_channel_background_mute_override[i]) {
+            bl00mbox_audio_channel_render(&(channels[i]), acc, mono_len, 1);
+        }
+    }
+#endif
+
+    for (uint16_t i = 0; i < mono_len; i++) {
+        st3m_scope_write((acc[i]) >> 4);
+
+        tx[2 * i] = acc[i];
+        tx[2 * i + 1] = acc[i];
+    }
+}
+
+// TEMP
+void bl00mbox_player_function(int16_t* rx, int16_t* tx, uint16_t len) {
+    bl00mbox_audio_render(rx, tx, len);
+}
diff --git a/components/bl00mbox/bl00mbox_plugin_registry.c b/components/bl00mbox/bl00mbox_plugin_registry.c
new file mode 100644
index 0000000000000000000000000000000000000000..f0e0734572c2664142c7def4283dcb806bef5b90
--- /dev/null
+++ b/components/bl00mbox/bl00mbox_plugin_registry.c
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: CC0-1.0
+#include "bl00mbox_plugin_registry.h"
+
+bl00mbox_plugin_registry_t* bl00mbox_plugin_registry = NULL;
+uint16_t bl00mbox_plugin_registry_len = 0;
+bool bl00mbox_plugin_registry_is_initialized = false;
+
+static void plugin_add(radspa_descriptor_t* descriptor) {
+    if (bl00mbox_plugin_registry_len == 65535) {
+        printf("too many plugins registered");
+        abort();
+    }
+
+    // create plugin registry entry
+    bl00mbox_plugin_registry_t* p = malloc(sizeof(bl00mbox_plugin_registry_t));
+    if (p == NULL) {
+        printf("bl00mbox: no memory for plugin list");
+        abort();
+    }
+    p->descriptor = descriptor;
+    p->next = NULL;
+
+    // go to end of list
+    bl00mbox_plugin_registry_t* plast = bl00mbox_plugin_registry;
+    if (plast == NULL) {
+        bl00mbox_plugin_registry = p;
+    } else {
+        while (plast->next != NULL) {
+            plast = plast->next;
+        }
+        plast->next = p;
+    }
+    bl00mbox_plugin_registry_len++;
+}
+
+uint16_t bl00mbox_plugin_registry_get_plugin_num(void) {
+    return bl00mbox_plugin_registry_len;
+}
+
+radspa_descriptor_t* bl00mbox_plugin_registry_get_descriptor_from_id(
+    uint32_t id) {
+    /// searches plugin registry for first descriptor with given id number
+    /// and returns pointer to it. returns NULL if no match is found.
+    bl00mbox_plugin_registry_t* p = bl00mbox_plugin_registry;
+    while (p != NULL) {
+        if (p->descriptor->id == id) break;
+        p = p->next;
+    }
+    if (p != NULL) return p->descriptor;
+    return NULL;
+}
+
+radspa_descriptor_t* bl00mbox_plugin_registry_get_descriptor_from_index(
+    uint32_t index) {
+    /// returns pointer to descriptor of registry entry at given index.
+    /// returns NULL if out of range.
+    if (index >= bl00mbox_plugin_registry_len) return NULL;
+    bl00mbox_plugin_registry_t* p = bl00mbox_plugin_registry;
+    for (uint16_t i = 0; i < index; i++) {
+        p = p->next;
+        if (p == NULL) {
+            printf("bl00mbox: plugin list length error");
+            abort();
+        }
+    }
+    return p->descriptor;
+}
+
+radspa_descriptor_t* bl00mbox_plugin_registry_get_id_from_index(
+    uint32_t index) {
+    /// returns pointer to descriptor of registry entry at given index.
+    /// returns NULL if out of range.
+    if (index >= bl00mbox_plugin_registry_len) return NULL;
+    bl00mbox_plugin_registry_t* p = bl00mbox_plugin_registry;
+    for (uint16_t i = 0; i < index; i++) {
+        p = p->next;
+        if (p == NULL) {
+            printf("bl00mbox: plugin list length error");
+            abort();
+        }
+    }
+    return p->descriptor;
+}
+
+/* REGISTER PLUGINS HERE!
+ * - include .c file to SRCS in bl00mbox/CMakeLists.txt
+ * - include .h file directory to INCLUDE_DIRS in bl00mbox/CMakeLists.txt
+ * - include .h file below
+ * - use plugin_add in bl00mbox_plugin_registry_init as
+ *   exemplified below
+ *
+ * NOTE: the plugin registry linked list is intended to be filled
+ * once at boot time. dynamically adding plugins at runtime may or
+ * may not work (but will call abort() if no memory is available),
+ * removing plugins from the registry at runtime is not intended.
+ */
+
+#include "ampliverter.h"
+#include "delay.h"
+#include "trad_synth.h"
+// #include "filter.h"
+// #include "sequence_timer.h"
+#include "sampler.h"
+#include "sequencer.h"
+void bl00mbox_plugin_registry_init(void) {
+    if (bl00mbox_plugin_registry_is_initialized) return;
+    plugin_add(&trad_osc_desc);
+    plugin_add(&ampliverter_desc);
+    plugin_add(&trad_env_desc);
+    plugin_add(&delay_desc);
+    //    plugin_add(&filter_desc);
+    //    plugin_add(&sequence_timer_desc);
+    plugin_add(&sequencer_desc);
+    plugin_add(&sampler_desc);
+}
diff --git a/components/bl00mbox/bl00mbox_radspa_requirements.c b/components/bl00mbox/bl00mbox_radspa_requirements.c
new file mode 100644
index 0000000000000000000000000000000000000000..5a931e4949877e1ebcfe579f3f5cdfffd7d827c7
--- /dev/null
+++ b/components/bl00mbox/bl00mbox_radspa_requirements.c
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: CC0-1.0
+#include "bl00mbox_radspa_requirements.h"
+
+bool radspa_host_request_buffer_render(int16_t *buf, uint16_t num_samples) {
+    bl00mbox_bud_t *bud = ((bl00mbox_connection_t *)buf)->source_bud;
+    bl00mbox_audio_bud_render(bud, num_samples);
+    return 1;
+}
+
+// py: bigtable = [int(22/200*(2**(14-5+8+x*4096/2400/64))) for x in range(64)]
+// for 48kHz main sample rate
+static const uint16_t bigtable[64] = {
+    14417, 14686, 14960, 15240, 15524, 15813, 16108, 16409, 16715, 17027, 17345,
+    17668, 17998, 18334, 18676, 19024, 19379, 19741, 20109, 20484, 20866, 21255,
+    21652, 22056, 22467, 22887, 23313, 23748, 24191, 24643, 25103, 25571, 26048,
+    26534, 27029, 27533, 28047, 28570, 29103, 29646, 30199, 30763, 31336, 31921,
+    32517, 33123, 33741, 34371, 35012, 35665, 36330, 37008, 37699, 38402, 39118,
+    39848, 40592, 41349, 42120, 42906, 43706, 44522, 45352, 46199
+};
+
+// py: smoltable = [int(22/240*(2**(15-5+9+x*4096/2400/64/64))) for x in
+// range(64)] for 48kHz main sample rate
+static const uint16_t smoltable[64] = {
+    48059, 48073, 48087, 48101, 48115, 48129, 48143, 48156, 48170, 48184, 48198,
+    48212, 48226, 48240, 48254, 48268, 48282, 48296, 48310, 48324, 48338, 48352,
+    48366, 48380, 48394, 48407, 48421, 48435, 48449, 48463, 48477, 48491, 48505,
+    48519, 48533, 48548, 48562, 48576, 48590, 48604, 48618, 48632, 48646, 48660,
+    48674, 48688, 48702, 48716, 48730, 48744, 48758, 48772, 48786, 48801, 48815,
+    48829, 48843, 48857, 48871, 48885, 48899, 48913, 48928, 48942,
+};
+
+uint32_t radspa_sct_to_rel_freq(int16_t sct, int16_t undersample_pow) {
+    /// returns approx. proportional to 2**((sct/2400) + undersample_pow) so
+    /// that a uint32_t accumulator overflows at 440Hz with sct = INT16_MAX -
+    /// 6*2400 when sampled at (48>>undersample_pow)kHz
+
+    // compiler explorer says this is 33 instructions with O2. might be alright?
+    uint32_t a = sct;
+    a = sct + 28 * 2400 - 32767 - 330;
+    // at O2 u get free division for each modulo. still slow, 10 instructions or
+    // so.
+    int16_t octa = a / 2400;
+    a = a % 2400;
+    uint8_t bigindex = a / 64;
+    uint8_t smolindex = a % 64;
+
+    uint32_t ret = 2;  // weird but trust us
+    ret *= bigtable[bigindex];
+    ret *= smoltable[smolindex];
+
+    int16_t shift = 27 - octa - undersample_pow;
+    if (shift > 0) {
+        ret = ret >> shift;
+    }
+    return ret;
+}
+
+int16_t radspa_clip(int32_t a) {
+    if (a > 32767) {
+        return 32767;
+    } else if (a < -32767) {
+        return -32767;
+    }
+    return a;
+}
+
+int16_t radspa_add_sat(int32_t a, int32_t b) { return radspa_clip(a + b); }
+int16_t radspa_mult_shift(int32_t a, int32_t b) {
+    return radspa_clip((a * b) >> 15);
+}
+
+int16_t radspa_trigger_start(int16_t velocity, int16_t *hist) {
+    int16_t ret = ((*hist) > 0) ? -velocity : velocity;
+    (*hist) = ret;
+    return ret;
+}
+
+int16_t radspa_trigger_stop(int16_t *hist) {
+    (*hist) = 0;
+    return 0;
+}
+
+int16_t radspa_trigger_get(int16_t trigger_signal, int16_t *hist) {
+    int16_t ret = 0;
+    if ((!trigger_signal) && (*hist)) {  // stop
+        ret = -1;
+    } else if (trigger_signal > 0) {
+        if ((*hist) <= 0) ret = trigger_signal;
+    } else if (trigger_signal < 0) {
+        if ((*hist) >= 0) ret = -trigger_signal;
+    }
+    (*hist) = trigger_signal;
+    return ret;
+}
diff --git a/components/bl00mbox/bl00mbox_user.c b/components/bl00mbox/bl00mbox_user.c
new file mode 100644
index 0000000000000000000000000000000000000000..2e7afb9d894cec2be3ee491f3136c2fbb3c537e0
--- /dev/null
+++ b/components/bl00mbox/bl00mbox_user.c
@@ -0,0 +1,839 @@
+// SPDX-License-Identifier: CC0-1.0
+#include "bl00mbox_user.h"
+
+static uint64_t bl00mbox_bud_index = 1;
+bl00mbox_bud_t *bl00mbox_channel_get_bud_by_index(uint8_t channel,
+                                                  uint32_t index);
+
+uint16_t bl00mbox_channel_buds_num(uint8_t channel) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    uint16_t ret = 0;
+    if (chan->buds != NULL) {
+        bl00mbox_bud_t *last = chan->buds;
+        ret++;
+        while (last->chan_next != NULL) {
+            last = last->chan_next;
+            ret++;
+        }
+    }
+    return ret;
+}
+
+uint64_t bl00mbox_channel_get_bud_by_list_pos(uint8_t channel, uint32_t pos) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    uint16_t ret = 0;
+    if (chan->buds != NULL) {
+        bl00mbox_bud_t *last = chan->buds;
+        if (pos == ret) return last->index;
+        ret++;
+        while (last->chan_next != NULL) {
+            last = last->chan_next;
+            if (pos == ret) return last->index;
+            ret++;
+        }
+    }
+    return 0;
+}
+
+uint16_t bl00mbox_channel_conns_num(uint8_t channel) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    uint16_t ret = 0;
+    if (chan->connections != NULL) {
+        bl00mbox_connection_t *last = chan->connections;
+        ret++;
+        while (last->chan_next != NULL) {
+            last = last->chan_next;
+            ret++;
+        }
+    }
+    return ret;
+}
+
+uint16_t bl00mbox_channel_mixer_num(uint8_t channel) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    uint16_t ret = 0;
+    if (chan->root_list != NULL) {
+        bl00mbox_channel_root_t *last = chan->root_list;
+        ret++;
+        while (last->next != NULL) {
+            last = last->next;
+            ret++;
+        }
+    }
+    return ret;
+}
+
+uint64_t bl00mbox_channel_get_bud_by_mixer_list_pos(uint8_t channel,
+                                                    uint32_t pos) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    uint16_t ret = 0;
+    if (chan->buds != NULL) {
+        bl00mbox_channel_root_t *last = chan->root_list;
+        if (pos == ret) return last->con->source_bud->index;
+        ret++;
+        while (last->next != NULL) {
+            last = last->next;
+            if (pos == ret) return last->con->source_bud->index;
+            ret++;
+        }
+    }
+    return 0;
+}
+
+uint32_t bl00mbox_channel_get_signal_by_mixer_list_pos(uint8_t channel,
+                                                       uint32_t pos) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    uint16_t ret = 0;
+    if (chan->buds != NULL) {
+        bl00mbox_channel_root_t *last = chan->root_list;
+        if (pos == ret) return last->con->signal_index;
+        ret++;
+        while (last->next != NULL) {
+            last = last->next;
+            if (pos == ret) return last->con->signal_index;
+            ret++;
+        }
+    }
+    return 0;
+}
+
+uint16_t bl00mbox_channel_subscriber_num(uint8_t channel, uint64_t bud_index,
+                                         uint16_t signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return 0;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return 0;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, signal_index);
+    if (sig == NULL) return 0;
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)sig->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return 0;
+
+    uint16_t ret = 0;
+    if (conn->subs != NULL) {
+        bl00mbox_connection_subscriber_t *last = conn->subs;
+        ret++;
+        while (last->next != NULL) {
+            last = last->next;
+            ret++;
+        }
+    }
+    return ret;
+}
+
+uint64_t bl00mbox_channel_get_bud_by_subscriber_list_pos(uint8_t channel,
+                                                         uint64_t bud_index,
+                                                         uint16_t signal_index,
+                                                         uint8_t pos) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return 0;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return 0;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, signal_index);
+    if (sig == NULL) return 0;
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)sig->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return 0;
+
+    uint16_t ret = 0;
+    if (conn->subs != NULL) {
+        bl00mbox_connection_subscriber_t *last = conn->subs;
+        if (pos == ret) return (last->type == 0) ? last->bud_index : 0;
+        ret++;
+        while (last->next != NULL) {
+            last = last->next;
+            if (pos == ret) return (last->type == 0) ? last->bud_index : 0;
+            ret++;
+        }
+    }
+    return 0;
+}
+
+int32_t bl00mbox_channel_get_signal_by_subscriber_list_pos(
+    uint8_t channel, uint64_t bud_index, uint16_t signal_index, uint8_t pos) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return 0;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return 0;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, signal_index);
+    if (sig == NULL) return 0;
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)sig->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return 0;
+
+    uint16_t ret = 0;
+    if (conn->subs != NULL) {
+        bl00mbox_connection_subscriber_t *last = conn->subs;
+        if (pos == ret) return (last->type == 0) ? last->signal_index : -1;
+        ret++;
+        while (last->next != NULL) {
+            last = last->next;
+            if (pos == ret) return (last->type == 0) ? last->signal_index : -1;
+            ret++;
+        }
+    }
+    return 0;
+}
+
+uint64_t bl00mbox_channel_get_source_bud(uint8_t channel, uint64_t bud_index,
+                                         uint16_t signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return 0;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return 0;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, signal_index);
+    if (sig == NULL) return 0;
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)sig->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return 0;
+    return conn->source_bud->index;
+}
+
+uint16_t bl00mbox_channel_get_source_signal(uint8_t channel, uint64_t bud_index,
+                                            uint16_t signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return 0;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return 0;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, signal_index);
+    if (sig == NULL) return 0;
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)sig->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return 0;
+    return conn->signal_index;
+}
+
+static bl00mbox_connection_t *create_connection(uint8_t channel) {
+    bl00mbox_connection_t *ret = malloc(sizeof(bl00mbox_connection_t));
+    if (ret == NULL) return NULL;
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    ret->chan_next = NULL;
+    ret->subs = NULL;
+    ret->channel = channel;
+
+    if (chan->connections != NULL) {
+        bl00mbox_connection_t *last = chan->connections;
+        while (last->chan_next != NULL) {
+            last = last->chan_next;
+        }
+        last->chan_next = ret;
+    } else {
+        chan->connections = ret;
+    }
+    return ret;
+}
+
+static bool weak_delete_connection(bl00mbox_connection_t *conn) {
+    if (conn->subs != NULL) return false;
+
+    // nullify source bud connection;
+    bl00mbox_bud_t *bud = conn->source_bud;
+    if (bud != NULL) {
+        radspa_signal_t *tx =
+            radspa_signal_get_by_index(bud->plugin, conn->signal_index);
+        if (tx != NULL) {
+            bl00mbox_audio_waitfor_pointer_change(&(tx->buffer), NULL);
+        }
+    }
+
+    // pop from channel list
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(conn->channel);
+    if (chan->connections != NULL) {
+        if (chan->connections != conn) {
+            bl00mbox_connection_t *prev = chan->connections;
+            while (prev->chan_next != conn) {
+                prev = prev->chan_next;
+                if (prev->chan_next == NULL) {
+                    break;
+                }
+            }
+            if (prev->chan_next != NULL)
+                bl00mbox_audio_waitfor_pointer_change(&(prev->chan_next),
+                                                      conn->chan_next);
+        } else {
+            bl00mbox_audio_waitfor_pointer_change(&(chan->connections),
+                                                  conn->chan_next);
+        }
+    }
+
+    free(conn);
+    return true;
+}
+
+bl00mbox_bud_t *bl00mbox_channel_get_bud_by_index(uint8_t channel,
+                                                  uint32_t index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return NULL;
+    if (chan->buds == NULL) return NULL;
+    bl00mbox_bud_t *bud = chan->buds;
+    while (true) {
+        if (bud->index == index) break;
+        bud = bud->chan_next;
+        if (bud == NULL) break;
+    }
+    return bud;
+}
+
+bl00mbox_bud_t *bl00mbox_channel_new_bud(uint8_t channel, uint32_t id,
+                                         uint32_t init_var) {
+    /// creates a new bud instance of the plugin with descriptor id "id" and the
+    /// initialization variable "init_var" and appends it to the plugin list of
+    /// the corresponding channel. returns pointer to the bud if successfull,
+    /// else NULL.
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return NULL;
+    radspa_descriptor_t *desc =
+        bl00mbox_plugin_registry_get_descriptor_from_id(id);
+    if (desc == NULL) return NULL;
+    bl00mbox_bud_t *bud = malloc(sizeof(bl00mbox_bud_t));
+    if (bud == NULL) return NULL;
+    radspa_t *plugin = desc->create_plugin_instance(init_var);
+    if (plugin == NULL) {
+        free(bud);
+        return NULL;
+    }
+
+    bud->plugin = plugin;
+    bud->channel = channel;
+    // TODO: look for empty indices
+    bud->index = bl00mbox_bud_index;
+    bl00mbox_bud_index++;
+    bud->chan_next = NULL;
+
+    // append to channel bud list
+    if (chan->buds == NULL) {
+        chan->buds = bud;
+    } else {
+        bl00mbox_bud_t *last = chan->buds;
+        while (last->chan_next != NULL) {
+            last = last->chan_next;
+        }
+        last->chan_next = bud;
+    }
+    bl00mbox_channel_event(channel);
+    return bud;
+}
+
+bool bl00mbox_channel_delete_bud(uint8_t channel, uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+
+    // disconnect all signals
+    uint16_t num_signals =
+        bl00mbox_channel_bud_get_num_signals(channel, bud_index);
+    for (uint16_t i = 0; i < num_signals; i++) {
+        bl00mbox_channel_disconnect_signal(channel, bud_index, i);
+    }
+
+    // pop from channel bud list
+    bl00mbox_bud_t *seek = chan->buds;
+    bool free_later = false;
+    if (chan->buds != NULL) {
+        bl00mbox_bud_t *prev = NULL;
+        while (seek != NULL) {
+            if (seek->index == bud_index) {
+                break;
+            }
+            prev = seek;
+            seek = seek->chan_next;
+        }
+        if (seek != NULL) {
+            if (prev != NULL) {
+                bl00mbox_audio_waitfor_pointer_change(&(prev->chan_next),
+                                                      seek->chan_next);
+            } else {
+                bl00mbox_audio_waitfor_pointer_change(&(chan->buds),
+                                                      seek->chan_next);
+            }
+            free_later = true;
+        }
+    }
+
+    bud->plugin->descriptor->destroy_plugin_instance(bud->plugin);
+    if (free_later) free(seek);
+    return true;
+}
+
+bool bl00mbox_channel_clear(uint8_t channel) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = chan->buds;
+    while (bud != NULL) {
+        bl00mbox_bud_t *bud_next = bud->chan_next;
+        bl00mbox_channel_delete_bud(channel, bud->index);
+        bud = bud_next;
+    }
+    return true;
+}
+
+bool bl00mbox_channel_connect_signal_to_output_mixer(
+    uint8_t channel, uint32_t bud_index, uint32_t bud_signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *tx =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (tx == NULL) return false;
+    if (!(tx->hints & RADSPA_SIGNAL_HINT_OUTPUT)) return false;
+
+    bl00mbox_channel_root_t *root = malloc(sizeof(bl00mbox_channel_root_t));
+    if (root == NULL) return false;
+    bl00mbox_connection_subscriber_t *sub =
+        malloc(sizeof(bl00mbox_connection_subscriber_t));
+    if (sub == NULL) {
+        free(root);
+        return false;
+    }
+
+    bl00mbox_connection_t *conn;
+    if (tx->buffer == NULL) {  // doesn't feed a buffer yet
+        conn = create_connection(channel);
+        if (conn == NULL) {
+            free(sub);
+            free(root);
+            return false;
+        }
+        // set up new connection
+        conn->signal_index = bud_signal_index;
+        conn->source_bud = bud;
+        tx->buffer = conn->buffer;
+    } else {
+        conn = (bl00mbox_connection_t *)
+                   tx->buffer;  // buffer sits on top of struct
+        if (conn->subs != NULL) {
+            bl00mbox_connection_subscriber_t *seek = conn->subs;
+            while (seek != NULL) {
+                if (seek->type == 1) {
+                    free(root);
+                    free(sub);
+                    return false;  // already connected
+                }
+                seek = seek->next;
+            }
+        }
+    }
+
+    sub->type = 1;
+    sub->bud_index = bud_index;
+    sub->signal_index = bud_signal_index;
+    sub->next = NULL;
+    if (conn->subs == NULL) {
+        conn->subs = sub;
+    } else {
+        bl00mbox_connection_subscriber_t *seek = conn->subs;
+        while (seek->next != NULL) {
+            seek = seek->next;
+        }
+        seek->next = sub;
+    }
+
+    root->con = conn;
+    root->next = NULL;
+
+    if (chan->root_list == NULL) {
+        chan->root_list = root;
+    } else {
+        bl00mbox_channel_root_t *last_root = chan->root_list;
+        while (last_root->next != NULL) {
+            last_root = last_root->next;
+        }
+        last_root->next = root;
+    }
+
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+bool bl00mbox_channel_disconnect_signal_from_output_mixer(
+    uint8_t channel, uint32_t bud_index, uint32_t bud_signal_index) {
+    // TODO
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *tx =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (tx == NULL) return false;
+    if (tx->buffer == NULL) return false;
+    if (!(tx->hints & RADSPA_SIGNAL_HINT_OUTPUT)) return false;
+
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)tx->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return false;           // not connected
+
+    bl00mbox_channel_root_t *rt = chan->root_list;
+    bl00mbox_channel_root_t *rt_prev = NULL;
+
+    while (rt != NULL) {
+        if (rt->con == conn) break;
+        rt_prev = rt;
+        rt = rt->next;
+    }
+    if (rt != NULL) {
+        if (rt_prev == NULL) {
+            bl00mbox_audio_waitfor_pointer_change(&(chan->root_list), rt->next);
+        } else {
+            bl00mbox_audio_waitfor_pointer_change(&(rt_prev->next), rt->next);
+        }
+        free(rt);
+    }
+
+    if (conn->subs != NULL) {
+        bl00mbox_connection_subscriber_t *seek = conn->subs;
+        bl00mbox_connection_subscriber_t *prev = NULL;
+        while (seek != NULL) {
+            if (seek->type == 1) {
+                break;
+            }
+            prev = seek;
+            seek = seek->next;
+        }
+        if (seek != NULL) {
+            if (prev != NULL) {
+                bl00mbox_audio_waitfor_pointer_change(&(prev->next),
+                                                      seek->next);
+            } else {
+                bl00mbox_audio_waitfor_pointer_change(&(conn->subs),
+                                                      seek->next);
+            }
+            free(seek);
+        }
+    }
+
+    weak_delete_connection(conn);
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+bool bl00mbox_channel_disconnect_signal_rx(uint8_t channel,
+                                           uint32_t bud_rx_index,
+                                           uint32_t bud_rx_signal_index) {
+    bl00mbox_bud_t *bud_rx =
+        bl00mbox_channel_get_bud_by_index(channel, bud_rx_index);
+    if (bud_rx == NULL) return false;  // bud index doesn't exist
+
+    radspa_signal_t *rx =
+        radspa_signal_get_by_index(bud_rx->plugin, bud_rx_signal_index);
+    if (rx == NULL) return false;  // signal index doesn't exist
+    if (rx->buffer == NULL) return false;
+    if (!(rx->hints & RADSPA_SIGNAL_HINT_INPUT)) return false;
+
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)rx->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return false;           // not connected
+
+    bl00mbox_bud_t *bud_tx = conn->source_bud;
+    if (bud_tx == NULL) return false;  // bud index doesn't exist
+    radspa_signal_t *tx =
+        radspa_signal_get_by_index(bud_tx->plugin, conn->signal_index);
+    if (tx == NULL) return false;  // signal index doesn't exist
+
+    bl00mbox_audio_waitfor_pointer_change(&(rx->buffer), NULL);
+
+    if (conn->subs != NULL) {
+        bl00mbox_connection_subscriber_t *seek = conn->subs;
+        bl00mbox_connection_subscriber_t *prev = NULL;
+        while (seek != NULL) {
+            if ((seek->signal_index == bud_rx_signal_index) &&
+                (seek->bud_index == bud_rx_index) && (seek->type == 0)) {
+                break;
+            }
+            prev = seek;
+            seek = seek->next;
+        }
+        if (seek != NULL) {
+            if (prev != NULL) {
+                bl00mbox_audio_waitfor_pointer_change(&(prev->next),
+                                                      seek->next);
+            } else {
+                bl00mbox_audio_waitfor_pointer_change(&(conn->subs),
+                                                      seek->next);
+            }
+            free(seek);
+        }
+    }
+
+    weak_delete_connection(conn);
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+bool bl00mbox_channel_disconnect_signal_tx(uint8_t channel,
+                                           uint32_t bud_tx_index,
+                                           uint32_t bud_tx_signal_index) {
+    bl00mbox_bud_t *bud_tx =
+        bl00mbox_channel_get_bud_by_index(channel, bud_tx_index);
+    if (bud_tx == NULL) return false;  // bud index doesn't exist
+
+    radspa_signal_t *tx =
+        radspa_signal_get_by_index(bud_tx->plugin, bud_tx_signal_index);
+    if (tx == NULL) return false;  // signal index doesn't exist
+    if (tx->buffer == NULL) return false;
+    if (!(tx->hints & RADSPA_SIGNAL_HINT_OUTPUT)) return false;
+
+    bl00mbox_connection_t *conn =
+        (bl00mbox_connection_t *)tx->buffer;  // buffer sits on top of struct
+    if (conn == NULL) return false;           // not connected
+
+    while (conn->subs != NULL) {
+        switch (conn->subs->type) {
+            case 0:
+                bl00mbox_channel_disconnect_signal_rx(
+                    channel, conn->subs->bud_index, conn->subs->signal_index);
+                break;
+            case 1:
+                bl00mbox_channel_disconnect_signal_from_output_mixer(
+                    channel, conn->subs->bud_index, conn->subs->signal_index);
+                break;
+        }
+    }
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+bool bl00mbox_channel_disconnect_signal(uint8_t channel, uint32_t bud_index,
+                                        uint32_t signal_index) {
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;  // bud index doesn't exist
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, signal_index);
+    if (sig == NULL) return false;  // signal index doesn't exist
+    if (sig->buffer == NULL) return false;
+
+    bl00mbox_channel_disconnect_signal_rx(channel, bud_index, signal_index);
+    bl00mbox_channel_disconnect_signal_tx(channel, bud_index, signal_index);
+    bl00mbox_channel_disconnect_signal_from_output_mixer(channel, bud_index,
+                                                         signal_index);
+    if (sig->buffer == NULL) return true;
+    return false;
+}
+
+bool bl00mbox_channel_connect_signal(uint8_t channel, uint32_t bud_rx_index,
+                                     uint32_t bud_rx_signal_index,
+                                     uint32_t bud_tx_index,
+                                     uint32_t bud_tx_signal_index) {
+    bl00mbox_bud_t *bud_rx =
+        bl00mbox_channel_get_bud_by_index(channel, bud_rx_index);
+    bl00mbox_bud_t *bud_tx =
+        bl00mbox_channel_get_bud_by_index(channel, bud_tx_index);
+    if (bud_tx == NULL || bud_rx == NULL)
+        return false;  // bud index doesn't exist
+
+    radspa_signal_t *rx =
+        radspa_signal_get_by_index(bud_rx->plugin, bud_rx_signal_index);
+    radspa_signal_t *tx =
+        radspa_signal_get_by_index(bud_tx->plugin, bud_tx_signal_index);
+    if (tx == NULL || rx == NULL) return false;  // signal index doesn't exist
+    if (!(rx->hints & RADSPA_SIGNAL_HINT_INPUT)) return false;
+    if (!(tx->hints & RADSPA_SIGNAL_HINT_OUTPUT)) return false;
+
+    bl00mbox_connection_t *conn;
+    bl00mbox_connection_subscriber_t *sub;
+    if (tx->buffer == NULL) {  // doesn't feed a buffer yet
+        conn = create_connection(channel);
+        if (conn == NULL) return false;  // no ram for connection
+        // set up new connection
+        conn->signal_index = bud_tx_signal_index;
+        conn->source_bud = bud_tx;
+        tx->buffer = conn->buffer;
+    } else {
+        if (rx->buffer == tx->buffer) return false;  // already connected
+        conn = (bl00mbox_connection_t *)
+                   tx->buffer;  // buffer sits on top of struct
+    }
+
+    bl00mbox_channel_disconnect_signal_rx(channel, bud_rx_index,
+                                          bud_rx_signal_index);
+
+    sub = malloc(sizeof(bl00mbox_connection_subscriber_t));
+    if (sub == NULL) {
+        weak_delete_connection(conn);
+        return false;
+    }
+    sub->type = 0;
+    sub->bud_index = bud_rx_index;
+    sub->signal_index = bud_rx_signal_index;
+    sub->next = NULL;
+    if (conn->subs == NULL) {
+        conn->subs = sub;
+    } else {
+        bl00mbox_connection_subscriber_t *seek = conn->subs;
+        while (seek->next != NULL) {
+            seek = seek->next;
+        }
+        seek->next = sub;
+    }
+
+    rx->buffer = tx->buffer;
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+bool bl00mbox_channel_bud_exists(uint8_t channel, uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) {
+        return false;
+    } else {
+        return true;
+    }
+}
+
+char *bl00mbox_channel_bud_get_name(uint8_t channel, uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    return bud->plugin->descriptor->name;
+}
+
+char *bl00mbox_channel_bud_get_description(uint8_t channel,
+                                           uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    return bud->plugin->descriptor->description;
+}
+
+uint32_t bl00mbox_channel_bud_get_plugin_id(uint8_t channel,
+                                            uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    return bud->plugin->descriptor->id;
+}
+
+uint16_t bl00mbox_channel_bud_get_num_signals(uint8_t channel,
+                                              uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    return bud->plugin->len_signals;
+}
+
+char *bl00mbox_channel_bud_get_signal_name(uint8_t channel, uint32_t bud_index,
+                                           uint32_t bud_signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (sig == NULL) return false;
+    return sig->name;
+}
+
+char *bl00mbox_channel_bud_get_signal_description(uint8_t channel,
+                                                  uint32_t bud_index,
+                                                  uint32_t bud_signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (sig == NULL) return false;
+    return sig->description;
+}
+
+char *bl00mbox_channel_bud_get_signal_unit(uint8_t channel, uint32_t bud_index,
+                                           uint32_t bud_signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (sig == NULL) return false;
+    return sig->unit;
+}
+
+bool bl00mbox_channel_bud_set_signal_value(uint8_t channel, uint32_t bud_index,
+                                           uint32_t bud_signal_index,
+                                           int16_t value) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (sig == NULL) return false;
+
+    sig->value = value;
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+int16_t bl00mbox_channel_bud_get_signal_value(uint8_t channel,
+                                              uint32_t bud_index,
+                                              uint32_t bud_signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (sig == NULL) return false;
+
+    return sig->value;
+}
+
+uint32_t bl00mbox_channel_bud_get_signal_hints(uint8_t channel,
+                                               uint32_t bud_index,
+                                               uint32_t bud_signal_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    radspa_signal_t *sig =
+        radspa_signal_get_by_index(bud->plugin, bud_signal_index);
+    if (sig == NULL) return false;
+
+    return sig->hints;
+}
+
+bool bl00mbox_channel_bud_set_table_value(uint8_t channel, uint32_t bud_index,
+                                          uint32_t table_index, int16_t value) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    if (bud->plugin->plugin_table == NULL) return false;
+    if (table_index >= bud->plugin->plugin_table_len) return false;
+    bud->plugin->plugin_table[table_index] = value;
+    bl00mbox_channel_event(channel);
+    return true;
+}
+
+int16_t bl00mbox_channel_bud_get_table_value(uint8_t channel,
+                                             uint32_t bud_index,
+                                             uint32_t table_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    if (bud->plugin->plugin_table == NULL) return false;
+    if (table_index >= bud->plugin->plugin_table_len) return false;
+    return bud->plugin->plugin_table[table_index];
+}
+
+int16_t bl00mbox_channel_bud_get_table_len(uint8_t channel,
+                                           uint32_t bud_index) {
+    bl00mbox_channel_t *chan = bl00mbox_get_channel(channel);
+    if (chan == NULL) return false;
+    bl00mbox_bud_t *bud = bl00mbox_channel_get_bud_by_index(channel, bud_index);
+    if (bud == NULL) return false;
+    return bud->plugin->plugin_table_len;
+}
diff --git a/components/bl00mbox/buds/tinysynth/tinysynth.c b/components/bl00mbox/buds/tinysynth/tinysynth.c
deleted file mode 100644
index ee0fc1c2d0e19a2b43ad289a66e4f933719142c1..0000000000000000000000000000000000000000
--- a/components/bl00mbox/buds/tinysynth/tinysynth.c
+++ /dev/null
@@ -1,210 +0,0 @@
-#include "tinysynth.h"
-#include <math.h>
-
-#define SYNTH_UNDERSAMPLING 1
-#define SYNTH_SAMPLE_RATE ((SAMPLE_RATE) / (SYNTH_UNDERSAMPLING))
-
-int16_t waveshaper(uint8_t shape, int16_t in);
-int16_t nes_noise(uint16_t* reg, uint8_t mode, uint8_t run);
-
-int16_t run_trad_env(trad_env_t* env) {
-    uint32_t tmp;
-    switch (env->env_phase) {
-        case TRAD_ENV_PHASE_OFF:
-            env->env_counter = 0;
-            ;
-            break;
-        case TRAD_ENV_PHASE_ATTACK:
-            tmp = env->env_counter + env->attack;
-            if (tmp < env->env_counter) {  // overflow
-                tmp = ~((uint32_t)0);      // max out
-                env->env_phase = TRAD_ENV_PHASE_DECAY;
-            }
-            env->env_counter = tmp;
-            break;
-        case TRAD_ENV_PHASE_DECAY:
-            tmp = env->env_counter - env->decay;
-            if (tmp > env->env_counter) {  // underflow
-                tmp = 0;                   // bottom out
-            }
-            env->env_counter = tmp;
-
-            if (env->env_counter <= env->sustain) {
-                env->env_counter = env->sustain;
-                env->env_phase = TRAD_ENV_PHASE_SUSTAIN;
-            }
-            break;
-        case TRAD_ENV_PHASE_SUSTAIN:
-            if (env->sustain == 0) env->env_phase = TRAD_ENV_PHASE_OFF;
-            env->env_counter = env->sustain;
-            break;
-        case TRAD_ENV_PHASE_RELEASE:
-            tmp = env->env_counter - env->release;
-            if (tmp > env->env_counter) {  // underflow
-                tmp = 0;                   // bottom out
-                env->env_phase = TRAD_ENV_PHASE_OFF;
-            }
-            env->env_counter = tmp;
-            break;
-    }
-    return env->env_counter >> 17;
-}
-
-int16_t run_trad_osc(trad_osc_t* osc) {
-    osc->undersampling_counter =
-        (osc->undersampling_counter + 1) % SYNTH_UNDERSAMPLING;
-    if (osc->undersampling_counter) return osc->prev_output;
-
-    int32_t ret;  // lil bit buffer for operations
-
-    int32_t env = run_trad_env(&(osc->env));
-    if (osc->env.env_phase == TRAD_ENV_PHASE_OFF) {
-        osc->counter = ((uint64_t)1) << 63;
-        return 0;
-    }
-
-    // run core sawtooth
-    // uint64_t incr = osc->freq * osc->bend;
-    uint64_t incr = osc->freq;
-    osc->counter += incr;
-
-    osc->overflow_event =
-        osc->counter_prev > osc->counter;  // no neg f linfm for now
-    osc->counter_prev = osc->counter;
-
-    if (osc->waveform >= 7) {
-        ret = nes_noise(&(osc->noise_reg), osc->waveform == 7,
-                        osc->overflow_event);
-    } else {
-        // apply waveshaper
-        int32_t tmp = (osc->counter) >> (33 + 16);
-        tmp *= 2;
-        tmp -= 32767;
-        ret = waveshaper(osc->waveform, tmp);
-    }
-
-    // apply volume
-    ret = (ret * env) >> 15;
-    ret = (ret * osc->vol) >> 15;
-    osc->prev_output = ret;
-    return ret;
-}
-
-int16_t nes_noise(uint16_t* reg, uint8_t mode, uint8_t run) {
-    if (run) {
-        uint8_t fb = *reg;
-        if (mode) {
-            fb = fb >> 6;
-        } else {
-            fb = fb >> 1;
-        }
-        fb = (fb ^ (*reg)) & 1;
-        *reg = (*reg >> 1);
-        *reg = (*reg) | (((uint16_t)fb) << 14);
-    }
-    return ((int16_t)(((*reg) & 1)) * 2 - 1) * 32767;
-}
-
-int16_t fake_square(int16_t triangle, int16_t pwm, int16_t gain) {
-    // max gain (1<<14)-1
-    int32_t tmp = triangle;
-    tmp += pwm;
-    tmp *= gain;
-    if (tmp > 32767) tmp = 32767;
-    if (tmp < -32767) tmp = -32767;
-    return tmp;
-}
-
-int16_t waveshaper(uint8_t shape, int16_t in) {
-    int32_t tmp = 0;
-    switch (shape) {
-        case TRAD_OSC_WAVE_SINE:  // TODO: implement proper sine
-        case TRAD_OSC_WAVE_FAKE_SINE:
-            tmp = waveshaper(TRAD_OSC_WAVE_TRI, in);
-            if (tmp > 0.) {
-                tmp = 32767 - tmp;
-                tmp = (tmp * tmp) >> 15;
-                tmp = 32767. - tmp;
-            } else {
-                tmp = 32767 + tmp;
-                tmp = (tmp * tmp) >> 15;
-                tmp = tmp - 32767.;
-            }
-            break;
-        case TRAD_OSC_WAVE_TRI:
-            tmp = in;
-            tmp += 16384;
-            if (tmp > 32767) tmp -= 65535;
-            if (tmp > 0) tmp = -tmp;
-            tmp = (2 * tmp) + 32767;
-            break;
-        case TRAD_OSC_WAVE_SAW:
-            tmp = in;
-            break;
-        case TRAD_OSC_WAVE_SQUARE:
-            tmp = waveshaper(TRAD_OSC_WAVE_TRI, in);
-            tmp = fake_square(tmp, 0, 100);
-            break;
-        case TRAD_OSC_WAVE_PULSE:
-            tmp = waveshaper(TRAD_OSC_WAVE_TRI, in);
-            tmp = fake_square(tmp, 12269, 100);
-            break;
-        case TRAD_OSC_WAVE_BLIP:
-            tmp = waveshaper(TRAD_OSC_WAVE_TRI, in);
-            tmp = fake_square(tmp, 20384, 100);
-            break;
-    }
-    if (tmp > 32767) tmp = 32767;
-    if (tmp < -32767) tmp = -32767;
-    return tmp;
-}
-
-#define NAT_LOG_SEMITONE 0.05776226504666215
-
-void trad_osc_set_freq_semitone(trad_osc_t* osc, float tone) {
-    trad_osc_set_freq_Hz(osc, 440. * exp(tone * NAT_LOG_SEMITONE));
-}
-
-void trad_osc_set_freq_Hz(trad_osc_t* osc, float freq) {
-    uint64_t max = ~((uint64_t)0);
-    osc->freq = (freq / (SYNTH_SAMPLE_RATE)) * max;
-}
-
-void trad_osc_set_waveform(trad_osc_t* osc, uint8_t waveform) {
-    osc->waveform = waveform;
-}
-
-void trad_osc_set_attack_ms(trad_osc_t* osc, float ms) {
-    osc->env.attack = (1000. / ms / (SYNTH_SAMPLE_RATE)) * (~((uint32_t)0));
-}
-
-void trad_osc_set_decay_ms(trad_osc_t* osc, float ms) {
-    osc->env.decay = (1000. / ms / (SYNTH_SAMPLE_RATE)) *
-                     ((~((uint32_t)0)) - osc->env.sustain);
-}
-
-void trad_osc_set_sustain(trad_osc_t* osc, float sus) {
-    uint32_t max = ~((uint32_t)0);
-    osc->env.sustain = max * sus;
-}
-
-void trad_osc_set_release_ms(trad_osc_t* osc, float ms) {
-    osc->env.release = (1000. / ms / (SYNTH_SAMPLE_RATE)) * osc->env.sustain;
-}
-
-void trad_env_stop(trad_osc_t* osc) {
-    if (osc->env.env_phase != TRAD_ENV_PHASE_OFF)
-        osc->env.env_phase = TRAD_ENV_PHASE_RELEASE;
-}
-
-void trad_env_fullstop(trad_osc_t* osc) {
-    osc->env.env_phase = TRAD_ENV_PHASE_OFF;  // stop and skip decay phase
-}
-
-void trad_env_start(trad_osc_t* osc) {
-    osc->env.env_phase = TRAD_ENV_PHASE_ATTACK;  // put into attack phase;
-}
-
-void trad_osc_set_vol(trad_osc_t* osc, float volume) {
-    osc->vol = 32767 * volume;
-}
diff --git a/components/bl00mbox/buds/tinysynth/tinysynth.h b/components/bl00mbox/buds/tinysynth/tinysynth.h
deleted file mode 100644
index 492d09105e140b7f682c2de6d803fecdd0c107e3..0000000000000000000000000000000000000000
--- a/components/bl00mbox/buds/tinysynth/tinysynth.h
+++ /dev/null
@@ -1,70 +0,0 @@
-#pragma once
-#include <stdint.h>
-#include <stdio.h>
-
-// #include "radspa.h"
-// #include "bl00mbox.h"
-
-#define SAMPLE_RATE 48000
-
-#define TRAD_OSC_DECAY_STEP 0.01
-#define TRAD_OSC_ATTACK_POP_BLOCK 16
-
-#define TRAD_ENV_PHASE_OFF 0
-#define TRAD_ENV_PHASE_ATTACK 1
-#define TRAD_ENV_PHASE_DECAY 2
-#define TRAD_ENV_PHASE_SUSTAIN 3
-#define TRAD_ENV_PHASE_RELEASE 4
-
-#define TRAD_OSC_WAVE_SINE 0
-#define TRAD_OSC_WAVE_FAKE_SINE 1
-#define TRAD_OSC_WAVE_TRI 2
-#define TRAD_OSC_WAVE_SAW 3
-#define TRAD_OSC_WAVE_SQUARE 4
-#define TRAD_OSC_WAVE_PULSE 5
-#define TRAD_OSC_WAVE_BLIP 6
-
-typedef struct {
-    uint32_t env_counter;
-    uint32_t attack;
-    uint32_t decay;
-    uint32_t sustain;
-    uint32_t release;
-    uint8_t env_phase;
-    uint8_t skip_hold;
-} trad_env_t;
-
-typedef struct {
-    // user variables
-
-    // internal data storage, not for user access
-    uint64_t freq;  // in hertz, negative frequencies for linFM allowed
-    uint64_t bend;
-    uint32_t vol;      // output volume
-    uint8_t waveform;  // 0: sine, 1: fast sine, 2: tri, 3: saw,
-                       // 4: square, 5: 33% pulse, 6: 25% pulse
-
-    uint64_t counter;       // state of central sawtooth oscillator.
-    uint64_t counter_prev;  // previous state of central sawtooth oscillator.
-    int8_t overflow_event;  // set to -1 when counter underflows (below -1),
-                            // set to +1 when counter overflows (above 1)
-                            // not reset or used by anything so far
-    uint8_t undersampling_counter;
-    int16_t prev_output;  // for undersampling
-    uint16_t noise_reg;
-    trad_env_t env;
-} trad_osc_t;
-
-int16_t run_trad_osc(trad_osc_t* osc);
-void trad_osc_set_freq_semitone(trad_osc_t* osc, float bend);
-void trad_osc_set_freq_Hz(trad_osc_t* osc, float freq);
-void trad_osc_set_waveform(trad_osc_t* osc, uint8_t waveform);
-void trad_osc_set_attack_ms(trad_osc_t* osc, float ms);
-void trad_osc_set_decay_ms(trad_osc_t* osc, float ms);
-void trad_osc_set_sustain(trad_osc_t* osc, float sus);
-void trad_osc_set_release_ms(trad_osc_t* osc, float ms);
-void trad_env_stop(trad_osc_t* osc);
-void trad_env_fullstop(trad_osc_t* osc);
-void trad_env_start(trad_osc_t* osc);
-
-void trad_osc_set_vol(trad_osc_t* osc, float volume);
diff --git a/components/bl00mbox/include/bl00mbox.h b/components/bl00mbox/include/bl00mbox.h
index 497e220e27009d9969fcb121c87287927ce0de0a..f73bf7c3d1498acd9edbe3438476fc137ab3a09e 100644
--- a/components/bl00mbox/include/bl00mbox.h
+++ b/components/bl00mbox/include/bl00mbox.h
@@ -1,3 +1,4 @@
+// SPDX-License-Identifier: CC0-1.0
 #pragma once
 #include <stdbool.h>
 #include <stdint.h>
@@ -7,4 +8,9 @@
 uint16_t bl00mbox_sources_count();
 uint16_t bl00mbox_source_add(void* render_data, void* render_function);
 void bl00mbox_source_remove(uint16_t index);
+void bl00mbox_audio_render(int16_t* rx, int16_t* tx, uint16_t len);
+
+// TEMP
 void bl00mbox_player_function(int16_t* rx, int16_t* tx, uint16_t len);
+
+void bl00mbox_init(void);
diff --git a/components/bl00mbox/include/bl00mbox_audio.h b/components/bl00mbox/include/bl00mbox_audio.h
new file mode 100644
index 0000000000000000000000000000000000000000..4fc5fbeadff5335d1b2e0746636eec38d8e8ea5a
--- /dev/null
+++ b/components/bl00mbox/include/bl00mbox_audio.h
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: CC0-1.0
+#pragma once
+
+// TODO: move this to kconfig someday
+#define BL00MBOX_MAX_BUFFER_LEN 256
+#define BL00MBOX_DEFAULT_CHANNEL_VOLUME 3000
+#define BL00MBOX_CHANNELS 32
+#define BL00MBOX_BACKGROUND_MUTE_OVERRIDE_ENABLE
+
+// TODO: remove st3m scope dependency
+#include "st3m_audio.h"
+#include "st3m_scope.h"
+
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+#include "radspa.h"
+
+struct _bl00mbox_bud_t;
+struct _bl00mbox_connection_source_t;
+struct _bl00mbox_channel_root_t;
+struct _bl00mbox_channel_t;
+
+typedef struct _bl00mbox_bud_t {
+    radspa_t* plugin;         // plugin
+    uint64_t index;           // unique index number for bud
+    uint32_t render_pass_id;  // may be used by host to determine whether
+                              // recomputation is necessary
+    uint8_t channel;          // index of channel that owns the plugin
+    struct _bl00mbox_bud_t* chan_next;  // for linked list in bl00mbox_channel_t
+} bl00mbox_bud_t;
+
+typedef struct _bl00mbox_connection_subscriber_t {
+    uint8_t type;  // 0: standard signal input, 1: output mixer
+    uint8_t channel;
+    uint64_t bud_index;
+    uint32_t signal_index;
+    struct _bl00mbox_connection_subscriber_t* next;
+} bl00mbox_connection_subscriber_t;
+
+typedef struct _bl00mbox_connection_t {       // child of bl00mbox_ll_t
+    int16_t buffer[BL00MBOX_MAX_BUFFER_LEN];  // MUST stay on top of struct bc
+                                              // type casting!
+    struct _bl00mbox_bud_t* source_bud;
+    uint32_t signal_index;  // signal of source_bud that renders to buffer
+    struct _bl00mbox_connection_subscriber_t* subs;
+    uint8_t channel;
+    struct _bl00mbox_connection_t*
+        chan_next;  // for linked list in bl00mbox_channel_t;
+} bl00mbox_connection_t;
+
+typedef struct _bl00mbox_channel_root_t {
+    struct _bl00mbox_connection_t* con;
+    struct _bl00mbox_channel_root_t* next;
+} bl00mbox_channel_root_t;
+
+typedef struct {
+    bool is_active;  // rendering can be skipped if false
+    bool is_free;
+    int32_t volume;
+    struct _bl00mbox_channel_root_t*
+        root_list;                 // list of all roots associated with channels
+    uint32_t render_pass_id;       // may be used by host to determine whether
+                                   // recomputation is necessary
+    struct _bl00mbox_bud_t* buds;  // linked list with all channel buds
+    struct _bl00mbox_connection_t*
+        connections;  // linked list with all channel connections
+} bl00mbox_channel_t;
+
+bl00mbox_channel_t* bl00mbox_get_channel(uint8_t chan);
+void bl00mbox_channel_enable(uint8_t chan);
+
+void bl00mbox_channel_enable(uint8_t chan);
+void bl00mbox_channel_disable(uint8_t chan);
+void bl00mbox_channel_set_volume(uint8_t chan, uint16_t volume);
+int16_t bl00mbox_channel_get_volume(uint8_t chan);
+void bl00mbox_channel_event(uint8_t chan);
+uint8_t bl00mbox_channel_get_free_index();
+void bl00mbox_channels_init();
+uint8_t bl00mbox_channel_get_foreground_index();
+void bl00mbox_channel_set_foreground_index(uint8_t index);
+
+bool bl00mbox_channel_get_background_mute_override(uint8_t channel_index);
+bool bl00mbox_channel_set_background_mute_override(uint8_t channel_index,
+                                                   bool enable);
+
+bool bl00mbox_audio_waitfor_pointer_change(void** ptr, void* new_val);
+void bl00mbox_audio_bud_render(bl00mbox_bud_t* bud, uint16_t num_samples);
diff --git a/components/bl00mbox/include/bl00mbox_plugin_registry.h b/components/bl00mbox/include/bl00mbox_plugin_registry.h
new file mode 100644
index 0000000000000000000000000000000000000000..44141d5a1e7728a272b64986935bbc48c0f11dcc
--- /dev/null
+++ b/components/bl00mbox/include/bl00mbox_plugin_registry.h
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: CC0-1.0
+#pragma once
+#include "radspa.h"
+#include "stdio.h"
+
+typedef struct _bl00mbox_plugin_registry_t {
+    radspa_descriptor_t* descriptor;
+    struct _bl00mbox_plugin_registry_t* next;
+} bl00mbox_plugin_registry_t;
+
+radspa_descriptor_t* bl00mbox_plugin_registry_get_descriptor_from_id(
+    uint32_t id);
+radspa_descriptor_t* bl00mbox_plugin_registry_get_descriptor_from_index(
+    uint32_t index);
+void bl00mbox_plugin_registry_init(void);
+uint16_t bl00mbox_plugin_registry_get_plugin_num(void);
diff --git a/components/bl00mbox/include/bl00mbox_radspa_requirements.h b/components/bl00mbox/include/bl00mbox_radspa_requirements.h
new file mode 100644
index 0000000000000000000000000000000000000000..39813840438b0ddc26fce564de5a0c623c6652ac
--- /dev/null
+++ b/components/bl00mbox/include/bl00mbox_radspa_requirements.h
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: CC0-1.0
+#pragma once
+#include "bl00mbox_audio.h"
+#include "radspa.h"
+#include "radspa_helpers.h"
diff --git a/components/bl00mbox/include/bl00mbox_user.h b/components/bl00mbox/include/bl00mbox_user.h
new file mode 100644
index 0000000000000000000000000000000000000000..447e55c907b29dad13018757bb4353847c6677a4
--- /dev/null
+++ b/components/bl00mbox/include/bl00mbox_user.h
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: CC0-1.0
+#pragma once
+
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <stdint.h>
+#include "bl00mbox_audio.h"
+#include "bl00mbox_plugin_registry.h"
+#include "radspa_helpers.h"
+
+uint16_t bl00mbox_channel_buds_num(uint8_t channel);
+uint64_t bl00mbox_channel_get_bud_by_list_pos(uint8_t channel, uint32_t pos);
+uint16_t bl00mbox_channel_conns_num(uint8_t channel);
+uint16_t bl00mbox_channel_mixer_num(uint8_t channel);
+uint64_t bl00mbox_channel_get_bud_by_mixer_list_pos(uint8_t channel,
+                                                    uint32_t pos);
+uint32_t bl00mbox_channel_get_signal_by_mixer_list_pos(uint8_t channel,
+                                                       uint32_t pos);
+bool bl00mbox_channel_clear(uint8_t channel);
+
+bool bl00mbox_channel_connect_signal_to_output_mixer(uint8_t channel,
+                                                     uint32_t bud_index,
+                                                     uint32_t bud_signal_index);
+bool bl00mbox_channel_connect_signal(uint8_t channel, uint32_t bud_rx_index,
+                                     uint32_t bud_rx_signal_index,
+                                     uint32_t bud_tx_index,
+                                     uint32_t bud_tx_signal_index);
+bool bl00mbox_channel_disconnect_signal_rx(uint8_t channel,
+                                           uint32_t bud_rx_index,
+                                           uint32_t bud_rx_signal_index);
+bool bl00mbox_channel_disconnect_signal_tx(uint8_t channel,
+                                           uint32_t bud_tx_index,
+                                           uint32_t bud_tx_signal_index);
+bool bl00mbox_channel_disconnect_signal(uint8_t channel, uint32_t bud_tx_index,
+                                        uint32_t bud_tx_signal_index);
+bool bl00mbox_channel_disconnect_signal_from_output_mixer(
+    uint8_t channel, uint32_t bud_index, uint32_t bud_signal_index);
+
+bl00mbox_bud_t* bl00mbox_channel_new_bud(uint8_t channel, uint32_t id,
+                                         uint32_t init_var);
+bool bl00mbox_channel_delete_bud(uint8_t channel, uint32_t bud_index);
+bool bl00mbox_channel_bud_exists(uint8_t channel, uint32_t bud_index);
+char* bl00mbox_channel_bud_get_name(uint8_t channel, uint32_t bud_index);
+char* bl00mbox_channel_bud_get_description(uint8_t channel, uint32_t bud_index);
+uint32_t bl00mbox_channel_bud_get_plugin_id(uint8_t channel,
+                                            uint32_t bud_index);
+uint16_t bl00mbox_channel_bud_get_num_signals(uint8_t channel,
+                                              uint32_t bud_index);
+
+char* bl00mbox_channel_bud_get_signal_name(uint8_t channel, uint32_t bud_index,
+                                           uint32_t bud_signal_index);
+char* bl00mbox_channel_bud_get_signal_description(uint8_t channel,
+                                                  uint32_t bud_index,
+                                                  uint32_t bud_signal_index);
+char* bl00mbox_channel_bud_get_signal_unit(uint8_t channel, uint32_t bud_index,
+                                           uint32_t bud_signal_index);
+bool bl00mbox_channel_bud_set_signal_value(uint8_t channel, uint32_t bud_index,
+                                           uint32_t bud_signal_index,
+                                           int16_t value);
+int16_t bl00mbox_channel_bud_get_signal_value(uint8_t channel,
+                                              uint32_t bud_index,
+                                              uint32_t bud_signal_index);
+uint32_t bl00mbox_channel_bud_get_signal_hints(uint8_t channel,
+                                               uint32_t bud_index,
+                                               uint32_t bud_signal_index);
+uint16_t bl00mbox_channel_subscriber_num(uint8_t channel, uint64_t bud_index,
+                                         uint16_t signal_index);
+uint64_t bl00mbox_channel_get_bud_by_subscriber_list_pos(uint8_t channel,
+                                                         uint64_t bud_index,
+                                                         uint16_t signal_index,
+                                                         uint8_t pos);
+int32_t bl00mbox_channel_get_signal_by_subscriber_list_pos(
+    uint8_t channel, uint64_t bud_index, uint16_t signal_index, uint8_t pos);
+uint64_t bl00mbox_channel_get_source_bud(uint8_t channel, uint64_t bud_index,
+                                         uint16_t signal_index);
+uint16_t bl00mbox_channel_get_source_signal(uint8_t channel, uint64_t bud_index,
+                                            uint16_t signal_index);
+
+bool bl00mbox_channel_bud_set_table_value(uint8_t channel, uint32_t bud_index,
+                                          uint32_t table_index, int16_t value);
+int16_t bl00mbox_channel_bud_get_table_value(uint8_t channel,
+                                             uint32_t bud_index,
+                                             uint32_t table_index);
+int16_t bl00mbox_channel_bud_get_table_len(uint8_t channel, uint32_t bud_index);
diff --git a/components/bl00mbox/include/bud_registry.h b/components/bl00mbox/include/bud_registry.h
deleted file mode 100644
index 6366273b7c4af407c707b4ac553d9d400e5c8c0b..0000000000000000000000000000000000000000
--- a/components/bl00mbox/include/bud_registry.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma once
-
-#define BUD_ENTRIES 1
-
-#include "tinysynth.h"
-
-bud_t
-
-    bud_t buds[BUD_ENTRIES] = { tinysynth_bud }
diff --git a/components/bl00mbox/include/radspa.h b/components/bl00mbox/include/radspa.h
deleted file mode 100644
index a50543528864eb06dc8b0a4218bdecac2a33bbbf..0000000000000000000000000000000000000000
--- a/components/bl00mbox/include/radspa.h
+++ /dev/null
@@ -1,28 +0,0 @@
-#pragma once
-
-// realtime audio developer's simple plugin api
-// written from scratch but largely inspired by
-// faint memories of the excellent ladspa api
-
-#define RADSPA_SIGNAL_INPUT 0
-#define RADSPA_SIGNAL_TYPE_OUTPUT 1
-#define RADSPA_SIGNAL_EVENT_TRIGGER 2
-
-#define RADSPA_SIGNAL_HINT_LIN 0
-#define RADSPA_SIGNAL_HINT_LOG 1
-
-#if 0
-typedef struct _radspa_signal_t(){
-    uint16_t type;
-    uint16_t hints;
-    char name[32];
-    _radspa_signal_t * next; //linked list
-} radspa_signal_t;
-
-typedef struct _radspa_t(){
-    radspa_signal_t * signals;
-    
-
-} radspa_t;
-
-#endif
diff --git a/components/bl00mbox/plugins/ampliverter.c b/components/bl00mbox/plugins/ampliverter.c
new file mode 100644
index 0000000000000000000000000000000000000000..e55685bd1ea1f9b31c70c0ffac54aa32cd6ea441
--- /dev/null
+++ b/components/bl00mbox/plugins/ampliverter.c
@@ -0,0 +1,93 @@
+#include "ampliverter.h"
+
+// we're defining a prototype for the create function because it has a circular
+// dependency with the descriptor
+radspa_t* ampliverter_create(uint32_t init_var);
+radspa_descriptor_t ampliverter_desc = {
+    .name = "ampliverter",
+    .id = 69,
+    .description = "saturating multiplication and addition",
+    .create_plugin_instance = ampliverter_create,
+    // with this we can only use radspa_standard_plugin_create to allocate
+    // memory. this restricts data layout flexibility for large buffers, but in
+    // return it offers offers a bit of protection from double free/memory leak
+    // issues and makes plugins easier to write.
+    .destroy_plugin_instance = radspa_standard_plugin_destroy
+};
+
+#define AMPLIVERTER_NUM_SIGNALS 4
+#define AMPLIVERTER_OUTPUT 0
+#define AMPLIVERTER_INPUT 1
+#define AMPLIVERTER_GAIN 2
+#define AMPLIVERTER_BIAS 3
+
+void ampliverter_run(radspa_t* ampliverter, uint16_t num_samples,
+                     uint32_t render_pass_id) {
+    // step 1: get signal pointers. since these are stored in a linked list this
+    // is a rather slow operation and should ideally be only be done once per
+    // call. if no signals with output hint are being read the run function may
+    // exit early.
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(ampliverter, AMPLIVERTER_OUTPUT);
+    if (output_sig->buffer == NULL) return;
+    radspa_signal_t* input_sig =
+        radspa_signal_get_by_index(ampliverter, AMPLIVERTER_INPUT);
+    radspa_signal_t* gain_sig =
+        radspa_signal_get_by_index(ampliverter, AMPLIVERTER_GAIN);
+    radspa_signal_t* bias_sig =
+        radspa_signal_get_by_index(ampliverter, AMPLIVERTER_BIAS);
+
+    static int16_t ret = 0;
+    for (uint16_t i = 0; i < num_samples; i++) {
+        // step 2: render the outputs. most of the time a simple for loop will
+        // be fine. using {*radspa_signal_t}->get_value is required to
+        // automatically switch between static values and streamed data at
+        // various sample rates, don't access the data directly
+        int16_t bias =
+            bias_sig->get_value(bias_sig, i, num_samples, render_pass_id);
+        int16_t gain =
+            gain_sig->get_value(gain_sig, i, num_samples, render_pass_id);
+        if (gain == 0) {
+            // make sure that the output buffer exists by comparing to NULL!
+            // (here done earlier outside of the loop)
+            ret = bias;
+        } else {
+            ret =
+                input_sig->get_value(input_sig, i, num_samples, render_pass_id);
+            // the helper functions make sure that potential future float
+            // versions behave as intended
+            ret = radspa_mult_shift(ret, gain);
+            ret = radspa_add_sat(ret, bias);
+        }
+        (output_sig->buffer)[i] = ret;
+    }
+    output_sig->value = ret;
+}
+
+radspa_t* ampliverter_create(uint32_t init_var) {
+    // init_var is not used in this example
+
+    // step 1: try to allocate enough memory for a standard plugin with the
+    // given amount of signals and plugin data. there is no plugin data in this
+    // case, but sizeof(void) is invalid sooo we're taking the next smallest
+    // thing (char) ig? we're not good at C. providing the descriptor address is
+    // required to make sure it is not forgotten.
+    radspa_t* ampliverter = radspa_standard_plugin_create(
+        &ampliverter_desc, AMPLIVERTER_NUM_SIGNALS, sizeof(char), 0);
+    if (ampliverter == NULL) return NULL;
+
+    // step 2: define run function
+    ampliverter->render = ampliverter_run;
+
+    // step 3: standard_plugin_create has already created dummy signals for us,
+    // we just need to fill them
+    radspa_signal_set(ampliverter, AMPLIVERTER_OUTPUT, "output",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(ampliverter, AMPLIVERTER_INPUT, "input",
+                      RADSPA_SIGNAL_HINT_INPUT, 0);
+    radspa_signal_set(ampliverter, AMPLIVERTER_GAIN, "gain",
+                      RADSPA_SIGNAL_HINT_INPUT, 32767);
+    radspa_signal_set(ampliverter, AMPLIVERTER_BIAS, "bias",
+                      RADSPA_SIGNAL_HINT_INPUT, 0);
+    return ampliverter;
+}
diff --git a/components/bl00mbox/plugins/ampliverter.h b/components/bl00mbox/plugins/ampliverter.h
new file mode 100644
index 0000000000000000000000000000000000000000..990c36a49e42895eae7dbbbecc3f58abbe532701
--- /dev/null
+++ b/components/bl00mbox/plugins/ampliverter.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <radspa.h>
+#include <radspa_helpers.h>
+
+extern radspa_descriptor_t ampliverter_desc;
+radspa_t* ampliverter_create(uint32_t init_var);
+void ampliverter_run(radspa_t* osc, uint16_t num_samples,
+                     uint32_t render_pass_id);
diff --git a/components/bl00mbox/plugins/brok3n/filter.c b/components/bl00mbox/plugins/brok3n/filter.c
new file mode 100644
index 0000000000000000000000000000000000000000..d15696d24bcdf11f48e643b78afff2a0be94dee6
--- /dev/null
+++ b/components/bl00mbox/plugins/brok3n/filter.c
@@ -0,0 +1,151 @@
+#include "filter.h"
+
+radspa_t* filter_create(uint32_t init_var);
+radspa_descriptor_t filter_desc = { .name = "filter",
+                                    .id = 69420,
+                                    .description = "simple filter",
+                                    .create_plugin_instance = filter_create,
+                                    .destroy_plugin_instance =
+                                        radspa_standard_plugin_destroy };
+
+#define FILTER_NUM_SIGNALS 5
+#define FILTER_OUTPUT 0
+#define FILTER_INPUT 1
+#define FILTER_FREQ 2
+#define FILTER_Q 3
+#define FILTER_GAIN 4
+
+static int16_t apply_filter(filter_data_t* data, int32_t input, int32_t gain) {
+    data->pos++;
+    if (data->pos >= 3) data->pos = 0;
+
+    int64_t out = 0;
+    int64_t in_acc = input;
+
+    for (int8_t i = 0; i < 2; i++) {
+        int8_t pos = data->pos - i - 1;
+        if (pos < 0) pos += 3;
+        in_acc += (2 - i) * data->in_history[pos];
+
+        int16_t shift = data->out_coeff_shift[i] - data->in_coeff_shift;
+        if (shift >= 0) {
+            out -= (data->out_history[pos] * data->out_coeff[i]) >> shift;
+        } else {
+            out -= (data->out_history[pos] * data->out_coeff[i]) << (-shift);
+        }
+    }
+    out += in_acc * data->in_coeff;
+    out = out >> data->in_coeff_shift;
+
+    data->in_history[data->pos] = input;
+    data->out_history[data->pos] = out;
+
+    return radspa_mult_shift(out, gain);
+}
+
+static uint8_t coeff_shift(float coeff) {
+    int32_t ret = 16;
+    while (1) {
+        int32_t test_val = (1 << ret) * coeff;
+        if (test_val < 0) test_val = -test_val;
+        if (test_val > (1 << 12)) {
+            ret -= 3;
+        } else if (test_val < (1 << 8)) {
+            ret += 3;
+        } else {
+            break;
+        }
+        if (ret > 28) break;
+        if (ret < 4) break;
+    }
+    return ret;
+}
+
+static void set_filter_coeffs(filter_data_t* data, float freq, float q) {
+    // molasses, sorry
+    if (freq == 0) return;
+    if (q == 0) return;
+    float K = 2 * 48000;
+    float omega = freq * 6.28;
+    float A[3];
+    A[0] = K * K;
+    A[1] = K * omega / q;
+    A[2] = omega * omega;
+    float B = omega * omega;
+
+    float sum_a = A[0] + A[1] + A[2];
+
+    float round;
+    int16_t shift;
+
+    round = B / sum_a;
+    shift = coeff_shift(round);
+    data->in_coeff = round * (1 << shift);
+    data->in_coeff_shift = shift;
+
+    round = 2. * (A[2] - A[0]) / sum_a;
+    shift = coeff_shift(round);
+    data->out_coeff[0] = round * (1 << shift);
+    data->out_coeff_shift[0] = shift;
+
+    round = (A[0] - A[1] + A[2]) / sum_a;
+    shift = coeff_shift(round);
+    data->out_coeff[1] = round * (1 << shift);
+    data->out_coeff_shift[1] = shift;
+}
+
+void filter_run(radspa_t* filter, uint16_t num_samples,
+                uint32_t render_pass_id) {
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(filter, FILTER_OUTPUT);
+    if (output_sig->buffer == NULL) return;
+    filter_data_t* data = filter->plugin_data;
+    radspa_signal_t* input_sig =
+        radspa_signal_get_by_index(filter, FILTER_INPUT);
+    radspa_signal_t* freq_sig = radspa_signal_get_by_index(filter, FILTER_FREQ);
+    radspa_signal_t* q_sig = radspa_signal_get_by_index(filter, FILTER_Q);
+    radspa_signal_t* gain_sig = radspa_signal_get_by_index(filter, FILTER_GAIN);
+
+    static int16_t ret = 0;
+
+    for (uint16_t i = 0; i < num_samples; i++) {
+        int16_t input =
+            radspa_signal_get_value(input_sig, i, num_samples, render_pass_id);
+        int32_t freq =
+            radspa_signal_get_value(freq_sig, i, num_samples, render_pass_id);
+        int16_t q =
+            radspa_signal_get_value(q_sig, i, num_samples, render_pass_id);
+        int16_t gain =
+            radspa_signal_get_value(gain_sig, i, num_samples, render_pass_id);
+
+        if ((freq != data->prev_freq) | (q != data->prev_q)) {
+            set_filter_coeffs(data, freq, ((float)q + 1.) / 1000.);
+            data->prev_freq = freq;
+            data->prev_q = q;
+        }
+
+        ret = apply_filter(data, input, gain);
+        (output_sig->buffer)[i] = ret;
+    }
+    output_sig->value = ret;
+}
+
+radspa_t* filter_create(uint32_t real_init_var) {
+    radspa_t* filter = radspa_standard_plugin_create(
+        &filter_desc, FILTER_NUM_SIGNALS, sizeof(filter_data_t), 0);
+    if (filter == NULL) return NULL;
+    filter->render = filter_run;
+    radspa_signal_set(filter, FILTER_OUTPUT, "output",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(filter, FILTER_INPUT, "input", RADSPA_SIGNAL_HINT_INPUT,
+                      0);
+    radspa_signal_set(filter, FILTER_FREQ, "freq", RADSPA_SIGNAL_HINT_INPUT,
+                      500);
+    radspa_signal_set(filter, FILTER_Q, "reso", RADSPA_SIGNAL_HINT_INPUT, 1000);
+    radspa_signal_set(filter, FILTER_GAIN, "gain", RADSPA_SIGNAL_HINT_INPUT,
+                      32760);
+    filter_data_t* data = filter->plugin_data;
+    data->pos = 0;
+    data->prev_freq = 1 << 24;
+    return filter;
+}
diff --git a/components/bl00mbox/plugins/brok3n/filter.h b/components/bl00mbox/plugins/brok3n/filter.h
new file mode 100644
index 0000000000000000000000000000000000000000..e575e7daca36dee48d17ba3ac24f98a115317ed8
--- /dev/null
+++ b/components/bl00mbox/plugins/brok3n/filter.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <radspa.h>
+
+typedef struct {
+    int64_t in_history[3];
+    int64_t in_coeff;
+    int16_t in_coeff_shift;
+    int64_t out_history[3];
+    int64_t out_coeff[2];
+    int16_t out_coeff_shift[2];
+
+    int32_t prev_freq;
+    int16_t prev_q;
+    int8_t pos;
+} filter_data_t;
+
+extern radspa_descriptor_t filter_desc;
+radspa_t* filter_create(uint32_t init_var);
+void filter_run(radspa_t* osc, uint16_t num_samples, uint32_t render_pass_id);
diff --git a/components/bl00mbox/plugins/delay.c b/components/bl00mbox/plugins/delay.c
new file mode 100644
index 0000000000000000000000000000000000000000..ede1d95a490180c060ef9c1dc3355a79acf3c252
--- /dev/null
+++ b/components/bl00mbox/plugins/delay.c
@@ -0,0 +1,111 @@
+#include "delay.h"
+
+radspa_t* delay_create(uint32_t init_var);
+radspa_descriptor_t delay_desc = {
+    .name = "delay",
+    .id = 42069,
+    .description = "simple delay with ms input and feedback",
+    .create_plugin_instance = delay_create,
+    .destroy_plugin_instance = radspa_standard_plugin_destroy
+};
+
+#define DELAY_NUM_SIGNALS 7
+#define DELAY_OUTPUT 0
+#define DELAY_INPUT 1
+#define DELAY_TIME 2
+#define DELAY_FEEDBACK 3
+#define DELAY_LEVEL 4
+#define DELAY_DRY_VOL 5
+#define DELAY_REC_VOL 6
+
+void delay_run(radspa_t* delay, uint16_t num_samples, uint32_t render_pass_id) {
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(delay, DELAY_OUTPUT);
+    if (output_sig->buffer == NULL) return;
+    delay_data_t* data = delay->plugin_data;
+    int16_t* buf = delay->plugin_table;
+    radspa_signal_t* input_sig = radspa_signal_get_by_index(delay, DELAY_INPUT);
+    radspa_signal_t* time_sig = radspa_signal_get_by_index(delay, DELAY_TIME);
+    radspa_signal_t* feedback_sig =
+        radspa_signal_get_by_index(delay, DELAY_FEEDBACK);
+    radspa_signal_t* level_sig = radspa_signal_get_by_index(delay, DELAY_LEVEL);
+    radspa_signal_t* dry_vol_sig =
+        radspa_signal_get_by_index(delay, DELAY_DRY_VOL);
+    radspa_signal_t* rec_vol_sig =
+        radspa_signal_get_by_index(delay, DELAY_REC_VOL);
+
+    static int16_t ret = 0;
+
+    uint32_t buffer_size = delay->plugin_table_len;
+
+    for (uint16_t i = 0; i < num_samples; i++) {
+        uint32_t time =
+            time_sig->get_value(time_sig, i, num_samples, render_pass_id);
+        if (time > data->max_delay) time = data->max_delay;
+
+        data->write_head_position++;
+        while (data->write_head_position >= buffer_size)
+            data->write_head_position -= buffer_size;  // maybe faster than %
+        if (time != data->time_prev) {
+            data->read_head_position =
+                time * (48000 / 1000) + data->write_head_position;
+            data->time_prev = time;
+        } else {
+            data->read_head_position++;
+        }
+        while (data->read_head_position >= buffer_size)
+            data->read_head_position -= buffer_size;
+
+        // int16_t * buf = &(data->buffer);
+
+        int16_t dry =
+            input_sig->get_value(input_sig, i, num_samples, render_pass_id);
+        int16_t wet = buf[data->read_head_position];
+
+        int16_t fb = feedback_sig->get_value(feedback_sig, i, num_samples,
+                                             render_pass_id);
+        int16_t level =
+            level_sig->get_value(level_sig, i, num_samples, render_pass_id);
+
+        int16_t dry_vol =
+            dry_vol_sig->get_value(dry_vol_sig, i, num_samples, render_pass_id);
+        int16_t rec_vol =
+            rec_vol_sig->get_value(rec_vol_sig, i, num_samples, render_pass_id);
+
+        if (rec_vol) {
+            buf[data->write_head_position] = radspa_add_sat(
+                radspa_mult_shift(rec_vol, dry), radspa_mult_shift(wet, fb));
+        }
+
+        ret = radspa_add_sat(radspa_mult_shift(dry_vol, dry),
+                             radspa_mult_shift(wet, level));
+
+        (output_sig->buffer)[i] = ret;
+    }
+    output_sig->value = ret;
+}
+
+radspa_t* delay_create(uint32_t init_var) {
+    if (init_var == 0) init_var = 500;
+    uint32_t buffer_size = init_var * (48000 / 1000);
+    radspa_t* delay = radspa_standard_plugin_create(
+        &delay_desc, DELAY_NUM_SIGNALS, sizeof(delay_data_t), buffer_size);
+
+    if (delay == NULL) return NULL;
+    delay_data_t* plugin_data = delay->plugin_data;
+    plugin_data->max_delay = init_var;
+    delay->render = delay_run;
+    radspa_signal_set(delay, DELAY_OUTPUT, "output", RADSPA_SIGNAL_HINT_OUTPUT,
+                      0);
+    radspa_signal_set(delay, DELAY_INPUT, "input", RADSPA_SIGNAL_HINT_INPUT, 0);
+    radspa_signal_set(delay, DELAY_TIME, "time", RADSPA_SIGNAL_HINT_INPUT, 200);
+    radspa_signal_set(delay, DELAY_FEEDBACK, "feedback",
+                      RADSPA_SIGNAL_HINT_INPUT, 16000);
+    radspa_signal_set(delay, DELAY_LEVEL, "level", RADSPA_SIGNAL_HINT_INPUT,
+                      16000);
+    radspa_signal_set(delay, DELAY_DRY_VOL, "dry_vol", RADSPA_SIGNAL_HINT_INPUT,
+                      32767);
+    radspa_signal_set(delay, DELAY_REC_VOL, "rec_vol", RADSPA_SIGNAL_HINT_INPUT,
+                      32767);
+    return delay;
+}
diff --git a/components/bl00mbox/plugins/delay.h b/components/bl00mbox/plugins/delay.h
new file mode 100644
index 0000000000000000000000000000000000000000..040a5af2cc9a4a7990055fa7e4fbf03da07a4ce0
--- /dev/null
+++ b/components/bl00mbox/plugins/delay.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <radspa.h>
+#include <radspa_helpers.h>
+
+typedef struct {
+    uint32_t read_head_position;
+    uint32_t write_head_position;
+    uint32_t max_delay;
+    uint32_t time_prev;
+} delay_data_t;
+
+extern radspa_descriptor_t delay_desc;
+radspa_t* delay_create(uint32_t init_var);
+void delay_run(radspa_t* osc, uint16_t num_samples, uint32_t render_pass_id);
diff --git a/components/bl00mbox/plugins/sampler.c b/components/bl00mbox/plugins/sampler.c
new file mode 100644
index 0000000000000000000000000000000000000000..c00041c5da1372c075c7cbc74e18ec288e57c9b8
--- /dev/null
+++ b/components/bl00mbox/plugins/sampler.c
@@ -0,0 +1,70 @@
+#include "sampler.h"
+
+radspa_t* sampler_create(uint32_t init_var);
+radspa_descriptor_t sampler_desc = {
+    .name = "sampler_ram",
+    .id = 696969,
+    .description = "simple sampler that stores a copy of the sample in ram",
+    .create_plugin_instance = sampler_create,
+    .destroy_plugin_instance = radspa_standard_plugin_destroy
+};
+
+#define SAMPLER_NUM_SIGNALS 2
+#define SAMPLER_OUTPUT 0
+#define SAMPLER_TRIGGER 1
+
+void sampler_run(radspa_t* sampler, uint16_t num_samples,
+                 uint32_t render_pass_id) {
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(sampler, SAMPLER_OUTPUT);
+    if (output_sig->buffer == NULL) return;
+    sampler_data_t* data = sampler->plugin_data;
+    int16_t* buf = sampler->plugin_table;
+    radspa_signal_t* trigger_sig =
+        radspa_signal_get_by_index(sampler, SAMPLER_TRIGGER);
+
+    static int32_t ret = 0;
+
+    uint32_t buffer_size = sampler->plugin_table_len;
+
+    for (uint16_t i = 0; i < num_samples; i++) {
+        int16_t trigger =
+            trigger_sig->get_value(trigger_sig, i, num_samples, render_pass_id);
+
+        int16_t vel = radspa_trigger_get(trigger, &(data->trigger_prev));
+
+        if (vel > 0) {
+            data->read_head_position = 0;
+            data->volume = vel;
+        } else if (vel < 0) {
+            data->read_head_position = buffer_size;
+        }
+
+        if (data->read_head_position < buffer_size) {
+            ret =
+                radspa_mult_shift(buf[data->read_head_position], data->volume);
+            data->read_head_position++;
+        } else {
+            // ret = (ret * 255)>>8; // avoid dc clicks with bad samples
+            ret = 0;
+        }
+
+        (output_sig->buffer)[i] = ret;
+    }
+    output_sig->value = ret;
+}
+
+radspa_t* sampler_create(uint32_t init_var) {
+    if (init_var == 0) return NULL;  // doesn't make sense
+    uint32_t buffer_size = init_var;
+    radspa_t* sampler =
+        radspa_standard_plugin_create(&sampler_desc, SAMPLER_NUM_SIGNALS,
+                                      sizeof(sampler_data_t), buffer_size);
+    if (sampler == NULL) return NULL;
+    sampler->render = sampler_run;
+    radspa_signal_set(sampler, SAMPLER_OUTPUT, "output",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(sampler, SAMPLER_TRIGGER, "trigger",
+                      RADSPA_SIGNAL_HINT_INPUT | RADSPA_SIGNAL_HINT_TRIGGER, 0);
+    return sampler;
+}
diff --git a/components/bl00mbox/plugins/sampler.h b/components/bl00mbox/plugins/sampler.h
new file mode 100644
index 0000000000000000000000000000000000000000..c9bfd8dd28acb401ffa7e3c958704b957d3b5c44
--- /dev/null
+++ b/components/bl00mbox/plugins/sampler.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <radspa.h>
+#include <radspa_helpers.h>
+
+typedef struct {
+    uint32_t read_head_position;
+    int16_t trigger_prev;
+    int16_t volume;
+} sampler_data_t;
+
+extern radspa_descriptor_t sampler_desc;
+radspa_t* sampler_create(uint32_t init_var);
+void sampler_run(radspa_t* osc, uint16_t num_samples, uint32_t render_pass_id);
diff --git a/components/bl00mbox/plugins/sequencer.c b/components/bl00mbox/plugins/sequencer.c
new file mode 100644
index 0000000000000000000000000000000000000000..21187962e47cc948f1e993c371f0cbe68a98c5d6
--- /dev/null
+++ b/components/bl00mbox/plugins/sequencer.c
@@ -0,0 +1,169 @@
+#include "sequencer.h"
+
+radspa_descriptor_t sequencer_desc = { .name = "sequencer",
+                                       .id = 56709,
+                                       .description = "i.o.u.",
+                                       .create_plugin_instance =
+                                           sequencer_create,
+                                       .destroy_plugin_instance =
+                                           radspa_standard_plugin_destroy };
+
+#define SEQUENCER_NUM_SIGNALS 6
+#define SEQUENCER_STEP 0
+#define SEQUENCER_STEP_LEN 1
+#define SEQUENCER_SYNC_OUT 2
+#define SEQUENCER_SYNC_IN 3
+#define SEQUENCER_BPM 4
+#define SEQUENCER_BEAT_DIV 5
+#define SEQUENCER_OUTPUT 6
+
+static uint64_t target(uint64_t step_len, uint64_t bpm, uint64_t beat_div) {
+    return (48000ULL * 60 * 4) / (bpm * beat_div);
+}
+
+radspa_t* sequencer_create(uint32_t init_var) {
+    // init_var:
+    //  lsbyte: number of channels
+    //  lsbyte+1: number of pixels in channel (>= bars*beats_div)
+    uint32_t num_tracks = 1;
+    uint32_t num_pixels = 16;
+    if (init_var) {
+        num_tracks = init_var & 0xFF;
+        num_pixels = (init_var >> 8) & 0xFF;
+    }
+    uint32_t table_size = num_tracks * (num_pixels + 1);
+    uint32_t num_signals =
+        num_tracks + SEQUENCER_NUM_SIGNALS;  // one for each channel output
+    radspa_t* sequencer = radspa_standard_plugin_create(
+        &sequencer_desc, num_signals, sizeof(sequencer_data_t), table_size);
+    sequencer->render = sequencer_run;
+
+    sequencer_data_t* data = sequencer->plugin_data;
+    data->track_step_len = num_pixels;
+    data->num_tracks = num_tracks;
+    data->bpm_prev = 120;
+    data->beat_div_prev = 16;
+    data->counter_target =
+        target(data->track_step_len, data->bpm_prev, data->beat_div_prev);
+    radspa_signal_set(sequencer, SEQUENCER_STEP, "step",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(sequencer, SEQUENCER_STEP_LEN, "step_len",
+                      RADSPA_SIGNAL_HINT_INPUT, num_pixels);
+    radspa_signal_set(sequencer, SEQUENCER_SYNC_OUT, "sync_out",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(sequencer, SEQUENCER_SYNC_IN, "sync_in",
+                      RADSPA_SIGNAL_HINT_INPUT | RADSPA_SIGNAL_HINT_TRIGGER, 0);
+    radspa_signal_set(sequencer, SEQUENCER_BPM, "bpm", RADSPA_SIGNAL_HINT_INPUT,
+                      data->bpm_prev);
+    radspa_signal_set(sequencer, SEQUENCER_BEAT_DIV, "beat_div",
+                      RADSPA_SIGNAL_HINT_INPUT, data->beat_div_prev);
+    radspa_signal_set(sequencer, SEQUENCER_OUTPUT, "output",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+
+    /*
+    for(uint8_t i = 0; i < num_signals; i++){
+        radspa_signal_set(sequencer, SEQUENCER_NUM_SIGNALS + i, "track",
+    RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    }
+    */
+
+    data->counter = 0;
+    data->sync_in_prev = 0;
+    data->sync_out = 32767;
+
+    return sequencer;
+}
+
+/* ~table encoding~
+ * first int16_t: track type:
+ * -32767 : trigger track
+ * 32767 : direct track
+ * in between: slew rate
+ */
+
+void sequencer_run(radspa_t* sequencer, uint16_t num_samples,
+                   uint32_t render_pass_id) {
+    radspa_signal_t* step_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_STEP);
+    radspa_signal_t* sync_out_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_SYNC_OUT);
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_OUTPUT);
+    if ((output_sig->buffer == NULL) && (sync_out_sig->buffer == NULL) &&
+        (step_sig->buffer == NULL))
+        return;
+
+    radspa_signal_t* step_len_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_STEP_LEN);
+    radspa_signal_t* sync_in_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_SYNC_IN);
+    radspa_signal_t* bpm_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_BPM);
+    radspa_signal_t* beat_div_sig =
+        radspa_signal_get_by_index(sequencer, SEQUENCER_BEAT_DIV);
+    sequencer_data_t* data = sequencer->plugin_data;
+    int16_t* table = sequencer->plugin_table;
+
+    int16_t s1 =
+        radspa_signal_get_value(step_len_sig, 0, num_samples, render_pass_id);
+    int16_t s2 = data->track_step_len;
+    data->step_target = s1 > 0 ? (s1 > s2 ? s2 : s1) : 1;
+
+    int16_t bpm = bpm_sig->get_value(bpm_sig, 0, num_samples, render_pass_id);
+    int16_t beat_div =
+        beat_div_sig->get_value(beat_div_sig, 0, num_samples, render_pass_id);
+    if ((bpm != data->bpm_prev) || (beat_div != data->beat_div_prev)) {
+        data->counter_target = target(data->track_step_len, bpm, beat_div);
+        data->bpm_prev = bpm;
+        data->beat_div_prev = beat_div;
+    }
+
+    for (uint16_t i = 0; i < num_samples; i++) {
+        data->counter++;
+        if (data->counter >= data->counter_target) {
+            data->counter = 0;
+            data->step++;
+            if (data->step >= data->step_target) {
+                data->step = 0;
+                data->sync_out = -data->sync_out;
+            }
+        }
+
+        int16_t sync_in =
+            sync_in_sig->get_value(sync_in_sig, i, num_samples, render_pass_id);
+        if (((sync_in > 0) && (data->sync_in_prev <= 0)) ||
+            ((sync_in > 0) && (data->sync_in_prev <= 0))) {
+            data->counter = 0;
+            data->step = 0;
+            data->sync_out = -data->sync_out;
+        }
+        data->sync_in_prev = sync_in;
+
+        if (!data->counter) {  // event just happened
+            for (uint8_t track = 0; track < data->num_tracks; track++) {
+                int16_t type = table[track * data->track_step_len];
+                int16_t stage_val =
+                    table[data->step + 1 + data->track_step_len * track];
+                if (type == 32767) {
+                    data->track_fill[track] = stage_val;
+                } else if (type == -32767) {
+                    if (stage_val > 0)
+                        data->track_fill[track] = radspa_trigger_start(
+                            stage_val, &(data->trigger_hist[track]));
+                    if (stage_val < 0)
+                        data->track_fill[track] =
+                            radspa_trigger_stop(&(data->trigger_hist[track]));
+                }
+            }
+        }
+
+        if (output_sig->buffer != NULL)
+            (output_sig->buffer)[i] = data->track_fill[0];
+        if (sync_out_sig->buffer != NULL)
+            (sync_out_sig->buffer)[i] = data->sync_out;
+        if (step_sig->buffer != NULL) (step_sig->buffer)[i] = data->step;
+    }
+    sync_out_sig->value = data->sync_out;
+    output_sig->value = data->track_fill[0];
+    step_sig->value = data->step;
+}
diff --git a/components/bl00mbox/plugins/sequencer.h b/components/bl00mbox/plugins/sequencer.h
new file mode 100644
index 0000000000000000000000000000000000000000..f59a3c092c7c71344c3b32c0cde92bdf4f134575
--- /dev/null
+++ b/components/bl00mbox/plugins/sequencer.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <math.h>
+#include "radspa.h"
+#include "radspa_helpers.h"
+
+typedef struct {
+    uint8_t num_tracks;
+    uint16_t track_step_len;
+    uint8_t step_target;
+    uint8_t step;
+    uint64_t counter;
+    uint64_t counter_target;
+    int16_t sync_in_prev;
+    int16_t sync_out;
+    int16_t bpm_prev;
+    int16_t beat_div_prev;
+    int16_t track_fill[1];
+    int16_t trigger_hist[1];
+} sequencer_data_t;
+
+extern radspa_descriptor_t sequencer_desc;
+radspa_t* sequencer_create(uint32_t init_var);
+void sequencer_run(radspa_t* osc, uint16_t num_samples,
+                   uint32_t render_pass_id);
diff --git a/components/bl00mbox/plugins/trad_synth/trad_synth.c b/components/bl00mbox/plugins/trad_synth/trad_synth.c
new file mode 100644
index 0000000000000000000000000000000000000000..d913f4567fc933723cb6c3c0a720d59643245493
--- /dev/null
+++ b/components/bl00mbox/plugins/trad_synth/trad_synth.c
@@ -0,0 +1,380 @@
+#include "trad_synth.h"
+
+// plugin descriptions in trad_synth.h
+
+static inline int16_t waveshaper(int16_t saw, int16_t shape);
+
+// plugin: trad_osc
+radspa_descriptor_t trad_osc_desc = {
+    .name = "osc_fm",
+    .id = 420,
+    .description = "simple audio band oscillator with classic waveforms",
+    .create_plugin_instance = trad_osc_create,
+    .destroy_plugin_instance = radspa_standard_plugin_destroy
+};
+
+#define TRAD_OSC_NUM_SIGNALS 4
+#define TRAD_OSC_OUTPUT 0
+#define TRAD_OSC_PITCH 1
+#define TRAD_OSC_WAVEFORM 2
+#define TRAD_OSC_LIN_FM 3
+
+radspa_t* trad_osc_create(uint32_t init_var) {
+    radspa_t* trad_osc = radspa_standard_plugin_create(
+        &trad_osc_desc, TRAD_OSC_NUM_SIGNALS, sizeof(trad_osc_data_t), 0);
+    trad_osc->render = trad_osc_run;
+    radspa_signal_set(trad_osc, TRAD_OSC_OUTPUT, "output",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(trad_osc, TRAD_OSC_PITCH, "pitch",
+                      RADSPA_SIGNAL_HINT_INPUT | RADSPA_SIGNAL_HINT_SCT, 18367);
+    radspa_signal_set(trad_osc, TRAD_OSC_WAVEFORM, "waveform",
+                      RADSPA_SIGNAL_HINT_INPUT, -16000);
+    radspa_signal_set(trad_osc, TRAD_OSC_LIN_FM, "lin_fm",
+                      RADSPA_SIGNAL_HINT_INPUT, 0);
+    return trad_osc;
+}
+
+void trad_osc_run(radspa_t* trad_osc, uint16_t num_samples,
+                  uint32_t render_pass_id) {
+    trad_osc_data_t* plugin_data = trad_osc->plugin_data;
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(trad_osc, TRAD_OSC_OUTPUT);
+    radspa_signal_t* pitch_sig =
+        radspa_signal_get_by_index(trad_osc, TRAD_OSC_PITCH);
+    radspa_signal_t* waveform_sig =
+        radspa_signal_get_by_index(trad_osc, TRAD_OSC_WAVEFORM);
+    radspa_signal_t* lin_fm_sig =
+        radspa_signal_get_by_index(trad_osc, TRAD_OSC_LIN_FM);
+    if (output_sig->buffer == NULL) return;
+
+    int16_t ret = 0;
+    for (uint16_t i = 0; i < num_samples; i++) {
+        int16_t pitch =
+            pitch_sig->get_value(pitch_sig, i, num_samples, render_pass_id);
+        int16_t wave = waveform_sig->get_value(waveform_sig, i, num_samples,
+                                               render_pass_id);
+        int32_t lin_fm =
+            lin_fm_sig->get_value(lin_fm_sig, i, num_samples, render_pass_id);
+
+        if (pitch != plugin_data->prev_pitch) {
+            plugin_data->incr = radspa_sct_to_rel_freq(pitch, 0);
+            plugin_data->prev_pitch = pitch;
+        }
+        plugin_data->counter += plugin_data->incr;
+        if (lin_fm) {
+            plugin_data->counter += lin_fm * (plugin_data->incr >> 15);
+        }
+
+        int32_t tmp = (plugin_data->counter) >> 17;
+        tmp = (tmp * 2) - 32767;
+        ret = waveshaper(tmp, wave);
+        (output_sig->buffer)[i] = ret;
+    }
+    output_sig->value = ret;
+}
+
+static inline int16_t triangle(int16_t saw) {
+    int32_t tmp = saw;
+    tmp += 16384;
+    if (tmp > 32767) tmp -= 65535;
+    if (tmp > 0) tmp = -tmp;
+    tmp = (2 * tmp) + 32767;
+    return tmp;
+}
+
+inline int16_t waveshaper(int16_t saw, int16_t shape) {
+    int32_t tmp = saw;
+    uint8_t sh = ((uint16_t)shape) >> 14;
+    sh = (sh + 2) % 4;
+    switch (sh) {
+        case 0:  // sine
+            tmp = triangle(tmp);
+            if (tmp > 0) {
+                tmp = 32767 - tmp;
+                tmp = (tmp * tmp) >> 15;
+                tmp = 32767. - tmp;
+            } else {
+                tmp = 32767 + tmp;
+                tmp = (tmp * tmp) >> 15;
+                tmp = tmp - 32767.;
+            }
+            break;
+        case 1:  // tri
+            tmp = triangle(tmp);
+            break;
+        case 2:  // square:
+            if (tmp > 0) {
+                tmp = 32767;
+            } else {
+                tmp = -32767;
+            }
+            break;
+        default:  // saw
+            break;
+    }
+    return tmp;
+}
+
+// plugin: trad_env
+radspa_descriptor_t trad_env_desc = { .name = "env_adsr",
+                                      .id = 42,
+                                      .description = "simple ADSR envelope",
+                                      .create_plugin_instance = trad_env_create,
+                                      .destroy_plugin_instance =
+                                          radspa_standard_plugin_destroy };
+
+#define TRAD_ENV_NUM_SIGNALS 9
+#define TRAD_ENV_OUTPUT 0
+#define TRAD_ENV_PHASE 1
+#define TRAD_ENV_INPUT 2
+#define TRAD_ENV_TRIGGER 3
+#define TRAD_ENV_ATTACK 4
+#define TRAD_ENV_DECAY 5
+#define TRAD_ENV_SUSTAIN 6
+#define TRAD_ENV_RELEASE 7
+#define TRAD_ENV_GATE 8
+
+#define TRAD_ENV_PHASE_OFF 0
+#define TRAD_ENV_PHASE_ATTACK 1
+#define TRAD_ENV_PHASE_DECAY 2
+#define TRAD_ENV_PHASE_SUSTAIN 3
+#define TRAD_ENV_PHASE_RELEASE 4
+
+radspa_t* trad_env_create(uint32_t init_var) {
+    radspa_t* trad_env = radspa_standard_plugin_create(
+        &trad_env_desc, TRAD_ENV_NUM_SIGNALS, sizeof(trad_env_data_t), 0);
+    trad_env->render = trad_env_run;
+    radspa_signal_set(trad_env, TRAD_ENV_OUTPUT, "output",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(trad_env, TRAD_ENV_PHASE, "phase",
+                      RADSPA_SIGNAL_HINT_OUTPUT, 0);
+    radspa_signal_set(trad_env, TRAD_ENV_INPUT, "input",
+                      RADSPA_SIGNAL_HINT_INPUT, 32767);
+    radspa_signal_set(trad_env, TRAD_ENV_TRIGGER, "trigger",
+                      RADSPA_SIGNAL_HINT_INPUT | RADSPA_SIGNAL_HINT_TRIGGER, 0);
+    radspa_signal_set(trad_env, TRAD_ENV_ATTACK, "attack",
+                      RADSPA_SIGNAL_HINT_INPUT, 100);
+    radspa_signal_set(trad_env, TRAD_ENV_DECAY, "decay",
+                      RADSPA_SIGNAL_HINT_INPUT, 250);
+    radspa_signal_set(trad_env, TRAD_ENV_SUSTAIN, "sustain",
+                      RADSPA_SIGNAL_HINT_INPUT, 16000);
+    radspa_signal_set(trad_env, TRAD_ENV_RELEASE, "release",
+                      RADSPA_SIGNAL_HINT_INPUT, 50);
+    radspa_signal_set(trad_env, TRAD_ENV_GATE, "gate", RADSPA_SIGNAL_HINT_INPUT,
+                      0);
+    radspa_signal_get_by_index(trad_env, TRAD_ENV_ATTACK)->unit = "ms";
+    radspa_signal_get_by_index(trad_env, TRAD_ENV_DECAY)->unit = "ms";
+    radspa_signal_get_by_index(trad_env, TRAD_ENV_SUSTAIN)->unit = "ms";
+
+    trad_env_data_t* data = trad_env->plugin_data;
+    data->trigger_prev = 0;
+    data->env_phase = TRAD_ENV_PHASE_OFF;
+
+    return trad_env;
+}
+
+static int16_t trad_env_run_single(trad_env_data_t* env) {
+    uint32_t tmp;
+    switch (env->env_phase) {
+        case TRAD_ENV_PHASE_OFF:
+            env->env_counter = 0;
+            ;
+            break;
+        case TRAD_ENV_PHASE_ATTACK:
+            tmp = env->env_counter + env->attack;
+            if (tmp < env->env_counter) {  // overflow
+                tmp = ~((uint32_t)0);      // max out
+                env->env_phase = TRAD_ENV_PHASE_DECAY;
+            }
+            env->env_counter = tmp;
+            break;
+        case TRAD_ENV_PHASE_DECAY:
+            tmp = env->env_counter - env->decay;
+            if (tmp > env->env_counter) {  // underflow
+                tmp = 0;                   // bottom out
+            }
+            env->env_counter = tmp;
+
+            if (env->env_counter <= env->sustain) {
+                env->env_counter = env->sustain;
+                env->env_phase = TRAD_ENV_PHASE_SUSTAIN;
+            } else if (env->env_counter < env->gate) {
+                env->env_counter = 0;
+                env->env_phase = TRAD_ENV_PHASE_OFF;
+            }
+            break;
+        case TRAD_ENV_PHASE_SUSTAIN:
+            if (env->sustain == 0) env->env_phase = TRAD_ENV_PHASE_OFF;
+            env->env_counter = env->sustain;
+            break;
+        case TRAD_ENV_PHASE_RELEASE:
+            tmp = env->env_counter - env->release;
+            if (tmp > env->env_counter) {  // underflow
+                tmp = 0;                   // bottom out
+                env->env_phase = TRAD_ENV_PHASE_OFF;
+            }
+            env->env_counter = tmp;
+            /*
+            if(env->env_counter < env->gate){
+                env->env_counter = 0;
+                env->env_phase = TRAD_ENV_PHASE_OFF;
+            }
+            */
+            break;
+    }
+    return env->env_counter >> 17;
+}
+
+#define SAMPLE_RATE_SORRY 48000
+#define TRAD_ENV_UNDERSAMPLING 5
+
+static inline uint32_t trad_env_time_ms_to_val_rise(uint16_t time_ms,
+                                                    uint32_t val) {
+    if (!time_ms) return UINT32_MAX;
+    uint32_t div = time_ms * ((SAMPLE_RATE_SORRY) / 1000);
+    return val / div;
+}
+
+static inline uint32_t uint32_sat_leftshift(uint32_t input, uint16_t left) {
+    if (!left) return input;                      // nothing to do
+    if (input >> (32 - left)) return UINT32_MAX;  // sat
+    return input << left;
+}
+
+void trad_env_run(radspa_t* trad_env, uint16_t num_samples,
+                  uint32_t render_pass_id) {
+    trad_env_data_t* plugin_data = trad_env->plugin_data;
+    radspa_signal_t* output_sig =
+        radspa_signal_get_by_index(trad_env, TRAD_ENV_OUTPUT);
+    radspa_signal_t* phase_sig =
+        radspa_signal_get_by_index(trad_env, TRAD_ENV_PHASE);
+    if ((output_sig->buffer == NULL) && (phase_sig->buffer == NULL)) return;
+    radspa_signal_t* trigger_sig =
+        radspa_signal_get_by_index(trad_env, TRAD_ENV_TRIGGER);
+    radspa_signal_t* input_sig =
+        radspa_signal_get_by_index(trad_env, TRAD_ENV_INPUT);
+    radspa_signal_t* attack_sig = NULL;
+    radspa_signal_t* decay_sig = NULL;
+    radspa_signal_t* sustain_sig = NULL;
+    radspa_signal_t* release_sig = NULL;
+    radspa_signal_t* gate_sig = NULL;
+
+    int16_t ret = output_sig->value;
+
+    for (uint16_t i = 0; i < num_samples; i++) {
+        static int16_t env = 0;
+
+        int16_t trigger =
+            trigger_sig->get_value(trigger_sig, i, num_samples, render_pass_id);
+        int16_t vel = radspa_trigger_get(trigger, &(plugin_data->trigger_prev));
+
+        if (vel < 0) {  // stop
+            if (plugin_data->env_phase != TRAD_ENV_PHASE_OFF) {
+                plugin_data->env_phase = TRAD_ENV_PHASE_RELEASE;
+                plugin_data->release_init_val = plugin_data->env_counter;
+            }
+        } else if (vel > 0) {  // start
+            plugin_data->env_phase = TRAD_ENV_PHASE_ATTACK;
+            plugin_data->velocity = ((uint32_t)vel) << 17;
+        }
+
+        if (!(i % (1 << TRAD_ENV_UNDERSAMPLING))) {
+            uint16_t time_ms;
+            uint32_t sus;
+            switch (plugin_data->env_phase) {
+                case TRAD_ENV_PHASE_OFF:
+                    break;
+                case TRAD_ENV_PHASE_ATTACK:
+                    if (attack_sig == NULL) {
+                        attack_sig = radspa_signal_get_by_index(
+                            trad_env, TRAD_ENV_ATTACK);
+                    }
+                    time_ms = attack_sig->get_value(attack_sig, i, num_samples,
+                                                    render_pass_id);
+                    if (time_ms != plugin_data->attack_prev_ms) {
+                        plugin_data->attack = uint32_sat_leftshift(
+                            trad_env_time_ms_to_val_rise(time_ms, UINT32_MAX),
+                            TRAD_ENV_UNDERSAMPLING);
+                        plugin_data->attack_prev_ms = time_ms;
+                    }
+                    break;
+                case TRAD_ENV_PHASE_DECAY:
+                    if (sustain_sig == NULL) {
+                        sustain_sig = radspa_signal_get_by_index(
+                            trad_env, TRAD_ENV_SUSTAIN);
+                    }
+                    sus = sustain_sig->get_value(sustain_sig, i, num_samples,
+                                                 render_pass_id);
+                    plugin_data->sustain = sus << 17;
+
+                    if (gate_sig == NULL) {
+                        gate_sig =
+                            radspa_signal_get_by_index(trad_env, TRAD_ENV_GATE);
+                    }
+                    sus = gate_sig->get_value(gate_sig, i, num_samples,
+                                              render_pass_id);
+                    plugin_data->gate = sus << 17;
+
+                    if (decay_sig == NULL) {
+                        decay_sig = radspa_signal_get_by_index(trad_env,
+                                                               TRAD_ENV_DECAY);
+                    }
+                    time_ms = decay_sig->get_value(decay_sig, i, num_samples,
+                                                   render_pass_id);
+                    if (time_ms != plugin_data->decay_prev_ms) {
+                        plugin_data->decay = uint32_sat_leftshift(
+                            trad_env_time_ms_to_val_rise(
+                                time_ms, UINT32_MAX - plugin_data->sustain),
+                            TRAD_ENV_UNDERSAMPLING);
+                        plugin_data->decay_prev_ms = time_ms;
+                    }
+                    break;
+                case TRAD_ENV_PHASE_SUSTAIN:
+                    if (sustain_sig == NULL) {
+                        sustain_sig = radspa_signal_get_by_index(
+                            trad_env, TRAD_ENV_SUSTAIN);
+                    }
+                    sus = sustain_sig->get_value(sustain_sig, i, num_samples,
+                                                 render_pass_id);
+                    plugin_data->sustain = sus << 17;
+                    break;
+                case TRAD_ENV_PHASE_RELEASE:
+                    if (gate_sig == NULL) {
+                        gate_sig =
+                            radspa_signal_get_by_index(trad_env, TRAD_ENV_GATE);
+                    }
+                    sus = gate_sig->get_value(gate_sig, i, num_samples,
+                                              render_pass_id);
+                    plugin_data->gate = sus << 17;
+
+                    if (release_sig == NULL) {
+                        release_sig = radspa_signal_get_by_index(
+                            trad_env, TRAD_ENV_RELEASE);
+                    }
+                    time_ms = release_sig->get_value(
+                        release_sig, i, num_samples, render_pass_id);
+                    if (time_ms != plugin_data->release_prev_ms) {
+                        plugin_data->release = uint32_sat_leftshift(
+                            trad_env_time_ms_to_val_rise(
+                                time_ms, plugin_data->release_init_val),
+                            TRAD_ENV_UNDERSAMPLING);
+                        plugin_data->release_prev_ms = time_ms;
+                    }
+                    break;
+            }
+            env = trad_env_run_single(plugin_data);
+        }
+        if (env) {
+            int16_t input =
+                input_sig->get_value(input_sig, i, num_samples, render_pass_id);
+            ret = radspa_mult_shift(env, input);
+        } else {
+            ret = 0;
+        }
+        if (phase_sig->buffer != NULL)
+            (phase_sig->buffer)[i] = plugin_data->env_phase;
+        if (output_sig->buffer != NULL) (output_sig->buffer)[i] = ret;
+    }
+    phase_sig->value = plugin_data->env_phase;
+    output_sig->value = ret;
+}
diff --git a/components/bl00mbox/plugins/trad_synth/trad_synth.h b/components/bl00mbox/plugins/trad_synth/trad_synth.h
new file mode 100644
index 0000000000000000000000000000000000000000..bb144d0faa01e39a4b67b891effff8f3112ce561
--- /dev/null
+++ b/components/bl00mbox/plugins/trad_synth/trad_synth.h
@@ -0,0 +1,44 @@
+#pragma once
+#include <math.h>
+#include "radspa.h"
+#include "radspa_helpers.h"
+
+/* provides traditional synthesizer functionality distributed over several
+ * plugins.
+ */
+
+/* plugin: trad_osc
+ * oscillator that can generate sine, square, saw and triangle waves in the
+ * audio band. uses trad_wave.
+ */
+
+typedef struct {
+    uint32_t counter;
+    int16_t prev_pitch;
+    int32_t incr;
+} trad_osc_data_t;
+
+extern radspa_descriptor_t trad_osc_desc;
+radspa_t* trad_osc_create(uint32_t init_var);
+void trad_osc_run(radspa_t* osc, uint16_t num_samples, uint32_t render_pass_id);
+
+typedef struct {
+    uint32_t env_counter;
+    uint32_t attack;
+    uint32_t decay;
+    uint32_t sustain;
+    uint32_t release;
+    uint32_t release_init_val;
+    uint16_t attack_prev_ms;
+    uint16_t decay_prev_ms;
+    uint16_t release_prev_ms;
+    uint32_t gate;
+    uint32_t velocity;
+    uint8_t env_phase;
+    uint8_t skip_hold;
+    int16_t trigger_prev;
+} trad_env_data_t;
+
+extern radspa_descriptor_t trad_env_desc;
+radspa_t* trad_env_create(uint32_t init_var);
+void trad_env_run(radspa_t* osc, uint16_t num_samples, uint32_t render_pass_id);
diff --git a/components/bl00mbox/radspa/LICENSE b/components/bl00mbox/radspa/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..0e259d42c996742e9e3cba14c677129b2c1b6311
--- /dev/null
+++ b/components/bl00mbox/radspa/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/components/bl00mbox/radspa/radspa.h b/components/bl00mbox/radspa/radspa.h
new file mode 100644
index 0000000000000000000000000000000000000000..759238c1843dc11553810cfe759d6e0d8c5eb2b0
--- /dev/null
+++ b/components/bl00mbox/radspa/radspa.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: CC0-1.0
+
+// Version 0.1.0
+
+/* Realtime Audio Developer's Simple Plugin Api
+ *
+ * Written from scratch but largely inspired by faint memories of the excellent
+ * ladspa.h For a simple plugin implementation example check ampliverter.c/.h :D
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+/* CONVENTIONS
+ *
+ * All plugins communicate all signals in int16_t data.
+ *
+ * SCT (semi-cent, 2/ct) is a mapping between int16_t pitch values and frequency
+ * values comparable to 1V/oct. in analog music electronics. A cent is a common
+ * unit in music theory and describes 1/100 of a semitone or 1/1200 of an
+ * octave. Typically 1 cent is precise enough for synth applications, however
+ * 1/2 cent is better and allows for a 27 octave range. INT16_MAX represents a
+ * frequency of 28160Hz, 6 octaves above middle A (440Hz) which is represented
+ * by INT16_MAX - 6 * 2400.
+ */
+
+// signal hints
+
+#define RADSPA_SIGNAL_HINT_INPUT 1
+#define RADSPA_SIGNAL_HINT_OUTPUT 2
+#define RADSPA_SIGNAL_HINT_TRIGGER 4
+
+#define RADSPA_SIGNAL_HINT_SCT 32
+
+struct _radspa_descriptor_t;
+struct _radspa_signal_t;
+struct _radspa_t;
+
+typedef struct _radspa_descriptor_t {
+    char* name;
+    uint32_t id;  // unique id number
+    char* description;
+    struct _radspa_t* (*create_plugin_instance)(uint32_t init_var);
+    void (*destroy_plugin_instance)(
+        struct _radspa_t* plugin);  // point to radspa_t
+} radspa_descriptor_t;
+
+typedef struct _radspa_signal_t {
+    uint32_t hints;
+    char* name;
+    char* description;
+    char* unit;
+    int8_t name_multiplex;
+
+    int16_t* buffer;  // full buffer of num_samples. may be NULL.
+
+    // used for input channels only
+    int16_t value;  // static value, should be used if buffer is NULL.
+    uint32_t render_pass_id;
+    // function to retrieve value. radspa_helpers provides an example.
+    int16_t (*get_value)(struct _radspa_signal_t* sig, int16_t index,
+                         uint16_t num_samples, uint32_t render_pass_id);
+
+    struct _radspa_signal_t* next;  // signals are in a linked list
+} radspa_signal_t;
+
+typedef struct _radspa_t {
+    const radspa_descriptor_t* descriptor;
+
+    // linked list of all i/o signals of the module and length of list
+    radspa_signal_t* signals;
+    uint8_t len_signals;
+
+    // renders all signal outputs for num_samples if render_pass_id has changed
+    // since the last call, else does nothing.
+    void (*render)(struct _radspa_t* plugin, uint16_t num_samples,
+                   uint32_t render_pass_id);
+
+    // stores id number of render pass.
+    uint32_t render_pass_id;
+
+    void* plugin_data;  // internal data for the plugin to use. should not be
+                        // accessed from outside.
+    uint32_t plugin_table_len;
+    int16_t* plugin_table;
+} radspa_t;
+
+/* REQUIREMENTS
+ * Hosts must provide implementations for the following functions:
+ */
+
+/* The return value should equal frequency(sct) * UINT32_MAX /
+ * (sample_rate>>undersample_pow) with: frequency(sct) = pow(2, (sct +
+ * 2708)/2400)
+ * /!\ Performance critical, might be called on a per-sample basis, do _not_
+ * just use pow()!
+ */
+extern uint32_t radspa_sct_to_rel_freq(int16_t sct, int16_t undersample_pow);
+
+// Return 1 if the buffer wasn't rendered already, 0 otherwise.
+extern bool radspa_host_request_buffer_render(int16_t* buf,
+                                              uint16_t num_samples);
+
+// limit a to -32767..32767
+extern int16_t radspa_clip(int32_t a);
+// saturating int16 addition
+extern int16_t radspa_add_sat(int32_t a, int32_t b);
+// (a*b)>>15
+extern int16_t radspa_mult_shift(int32_t a, int32_t b);
+
+extern int16_t radspa_trigger_start(int16_t velocity, int16_t* hist);
+extern int16_t radspa_trigger_stop(int16_t* hist);
+extern int16_t radspa_trigger_get(int16_t trigger_signal, int16_t* hist);
diff --git a/components/bl00mbox/radspa/radspa_helpers.c b/components/bl00mbox/radspa/radspa_helpers.c
new file mode 100644
index 0000000000000000000000000000000000000000..66848db4e712e5e924b1c785a64c18f4c790c92f
--- /dev/null
+++ b/components/bl00mbox/radspa/radspa_helpers.c
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: CC0-1.0
+#include "radspa_helpers.h"
+
+radspa_signal_t* radspa_signal_get_by_index(radspa_t* plugin,
+                                            uint16_t signal_index) {
+    radspa_signal_t* ret = plugin->signals;
+    for (uint16_t i = 0; i < signal_index; i++) {
+        ret = ret->next;
+        if (ret == NULL) break;
+    }
+    return ret;
+}
+
+void radspa_signal_set(radspa_t* plugin, uint8_t signal_index, char* name,
+                       uint32_t hints, int16_t value) {
+    radspa_signal_t* sig = radspa_signal_get_by_index(plugin, signal_index);
+    if (sig == NULL) return;
+    sig->name = name;
+    sig->hints = hints;
+    sig->value = value;
+}
+
+int16_t radspa_signal_add(radspa_t* plugin, char* name, uint32_t hints,
+                          int16_t value) {
+    radspa_signal_t* sig = calloc(1, sizeof(radspa_signal_t));
+    if (sig == NULL) return -1;  // allocation failed
+    sig->name = name;
+    sig->hints = hints;
+    sig->unit = "";
+    sig->description = "";
+    sig->buffer = NULL;
+    sig->next = NULL;
+    sig->value = value;
+    sig->name_multiplex = -1;
+    sig->get_value = radspa_signal_get_value;
+
+    // find end of linked list
+    uint16_t list_index = 0;
+    if (plugin->signals == NULL) {
+        plugin->signals = sig;
+    } else {
+        radspa_signal_t* sigs = plugin->signals;
+        list_index++;
+        while (sigs->next != NULL) {
+            sigs = sigs->next;
+            list_index++;
+        }
+        sigs->next = sig;
+    }
+    if (plugin->len_signals != list_index) {
+        abort();
+    }
+    plugin->len_signals++;
+    return list_index;
+}
+
+int16_t radspa_signal_get_value(radspa_signal_t* sig, int16_t index,
+                                uint16_t num_samples, uint32_t render_pass_id) {
+    if (sig->buffer != NULL) {
+        if (sig->render_pass_id != render_pass_id) {
+            radspa_host_request_buffer_render(
+                sig->buffer, num_samples);  //, render_pass_id);
+            sig->render_pass_id = render_pass_id;
+        }
+        return sig->buffer[index];
+    }
+    return sig->value;
+}
+
+radspa_t* radspa_standard_plugin_create(radspa_descriptor_t* desc,
+                                        uint8_t num_signals,
+                                        size_t plugin_data_size,
+                                        uint32_t plugin_table_size) {
+    radspa_t* ret = calloc(1, sizeof(radspa_t));
+    if (ret == NULL) return NULL;
+    if (plugin_data_size) {
+        ret->plugin_data = calloc(1, plugin_data_size);
+        if (ret->plugin_data == NULL) {
+            free(ret);
+            return NULL;
+        }
+    }
+    ret->signals = NULL;
+    ret->len_signals = 0;
+    ret->render = NULL;
+    ret->descriptor = desc;
+    ret->plugin_table_len = plugin_table_size;
+
+    bool init_failed = false;
+    for (uint8_t i = 0; i < num_signals; i++) {
+        if (radspa_signal_add(ret, "UNINITIALIZED", 0, 0) == -1) {
+            init_failed = true;
+            break;
+        }
+    }
+
+    if (ret->plugin_table_len) {
+        ret->plugin_table = calloc(plugin_table_size, sizeof(int16_t));
+        if (ret->plugin_table == NULL) init_failed = true;
+    } else {
+        ret->plugin_table = NULL;
+    }
+
+    if (init_failed) {
+        radspa_standard_plugin_destroy(ret);
+        return NULL;
+    }
+    return ret;
+}
+
+void radspa_standard_plugin_destroy(radspa_t* plugin) {
+    radspa_signal_t* sig = plugin->signals;
+    while (sig != NULL) {
+        radspa_signal_t* sig_next = sig->next;
+        free(sig);
+        sig = sig_next;
+    }
+    if (plugin->plugin_table != NULL) free(plugin->plugin_table);
+    if (plugin->plugin_data != NULL) free(plugin->plugin_data);
+    free(plugin);
+}
diff --git a/components/bl00mbox/radspa/radspa_helpers.h b/components/bl00mbox/radspa/radspa_helpers.h
new file mode 100644
index 0000000000000000000000000000000000000000..0db184684991dde91e0bd73dabf8f0d9446523d4
--- /dev/null
+++ b/components/bl00mbox/radspa/radspa_helpers.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: CC0-1.0
+#pragma once
+#include "radspa.h"
+
+// adds signal to plugin instance struct. typically used to initiate a plugin
+// instance.
+int16_t radspa_signal_add(radspa_t* plugin, char* name, uint32_t hints,
+                          int16_t value);
+// as above, but sets parameters of an already existing signal with at list
+// position signal_index
+void radspa_signal_set(radspa_t* plugin, uint8_t signal_index, char* name,
+                       uint32_t hints, int16_t value);
+// get signal struct from a signal index
+radspa_signal_t* radspa_signal_get_by_index(radspa_t* plugin,
+                                            uint16_t signal_index);
+
+radspa_t* radspa_standard_plugin_create(radspa_descriptor_t* desc,
+                                        uint8_t num_signals,
+                                        size_t plugin_data_size,
+                                        uint32_t plugin_table_size);
+void radspa_standard_plugin_destroy(radspa_t* plugin);
+
+// frees all signal structs. typically used to destroy a plugin instance.
+void radspa_signals_free(radspa_t* plugin);
+
+/* returns the value that a signal has at a given moment in time. time is
+ * represented as the buffer index. requests rendering from host and requires
+ * implementation of radspa_host_request_buffer_render.
+ */
+int16_t radspa_signal_get_value(radspa_signal_t* sig, int16_t index,
+                                uint16_t num_samples, uint32_t render_pass_id);
diff --git a/components/micropython/usermodule/micropython.cmake b/components/micropython/usermodule/micropython.cmake
index 05d672a28c4764e1e0bf6ebab0614269f6cc30a0..3ce8ad1cd3c5d15e1fd7232a2b661a4fa73a9ece 100644
--- a/components/micropython/usermodule/micropython.cmake
+++ b/components/micropython/usermodule/micropython.cmake
@@ -8,7 +8,7 @@ target_sources(usermod_badge23 INTERFACE
     ${CMAKE_CURRENT_LIST_DIR}/mp_hardware.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_leds.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_audio.c
-    ${CMAKE_CURRENT_LIST_DIR}/mp_bl00mbox.c
+    ${CMAKE_CURRENT_LIST_DIR}/mp_sys_bl00mbox.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_badge_link.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_imu.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_kernel.c
diff --git a/components/micropython/usermodule/mp_bl00mbox.c b/components/micropython/usermodule/mp_bl00mbox.c
deleted file mode 100644
index 00e83f284e2c8b62efa4203f88701f454ae00356..0000000000000000000000000000000000000000
--- a/components/micropython/usermodule/mp_bl00mbox.c
+++ /dev/null
@@ -1,163 +0,0 @@
-#include <stdio.h>
-
-#include "py/obj.h"
-#include "py/runtime.h"
-
-#include "bl00mbox.h"
-#include "tinysynth.h"
-
-#if !MICROPY_ENABLE_FINALISER
-#error "BADGE23_SYNTH requires MICROPY_ENABLE_FINALISER"
-#endif
-
-typedef struct _synth_tinysynth_obj_t {
-    mp_obj_base_t base;
-    trad_osc_t osc;
-    uint16_t source_index;
-} synth_tinysynth_obj_t;
-
-const mp_obj_type_t synth_tinysynth_type;
-
-STATIC void tinysynth_print(const mp_print_t *print, mp_obj_t self_in,
-                            mp_print_kind_t kind) {
-    (void)kind;
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_print_str(print, "tinysynth(");
-    mp_obj_print_helper(print, mp_obj_new_int(self->source_index), PRINT_REPR);
-    mp_print_str(print, ")");
-}
-
-STATIC mp_obj_t tinysynth_make_new(const mp_obj_type_t *type, size_t n_args,
-                                   size_t n_kw, const mp_obj_t *args) {
-    mp_arg_check_num(n_args, n_kw, 1, 1, true);
-    synth_tinysynth_obj_t *self =
-        m_new_obj_with_finaliser(synth_tinysynth_obj_t);
-    self->base.type = &synth_tinysynth_type;
-
-    self->osc.vol = 32767;
-    self->osc.bend = 1;
-    self->osc.noise_reg = 1;
-    self->osc.waveform = TRAD_OSC_WAVE_TRI;
-
-    trad_osc_set_freq_Hz(&(self->osc), mp_obj_get_float(args[0]));
-    trad_osc_set_attack_ms(&(self->osc), 20);
-    trad_osc_set_decay_ms(&(self->osc), 500);
-    trad_osc_set_sustain(&(self->osc), 0);
-    trad_osc_set_release_ms(&(self->osc), 500);
-    self->source_index = bl00mbox_source_add(&(self->osc), run_trad_osc);
-
-    return MP_OBJ_FROM_PTR(self);
-}
-
-// Class methods
-STATIC mp_obj_t tinysynth_start(mp_obj_t self_in) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_env_start(&(self->osc));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_1(tinysynth_start_obj, tinysynth_start);
-
-STATIC mp_obj_t tinysynth_stop(mp_obj_t self_in) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_env_stop(&(self->osc));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_1(tinysynth_stop_obj, tinysynth_stop);
-
-STATIC mp_obj_t tinysynth_freq(mp_obj_t self_in, mp_obj_t freq) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_freq_Hz(&(self->osc), mp_obj_get_float(freq));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_freq_obj, tinysynth_freq);
-
-STATIC mp_obj_t tinysynth_tone(mp_obj_t self_in, mp_obj_t tone) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_freq_semitone(&(self->osc), mp_obj_get_float(tone));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_tone_obj, tinysynth_tone);
-
-STATIC mp_obj_t tinysynth_waveform(mp_obj_t self_in, mp_obj_t waveform) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_waveform(&(self->osc), mp_obj_get_int(waveform));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_waveform_obj, tinysynth_waveform);
-
-STATIC mp_obj_t tinysynth_attack_ms(mp_obj_t self_in, mp_obj_t attack_ms) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_attack_ms(&(self->osc), mp_obj_get_float(attack_ms));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_attack_ms_obj, tinysynth_attack_ms);
-
-STATIC mp_obj_t tinysynth_decay_ms(mp_obj_t self_in, mp_obj_t decay_ms) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_decay_ms(&(self->osc), mp_obj_get_int(decay_ms));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_decay_ms_obj, tinysynth_decay_ms);
-
-STATIC mp_obj_t tinysynth_release_ms(mp_obj_t self_in, mp_obj_t release_ms) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_release_ms(&(self->osc), mp_obj_get_int(release_ms));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_release_ms_obj, tinysynth_release_ms);
-
-STATIC mp_obj_t tinysynth_sustain(mp_obj_t self_in, mp_obj_t sus) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_sustain(&(self->osc), mp_obj_get_float(sus));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_sustain_obj, tinysynth_sustain);
-
-STATIC mp_obj_t tinysynth_volume(mp_obj_t self_in, mp_obj_t vol) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    trad_osc_set_vol(&(self->osc), mp_obj_get_float(vol));
-    return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_2(tinysynth_volume_obj, tinysynth_volume);
-
-STATIC mp_obj_t tinysynth_deinit(mp_obj_t self_in) {
-    synth_tinysynth_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    bl00mbox_source_remove(self->source_index);
-    return mp_const_none;
-}
-
-MP_DEFINE_CONST_FUN_OBJ_1(tinysynth_deinit_obj, tinysynth_deinit);
-
-STATIC const mp_rom_map_elem_t tinysynth_locals_dict_table[] = {
-    { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&tinysynth_start_obj) },
-    { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&tinysynth_stop_obj) },
-    { MP_ROM_QSTR(MP_QSTR_freq), MP_ROM_PTR(&tinysynth_freq_obj) },
-    { MP_ROM_QSTR(MP_QSTR_tone), MP_ROM_PTR(&tinysynth_tone_obj) },
-    { MP_ROM_QSTR(MP_QSTR_waveform), MP_ROM_PTR(&tinysynth_waveform_obj) },
-    { MP_ROM_QSTR(MP_QSTR_attack_ms), MP_ROM_PTR(&tinysynth_attack_ms_obj) },
-    { MP_ROM_QSTR(MP_QSTR_decay_ms), MP_ROM_PTR(&tinysynth_decay_ms_obj) },
-    { MP_ROM_QSTR(MP_QSTR_volume), MP_ROM_PTR(&tinysynth_sustain_obj) },
-    { MP_ROM_QSTR(MP_QSTR_sustain), MP_ROM_PTR(&tinysynth_sustain_obj) },
-    { MP_ROM_QSTR(MP_QSTR_release_ms), MP_ROM_PTR(&tinysynth_release_ms_obj) },
-    { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&tinysynth_deinit_obj) },
-};
-
-STATIC MP_DEFINE_CONST_DICT(tinysynth_locals_dict, tinysynth_locals_dict_table);
-
-MP_DEFINE_CONST_OBJ_TYPE(synth_tinysynth_type, MP_QSTR_bl00mbox,
-                         MP_TYPE_FLAG_NONE, make_new, tinysynth_make_new, print,
-                         tinysynth_print, locals_dict, &tinysynth_locals_dict);
-
-STATIC const mp_map_elem_t bl00mbox_globals_table[] = {
-    { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_bl00mbox) },
-    { MP_OBJ_NEW_QSTR(MP_QSTR_tinysynth), (mp_obj_t)&synth_tinysynth_type },
-};
-
-STATIC MP_DEFINE_CONST_DICT(mp_module_bl00mbox_globals, bl00mbox_globals_table);
-
-const mp_obj_module_t bl00mbox_user_cmodule = {
-    .base = { &mp_type_module },
-    .globals = (mp_obj_dict_t *)&mp_module_bl00mbox_globals,
-};
-
-MP_REGISTER_MODULE(MP_QSTR_bl00mbox, bl00mbox_user_cmodule);
diff --git a/components/micropython/usermodule/mp_sys_bl00mbox.c b/components/micropython/usermodule/mp_sys_bl00mbox.c
new file mode 100644
index 0000000000000000000000000000000000000000..40668da111bcbdadc6b72b8ffe31261ffbbea9a5
--- /dev/null
+++ b/components/micropython/usermodule/mp_sys_bl00mbox.c
@@ -0,0 +1,543 @@
+// SPDX-License-Identifier: CC0-1.0
+#include <stdio.h>
+
+#include "py/obj.h"
+#include "py/runtime.h"
+
+#include "bl00mbox.h"
+#include "bl00mbox_plugin_registry.h"
+#include "bl00mbox_user.h"
+
+// ========================
+//    PLUGIN OPERATIONS
+// ========================
+STATIC mp_obj_t mp_plugin_index_get_id(mp_obj_t index) {
+    /// prints name
+    radspa_descriptor_t *desc =
+        bl00mbox_plugin_registry_get_descriptor_from_index(
+            mp_obj_get_int(index));
+    if (desc == NULL) return mp_const_none;
+    return mp_obj_new_int(desc->id);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_plugin_index_get_id_obj,
+                                 mp_plugin_index_get_id);
+
+STATIC mp_obj_t mp_plugin_index_get_name(mp_obj_t index) {
+    /// prints name
+    radspa_descriptor_t *desc =
+        bl00mbox_plugin_registry_get_descriptor_from_index(
+            mp_obj_get_int(index));
+    if (desc == NULL) return mp_const_none;
+    return mp_obj_new_str(desc->name, strlen(desc->name));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_plugin_index_get_name_obj,
+                                 mp_plugin_index_get_name);
+
+STATIC mp_obj_t mp_plugin_index_get_description(mp_obj_t index) {
+    /// prints name
+    radspa_descriptor_t *desc =
+        bl00mbox_plugin_registry_get_descriptor_from_index(
+            mp_obj_get_int(index));
+    if (desc == NULL) return mp_const_none;
+    return mp_obj_new_str(desc->description, strlen(desc->description));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_plugin_index_get_description_obj,
+                                 mp_plugin_index_get_description);
+
+STATIC mp_obj_t mp_plugin_registry_num_plugins(void) {
+    return mp_obj_new_int(bl00mbox_plugin_registry_get_plugin_num());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_plugin_registry_num_plugins_obj,
+                                 mp_plugin_registry_num_plugins);
+
+// ========================
+//    CHANNEL OPERATIONS
+// ========================
+
+STATIC mp_obj_t mp_channel_clear(mp_obj_t index) {
+    return mp_obj_new_bool(bl00mbox_channel_clear(mp_obj_get_int(index)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_clear_obj, mp_channel_clear);
+
+STATIC mp_obj_t mp_channel_get_free() {
+    return mp_obj_new_int(bl00mbox_channel_get_free_index());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_channel_get_free_obj, mp_channel_get_free);
+
+STATIC mp_obj_t mp_channel_get_foreground() {
+    return mp_obj_new_int(bl00mbox_channel_get_foreground_index());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_channel_get_foreground_obj,
+                                 mp_channel_get_foreground);
+
+STATIC mp_obj_t mp_channel_set_foreground(mp_obj_t index) {
+    bl00mbox_channel_set_foreground_index(mp_obj_get_int(index));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_set_foreground_obj,
+                                 mp_channel_set_foreground);
+
+STATIC mp_obj_t mp_channel_get_background_mute_override(mp_obj_t index) {
+    bool ret =
+        bl00mbox_channel_get_background_mute_override(mp_obj_get_int(index));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_get_background_mute_override_obj,
+                                 mp_channel_get_background_mute_override);
+
+STATIC mp_obj_t mp_channel_set_background_mute_override(mp_obj_t index,
+                                                        mp_obj_t enable) {
+    bool ret = bl00mbox_channel_set_background_mute_override(
+        mp_obj_get_int(index), mp_obj_is_true(enable));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_set_background_mute_override_obj,
+                                 mp_channel_set_background_mute_override);
+
+STATIC mp_obj_t mp_channel_enable(mp_obj_t chan) {
+    bl00mbox_channel_enable(mp_obj_get_int(chan));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_enable_obj, mp_channel_enable);
+
+STATIC mp_obj_t mp_channel_disable(mp_obj_t chan) {
+    bl00mbox_channel_disable(mp_obj_get_int(chan));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_disable_obj, mp_channel_disable);
+
+STATIC mp_obj_t mp_channel_set_volume(mp_obj_t chan, mp_obj_t vol) {
+    bl00mbox_channel_set_volume(mp_obj_get_int(chan), mp_obj_get_int(vol));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_set_volume_obj,
+                                 mp_channel_set_volume);
+
+STATIC mp_obj_t mp_channel_get_volume(mp_obj_t chan) {
+    return mp_obj_new_int(bl00mbox_channel_get_volume(mp_obj_get_int(chan)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_get_volume_obj,
+                                 mp_channel_get_volume);
+
+STATIC mp_obj_t mp_channel_buds_num(mp_obj_t chan) {
+    return mp_obj_new_int(bl00mbox_channel_buds_num(mp_obj_get_int(chan)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_buds_num_obj, mp_channel_buds_num);
+
+STATIC mp_obj_t mp_channel_get_bud_by_list_pos(mp_obj_t chan, mp_obj_t pos) {
+    return mp_obj_new_int(bl00mbox_channel_get_bud_by_list_pos(
+        mp_obj_get_int(chan), mp_obj_get_int(pos)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_get_bud_by_list_pos_obj,
+                                 mp_channel_get_bud_by_list_pos);
+
+STATIC mp_obj_t mp_channel_conns_num(mp_obj_t chan) {
+    return mp_obj_new_int(bl00mbox_channel_conns_num(mp_obj_get_int(chan)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_conns_num_obj,
+                                 mp_channel_conns_num);
+
+STATIC mp_obj_t mp_channel_mixer_num(mp_obj_t chan) {
+    return mp_obj_new_int(bl00mbox_channel_mixer_num(mp_obj_get_int(chan)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_channel_mixer_num_obj,
+                                 mp_channel_mixer_num);
+
+STATIC mp_obj_t mp_channel_get_bud_by_mixer_list_pos(mp_obj_t chan,
+                                                     mp_obj_t pos) {
+    return mp_obj_new_int(bl00mbox_channel_get_bud_by_mixer_list_pos(
+        mp_obj_get_int(chan), mp_obj_get_int(pos)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_get_bud_by_mixer_list_pos_obj,
+                                 mp_channel_get_bud_by_mixer_list_pos);
+
+STATIC mp_obj_t mp_channel_get_signal_by_mixer_list_pos(mp_obj_t chan,
+                                                        mp_obj_t pos) {
+    return mp_obj_new_int(bl00mbox_channel_get_signal_by_mixer_list_pos(
+        mp_obj_get_int(chan), mp_obj_get_int(pos)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_get_signal_by_mixer_list_pos_obj,
+                                 mp_channel_get_signal_by_mixer_list_pos);
+
+// ========================
+//      BUD OPERATIONS
+// ========================
+
+STATIC mp_obj_t mp_channel_new_bud(mp_obj_t chan, mp_obj_t id,
+                                   mp_obj_t init_var) {
+    bl00mbox_bud_t *bud = bl00mbox_channel_new_bud(
+        mp_obj_get_int(chan), mp_obj_get_int(id), mp_obj_get_int(init_var));
+    if (bud == NULL) return mp_const_none;
+    return mp_obj_new_int(bud->index);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_new_bud_obj, mp_channel_new_bud);
+
+STATIC mp_obj_t mp_channel_delete_bud(mp_obj_t chan, mp_obj_t bud) {
+    bool ret =
+        bl00mbox_channel_delete_bud(mp_obj_get_int(chan), mp_obj_get_int(bud));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_delete_bud_obj,
+                                 mp_channel_delete_bud);
+
+STATIC mp_obj_t mp_channel_bud_exists(mp_obj_t chan, mp_obj_t bud) {
+    bool ret =
+        bl00mbox_channel_bud_exists(mp_obj_get_int(chan), mp_obj_get_int(bud));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_bud_exists_obj,
+                                 mp_channel_bud_exists);
+
+STATIC mp_obj_t mp_channel_bud_get_name(mp_obj_t chan, mp_obj_t bud) {
+    char *name = bl00mbox_channel_bud_get_name(mp_obj_get_int(chan),
+                                               mp_obj_get_int(bud));
+    return mp_obj_new_str(name, strlen(name));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_bud_get_name_obj,
+                                 mp_channel_bud_get_name);
+
+STATIC mp_obj_t mp_channel_bud_get_description(mp_obj_t chan, mp_obj_t bud) {
+    char *description = bl00mbox_channel_bud_get_description(
+        mp_obj_get_int(chan), mp_obj_get_int(bud));
+    return mp_obj_new_str(description, strlen(description));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_bud_get_description_obj,
+                                 mp_channel_bud_get_description);
+
+STATIC mp_obj_t mp_channel_bud_get_plugin_id(mp_obj_t chan, mp_obj_t bud) {
+    uint32_t plugin_id = bl00mbox_channel_bud_get_plugin_id(
+        mp_obj_get_int(chan), mp_obj_get_int(bud));
+    return mp_obj_new_int(plugin_id);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_bud_get_plugin_id_obj,
+                                 mp_channel_bud_get_plugin_id);
+
+STATIC mp_obj_t mp_channel_bud_get_num_signals(mp_obj_t chan, mp_obj_t bud) {
+    uint16_t ret = bl00mbox_channel_bud_get_num_signals(mp_obj_get_int(chan),
+                                                        mp_obj_get_int(bud));
+    return mp_obj_new_int(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_bud_get_num_signals_obj,
+                                 mp_channel_bud_get_num_signals);
+
+// ========================
+//      SIGNAL OPERATIONS
+// ========================
+
+STATIC mp_obj_t mp_channel_bud_get_signal_name(mp_obj_t chan, mp_obj_t bud,
+                                               mp_obj_t signal) {
+    char *name = bl00mbox_channel_bud_get_signal_name(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_str(name, strlen(name));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_bud_get_signal_name_obj,
+                                 mp_channel_bud_get_signal_name);
+
+STATIC mp_obj_t mp_channel_bud_get_signal_description(mp_obj_t chan,
+                                                      mp_obj_t bud,
+                                                      mp_obj_t signal) {
+    char *description = bl00mbox_channel_bud_get_signal_description(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_str(description, strlen(description));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_bud_get_signal_description_obj,
+                                 mp_channel_bud_get_signal_description);
+
+STATIC mp_obj_t mp_channel_bud_get_signal_unit(mp_obj_t chan, mp_obj_t bud,
+                                               mp_obj_t signal) {
+    char *unit = bl00mbox_channel_bud_get_signal_unit(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_str(unit, strlen(unit));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_bud_get_signal_unit_obj,
+                                 mp_channel_bud_get_signal_unit);
+
+STATIC mp_obj_t mp_channel_bud_get_signal_hints(mp_obj_t chan, mp_obj_t bud,
+                                                mp_obj_t signal) {
+    uint32_t val = bl00mbox_channel_bud_get_signal_hints(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_int(val);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_bud_get_signal_hints_obj,
+                                 mp_channel_bud_get_signal_hints);
+
+STATIC mp_obj_t mp_channel_bud_set_signal_value(size_t n_args,
+                                                const mp_obj_t *args) {
+    bool success = bl00mbox_channel_bud_set_signal_value(
+        mp_obj_get_int(args[0]),   // chan
+        mp_obj_get_int(args[1]),   // bud_index
+        mp_obj_get_int(args[2]),   // bud_signal_index
+        mp_obj_get_int(args[3]));  // value
+    return mp_obj_new_bool(success);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_channel_bud_set_signal_value_obj,
+                                           4, 4,
+                                           mp_channel_bud_set_signal_value);
+
+STATIC mp_obj_t mp_channel_bud_get_signal_value(mp_obj_t chan, mp_obj_t bud,
+                                                mp_obj_t signal) {
+    int16_t val = bl00mbox_channel_bud_get_signal_value(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_int(val);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_bud_get_signal_value_obj,
+                                 mp_channel_bud_get_signal_value);
+
+STATIC mp_obj_t mp_channel_subscriber_num(mp_obj_t chan, mp_obj_t bud,
+                                          mp_obj_t signal) {
+    return mp_obj_new_int(bl00mbox_channel_subscriber_num(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_subscriber_num_obj,
+                                 mp_channel_subscriber_num);
+
+STATIC mp_obj_t
+mp_channel_get_bud_by_subscriber_list_pos(size_t n_args, const mp_obj_t *args) {
+    return mp_obj_new_int(bl00mbox_channel_get_bud_by_subscriber_list_pos(
+        mp_obj_get_int(args[0]),  // chan
+        mp_obj_get_int(args[1]),  // bud_index
+        mp_obj_get_int(args[2]),  // bud_signal_index
+        mp_obj_get_int(args[3]))  // pos
+    );
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+    mp_channel_get_bud_by_subscriber_list_pos_obj, 4, 4,
+    mp_channel_get_bud_by_subscriber_list_pos);
+
+STATIC mp_obj_t mp_channel_get_signal_by_subscriber_list_pos(
+    size_t n_args, const mp_obj_t *args) {
+    return mp_obj_new_int(bl00mbox_channel_get_signal_by_subscriber_list_pos(
+        mp_obj_get_int(args[0]),  // chan
+        mp_obj_get_int(args[1]),  // bud_index
+        mp_obj_get_int(args[2]),  // bud_signal_index
+        mp_obj_get_int(args[3]))  // pos
+    );
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+    mp_channel_get_signal_by_subscriber_list_pos_obj, 4, 4,
+    mp_channel_get_signal_by_subscriber_list_pos);
+
+STATIC mp_obj_t mp_channel_get_source_bud(mp_obj_t chan, mp_obj_t bud,
+                                          mp_obj_t signal) {
+    uint64_t val = bl00mbox_channel_get_source_bud(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_int(val);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_get_source_bud_obj,
+                                 mp_channel_get_source_bud);
+
+STATIC mp_obj_t mp_channel_get_source_signal(mp_obj_t chan, mp_obj_t bud,
+                                             mp_obj_t signal) {
+    uint64_t val = bl00mbox_channel_get_source_signal(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_int(val);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_get_source_signal_obj,
+                                 mp_channel_get_source_signal);
+
+// ========================
+//      TABLE OPERATIONS
+// ========================
+
+STATIC mp_obj_t mp_channel_bud_set_table_value(size_t n_args,
+                                               const mp_obj_t *args) {
+    bool success = bl00mbox_channel_bud_set_table_value(
+        mp_obj_get_int(args[0]),   // chan
+        mp_obj_get_int(args[1]),   // bud_index
+        mp_obj_get_int(args[2]),   // table_index
+        mp_obj_get_int(args[3]));  // value
+    return mp_obj_new_bool(success);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_channel_bud_set_table_value_obj,
+                                           4, 4,
+                                           mp_channel_bud_set_table_value);
+
+STATIC mp_obj_t mp_channel_bud_get_table_value(mp_obj_t chan, mp_obj_t bud,
+                                               mp_obj_t table_index) {
+    int16_t val = bl00mbox_channel_bud_get_table_value(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(table_index));
+    return mp_obj_new_int(val);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_bud_get_table_value_obj,
+                                 mp_channel_bud_get_table_value);
+
+STATIC mp_obj_t mp_channel_bud_get_table_len(mp_obj_t chan, mp_obj_t bud) {
+    uint32_t val = bl00mbox_channel_bud_get_table_len(mp_obj_get_int(chan),
+                                                      mp_obj_get_int(bud));
+    return mp_obj_new_int(val);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_channel_bud_get_table_len_obj,
+                                 mp_channel_bud_get_table_len);
+
+// ========================
+//  CONNECTION OPERATIONS
+// ========================
+
+STATIC mp_obj_t mp_channel_disconnect_signal_rx(mp_obj_t chan, mp_obj_t bud,
+                                                mp_obj_t signal) {
+    bool ret = bl00mbox_channel_disconnect_signal_rx(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_disconnect_signal_rx_obj,
+                                 mp_channel_disconnect_signal_rx);
+
+STATIC mp_obj_t mp_channel_disconnect_signal_tx(mp_obj_t chan, mp_obj_t bud,
+                                                mp_obj_t signal) {
+    bool ret = bl00mbox_channel_disconnect_signal_tx(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_disconnect_signal_tx_obj,
+                                 mp_channel_disconnect_signal_tx);
+
+STATIC mp_obj_t mp_channel_connect_signal(size_t n_args, const mp_obj_t *args) {
+    bool success = bl00mbox_channel_connect_signal(
+        mp_obj_get_int(args[0]),   // chan
+        mp_obj_get_int(args[1]),   // bud_tx_index
+        mp_obj_get_int(args[2]),   // bud_tx_signal_index
+        mp_obj_get_int(args[3]),   // bud_tx_index
+        mp_obj_get_int(args[4]));  // bud_tx_signal_index
+    return mp_obj_new_bool(success);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_channel_connect_signal_obj, 5, 5,
+                                           mp_channel_connect_signal);
+
+STATIC mp_obj_t mp_channel_connect_signal_to_output_mixer(
+    mp_obj_t chan, mp_obj_t bud_index, mp_obj_t bud_signal_index) {
+    bool success = bl00mbox_channel_connect_signal_to_output_mixer(
+        mp_obj_get_int(chan), mp_obj_get_int(bud_index),
+        mp_obj_get_int(bud_signal_index));
+    return mp_obj_new_bool(success);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_channel_connect_signal_to_output_mixer_obj,
+                                 mp_channel_connect_signal_to_output_mixer);
+
+STATIC mp_obj_t mp_channel_disconnect_signal_from_output_mixer(
+    mp_obj_t chan, mp_obj_t bud, mp_obj_t signal) {
+    bool ret = bl00mbox_channel_disconnect_signal_from_output_mixer(
+        mp_obj_get_int(chan), mp_obj_get_int(bud), mp_obj_get_int(signal));
+    return mp_obj_new_bool(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_3(
+    mp_channel_disconnect_signal_from_output_mixer_obj,
+    mp_channel_disconnect_signal_from_output_mixer);
+
+STATIC const mp_map_elem_t bl00mbox_globals_table[] = {
+    { MP_OBJ_NEW_QSTR(MP_QSTR___name__),
+      MP_OBJ_NEW_QSTR(MP_QSTR_sys_bl00mbox) },
+
+    // PLUGIN OPERATIONS
+    { MP_ROM_QSTR(MP_QSTR_plugin_registry_num_plugins),
+      MP_ROM_PTR(&mp_plugin_registry_num_plugins_obj) },
+    { MP_ROM_QSTR(MP_QSTR_plugin_index_get_id),
+      MP_ROM_PTR(&mp_plugin_index_get_id_obj) },
+    { MP_ROM_QSTR(MP_QSTR_plugin_index_get_name),
+      MP_ROM_PTR(&mp_plugin_index_get_name_obj) },
+    { MP_ROM_QSTR(MP_QSTR_plugin_index_get_description),
+      MP_ROM_PTR(&mp_plugin_index_get_description_obj) },
+
+    // CHANNEL OPERATIONS
+    { MP_ROM_QSTR(MP_QSTR_channel_get_free),
+      MP_ROM_PTR(&mp_channel_get_free_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_foreground),
+      MP_ROM_PTR(&mp_channel_get_foreground_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_set_foreground),
+      MP_ROM_PTR(&mp_channel_set_foreground_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_background_mute_override),
+      MP_ROM_PTR(&mp_channel_get_background_mute_override_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_set_background_mute_override),
+      MP_ROM_PTR(&mp_channel_set_background_mute_override_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_enable), MP_ROM_PTR(&mp_channel_enable_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_disable),
+      MP_ROM_PTR(&mp_channel_disable_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_clear), MP_ROM_PTR(&mp_channel_clear_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_set_volume),
+      MP_ROM_PTR(&mp_channel_set_volume_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_volume),
+      MP_ROM_PTR(&mp_channel_get_volume_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_buds_num),
+      MP_ROM_PTR(&mp_channel_buds_num_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_bud_by_list_pos),
+      MP_ROM_PTR(&mp_channel_get_bud_by_list_pos_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_conns_num),
+      MP_ROM_PTR(&mp_channel_conns_num_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_mixer_num),
+      MP_ROM_PTR(&mp_channel_mixer_num_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_bud_by_mixer_list_pos),
+      MP_ROM_PTR(&mp_channel_get_bud_by_mixer_list_pos_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_signal_by_mixer_list_pos),
+      MP_ROM_PTR(&mp_channel_get_signal_by_mixer_list_pos_obj) },
+
+    // BUD OPERATIONS
+    { MP_ROM_QSTR(MP_QSTR_channel_new_bud),
+      MP_ROM_PTR(&mp_channel_new_bud_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_delete_bud),
+      MP_ROM_PTR(&mp_channel_delete_bud_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_exists),
+      MP_ROM_PTR(&mp_channel_bud_exists_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_name),
+      MP_ROM_PTR(&mp_channel_bud_get_name_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_description),
+      MP_ROM_PTR(&mp_channel_bud_get_description_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_plugin_id),
+      MP_ROM_PTR(&mp_channel_bud_get_plugin_id_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_num_signals),
+      MP_ROM_PTR(&mp_channel_bud_get_num_signals_obj) },
+
+    // SIGNAL OPERATIONS
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_signal_name),
+      MP_ROM_PTR(&mp_channel_bud_get_signal_name_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_signal_description),
+      MP_ROM_PTR(&mp_channel_bud_get_signal_description_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_signal_unit),
+      MP_ROM_PTR(&mp_channel_bud_get_signal_unit_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_set_signal_value),
+      MP_ROM_PTR(&mp_channel_bud_set_signal_value_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_signal_value),
+      MP_ROM_PTR(&mp_channel_bud_get_signal_value_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_signal_hints),
+      MP_ROM_PTR(&mp_channel_bud_get_signal_hints_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_subscriber_num),
+      MP_ROM_PTR(&mp_channel_subscriber_num_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_bud_by_subscriber_list_pos),
+      MP_ROM_PTR(&mp_channel_get_bud_by_subscriber_list_pos_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_signal_by_subscriber_list_pos),
+      MP_ROM_PTR(&mp_channel_get_signal_by_subscriber_list_pos_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_source_bud),
+      MP_ROM_PTR(&mp_channel_get_source_bud_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_get_source_signal),
+      MP_ROM_PTR(&mp_channel_get_source_signal_obj) },
+
+    // TABLE OPERATIONS
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_set_table_value),
+      MP_ROM_PTR(&mp_channel_bud_set_table_value_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_table_value),
+      MP_ROM_PTR(&mp_channel_bud_get_table_value_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_bud_get_table_len),
+      MP_ROM_PTR(&mp_channel_bud_get_table_len_obj) },
+
+    // CONNECTION OPERATIONS
+    { MP_ROM_QSTR(MP_QSTR_channel_connect_signal),
+      MP_ROM_PTR(&mp_channel_connect_signal_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_disconnect_signal_rx),
+      MP_ROM_PTR(&mp_channel_disconnect_signal_rx_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_disconnect_signal_tx),
+      MP_ROM_PTR(&mp_channel_disconnect_signal_tx_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_connect_signal_to_output_mixer),
+      MP_ROM_PTR(&mp_channel_connect_signal_to_output_mixer_obj) },
+    { MP_ROM_QSTR(MP_QSTR_channel_disconnect_signal_from_output_mixer),
+      MP_ROM_PTR(&mp_channel_disconnect_signal_from_output_mixer_obj) },
+
+    // CONSTANTS
+    { MP_ROM_QSTR(MP_QSTR_NUM_CHANNELS), MP_ROM_INT(BL00MBOX_CHANNELS) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(mp_module_bl00mbox_globals, bl00mbox_globals_table);
+
+const mp_obj_module_t bl00mbox_user_cmodule = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_bl00mbox_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_sys_bl00mbox, bl00mbox_user_cmodule);
diff --git a/components/st3m/st3m_audio.c b/components/st3m/st3m_audio.c
index d8d66aafda14debb8e54736f1b933d303bacb64b..12c3e22a4844682e23dfed68a292996733378102 100644
--- a/components/st3m/st3m_audio.c
+++ b/components/st3m/st3m_audio.c
@@ -276,7 +276,7 @@ void st3m_audio_init(void) {
     _output_apply(&state.speaker);
     _output_apply(&state.headphones);
 
-    xTaskCreate(&_audio_player_task, "audio", 3000, NULL,
+    xTaskCreate(&_audio_player_task, "audio", 10000, NULL,
                 configMAX_PRIORITIES - 1, NULL);
     xTaskCreate(&_jacksense_update_task, "jacksense", 2048, NULL,
                 configMAX_PRIORITIES - 2, NULL);
diff --git a/docs/api/synth.rst b/docs/api/synth.rst
deleted file mode 100644
index eaae62fb9aff5872e250bd712cab0b1b3ffc1452..0000000000000000000000000000000000000000
--- a/docs/api/synth.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-.. py:module:: synth
-
-``synth`` module
-================
-
-.. py:class:: tinysynth
-
-   .. py:method:: start
-   .. py:method:: stop
-   .. py:method:: freq
-   .. py:method:: tone
-   .. py:method:: waveform
-   .. py:method:: attack
-   .. py:method:: decay
diff --git a/docs/badge/bl00mbox.rst b/docs/badge/bl00mbox.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6113157ce3eebc3b437b18e2f8b0adf7167d079a
--- /dev/null
+++ b/docs/badge/bl00mbox.rst
@@ -0,0 +1,230 @@
+.. _bl00mbox:
+
+bl00mbox
+==========
+
+bl00mbox is a modular audio engine designed for the flow3r badge. It it
+suitable for live coding and is best explored in a REPL for the time being.
+
+Upcoming features
+-----------------
+
+(in no specific order)
+
+1) Expose hardware such as captouch and IMU as pseudo-signals that buds can subscribe to. This frees the repl for parameter manipulation while the backend takes care of the playing surface, neat for live coding.
+
+2) Cross-channel connections
+
+3) Stepped value naming
+
+4) Better signal/bud representation in patches
+
+Using patches
+-------------
+
+In bl00mbox all sound sources live in a channel. This allows for easy 
+application and patch management. Ideally the OS should provide each application
+with a channel instance, but you can also spawn one directly:
+
+.. code-block:: pycon
+
+    >>> import bl00mbox
+    # get a channel
+    >>> blm = bl00mbox.Channel()
+    # set channel volume
+    >>> blm.volume = 5000
+
+The easiest way to get sound is to use patches. These are "macroscopic" units
+that directly connect to the output and provide a compact UI. Here's how to
+create one:
+
+.. code-block:: pycon
+
+    # no enter here, press tab instead for autocomplete to see patches
+    >>> bl00mbox.patches.
+    # create a patch instance
+    >>> tiny = blm.new(bl00mbox.patches.tinysynth_fm)
+    # play it!
+    >>> tiny.start()
+    # try autocomplete here too!
+    >>> tiny.
+    # patches come with very individual parameters!
+    >>> tiny.fm_waveform(tiny.SAW)
+    >>> tiny.start()
+
+Using buds
+----------
+
+We can inspect the patch we created earlier:
+
+.. code-block:: pycon
+
+    >>> tiny
+    [patch] tinysynth_fm
+      [bud 32] osc_fm
+        output [output]: 0 => input in [bud 34] ampliverter
+        pitch [input/pitch]: 18367 / 0.0 semitones / 440.0Hz
+        waveform [input]: -1
+        lin_fm [input]: 0 <= output in [bud 35] osc_fm
+      [bud 33] env_adsr
+        output [output]: 0 => gain in [bud 34] ampliverter
+        phase [output]: 0
+        input [input]: 32767
+        trigger [input/trigger]: 0
+        attack [ms] [input]: 20
+        decay [ms] [input]: 1000
+        sustain [ms] [input]: 0
+        release [input]: 100
+        gate [input]: 0
+      [bud 34] ampliverter
+        output [output]: 0 ==> [channel mixer]
+        input [input]: 0 <= output in [bud 32] osc_fm
+        gain [input]: 0 <= output in [bud 33] env_adsr
+        bias [input]: 0
+      [bud 35] osc_fm
+        output [output]: 0 => lin_fm in [bud 32] osc_fm
+        pitch [input/pitch]: 21539 / 15.86 semitones / 1099.801Hz
+        waveform [input]: 1
+        lin_fm [input]: 0
+
+
+The patch is actually composed of buds! Buds are wrappers that contain atomic plugins. Each
+plugin is composed of signals that can be connected to other signals. Signals can have different
+properties that are listed behind their name in square brackets. For starters, each signal is
+either an input or output. Connections always happen between an input and an output. Outputs
+can fan out to multiple inputs, but inputs can only receive data from a single output. If no
+output is connected to an input, it has a static value.
+
+.. note::
+    A special case is the channel mixer (an [input] signal) which only fakes
+    being a bl00mbox signal and can accept multiple outputs.
+
+Let's play around with that a bit more and create some fresh unbothered buds:
+
+.. code-block:: pycon
+
+    # use autocomplete to see plugins
+    >>> bl00mbox.plugins.
+    # print details about specific plugin
+    >>> bl00mbox.plugins.ampliverter
+    # create a new bud
+    >>> osc = blm.new(bl00mbox.plugins.osc_fm)
+    >>> env = blm.new(bl00mbox.plugins.env_adsr)
+
+You can inspect properties of the new buds just as with a patch - in fact, many patches simply print
+all their contained buds and maybe some extra info (but that doesn't have to be the case and is up
+to the patch designer).
+
+.. note::
+    As of now patch designers can hide buds within the internal structure however they like and
+    you kind of have to know where to find stuff. We'll come up with a better solution soon!
+
+.. code-block:: pycon
+
+    # print general info about bud
+    >>> osc
+    [bud 36] osc_fm
+      output [output]: 0
+      pitch [input/pitch]: 18367 / 0.0 semitones / 440.0Hz
+      waveform [input]: -16000
+      lin_fm [input]: 0
+
+    # print info about a specific bud signal
+    >>> env.signals.trigger
+    trigger [input/trigger]: 0
+
+We can connect signals by using the "=" operator. The channel provides its own [input] signal for routing
+audio to the audio outputs. Let's connect the oscillator to it:
+
+.. code-block:: pycon
+
+    # assign an output to an input...
+    >>> env.signals.input = osc.signals.output
+    # ...or an input to an output!
+    >>> env.signals.output = blm.mixer
+
+Earlier we saw that env.signals.trigger is of type [input/trigger]. The [trigger] type comes with a special
+function to start an event:
+
+.. code-block:: pycon
+
+    # you should hear something when calling this!
+    >>> env.signals.trigger.start()
+
+If a signal is an input you can directly assign a value to it. Some signal types come with special setter
+functions, for example [pitch] types support multiple abstract input concepts:
+
+.. code-block:: pycon
+
+    # assign raw value to an input signal
+    >>> env.signals.sustain = 16000
+    # assign a abstract numeric value to a [pitch] with a special setter
+    >>> osc.signals.pitch.freq = 220
+    >>> osc.signals.pitch.tone = "Gb4"
+
+Raw signal values range generally from -32767..32767. Since sustain is nonzero now, the tone doesn't
+automatically stop after calling .start()
+
+.. code-block:: pycon
+
+    # plays forever...
+    >>> env.signals.trigger.start()
+    # ...until you call this!
+    >>> env.signals.trigger.start()
+
+
+Example 1: Auto bassline
+------------------------
+
+.. code-block:: pycon
+
+    >>> import bl00mbox
+
+    >>> blm = bl00mbox.Channel()
+    >>> blm.volume = 10000
+    >>> osc1 = blm.new(bl00mbox.plugins.osc_fm)
+    >>> env1 = blm.new(bl00mbox.plugins.env_adsr)
+    >>> env1.signals.output = blm.mixer
+    >>> env1.signals.input = osc1.signals.output
+
+    >>> osc2 = blm.new(bl00mbox.plugins.osc_fm)
+    >>> env2 = blm.new(bl00mbox.plugins.env_adsr)
+    >>> env2.signals.input = osc2.signals.output
+
+    >>> env2.signals.output = osc1.signals.lin_fm
+
+    >>> env1.signals.sustain = 0
+    >>> env2.signals.sustain = 0
+    >>> env1.signals.attack = 10
+    >>> env2.signals.attack = 100
+    >>> env1.signals.decay = 800
+    >>> env2.signals.decay = 800
+
+    >>> osc1.signals.pitch.tone = -12
+    >>> osc2.signals.pitch.tone = -24
+
+    >>> osc3 = blm.new(bl00mbox.plugins.osc_fm)
+    >>> osc3.signals.waveform = 0
+    >>> osc3.signals.pitch.tone = -100
+    >>> osc3.signals.output = env1.signals.trigger
+    >>> osc3.signals.output = env2.signals.trigger
+
+    >>> osc4 = blm.new(bl00mbox.plugins.osc_fm)
+    >>> osc4.signals.waveform = 32767
+    >>> osc4.signals.pitch.tone = -124
+
+    >>> amp1 = blm.new(bl00mbox.plugins.ampliverter)
+    >>> amp1.signals.input = osc4.signals.output
+    >>> amp1.signals.bias = 18376 - 2400
+    >>> amp1.signals.gain = 300
+
+    >>> amp1.signals.output = osc1.signals.pitch
+
+    >>> amp2 = blm.new(bl00mbox.plugins.ampliverter)
+    >>> amp2.signals.input = amp1.signals.output
+    >>> amp2.signals.bias = - 2400
+    >>> amp2.signals.gain = 31000
+
+    >>> amp2.signals.output = osc2.signals.pitch
+    >>> osc2.signals.output = blm.mixer
+
diff --git a/docs/index.rst b/docs/index.rst
index 36f67ff4bfb39a1a6849e2d536ed55475e0dade9..93040c68a4e73d12a89b84a483b31c627b9a0958 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -16,6 +16,7 @@ Welcome to flow3r's documentation!
    badge/firmware.rst
    badge/firmware-development.rst
    badge/badge_link.rst
+   badge/bl00mbox.rst
 
 .. toctree::
    :maxdepth: 1
@@ -27,10 +28,8 @@ Welcome to flow3r's documentation!
    api/hardware.rst
    api/kernel.rst
    api/leds.rst
-   api/synth.rst
    api/ctx.rst
 
-
 Indices and tables
 ==================
 
diff --git a/main/main.c b/main/main.c
index 3b680572630e1fc61a6722d0166b8127c25caaad..a4e8be6e316126f5f3897da8290a57e15f8416d5 100644
--- a/main/main.c
+++ b/main/main.c
@@ -129,7 +129,7 @@ void flow3r_startup(void) {
 
     st3m_scope_init();
     st3m_audio_init();
-    st3m_audio_set_player_function(bl00mbox_player_function);
+    bl00mbox_init();
 
     st3m_mode_set(st3m_mode_kind_starting, "micropython");
     st3m_mode_update_display(NULL);
diff --git a/python_payload/apps/demo_harmonic/__init__.py b/python_payload/apps/demo_harmonic/__init__.py
index 573ae3c144778c60e2574189bf93aa6f27dffaf1..936207e9dbd815dbe3e91b32b97a2f80a02e321c 100644
--- a/python_payload/apps/demo_harmonic/__init__.py
+++ b/python_payload/apps/demo_harmonic/__init__.py
@@ -1,5 +1,7 @@
-from bl00mbox import tinysynth
 import captouch
+import bl00mbox
+
+blm = bl00mbox.Channel()
 import leds
 import hardware
 
@@ -15,7 +17,6 @@ chords = [
     [3, 7, 10, 14, 15],
 ]
 
-
 from st3m.application import Application
 
 
@@ -26,27 +27,21 @@ class HarmonicApp(Application):
         self.color_intensity = 0.0
         self.chord_index = 0
         self.chord: List[int] = []
-        self.synths = [tinysynth(440) for i in range(15)]
+        self.synths = [blm.new(bl00mbox.patches.tinysynth_fm) for i in range(5)]
+        self.cp_prev = captouch.read()
+
         for i, synth in enumerate(self.synths):
-            synth.decay_ms(100)
-            synth.sustain(0.5)
-            if i < 5:
-                synth.waveform(1)
-                synth.volume(0.5)
-                synth.release_ms(1200)
-            elif i < 10:
-                synth.waveform(1)
-                synth.attack_ms(300)
-                synth.volume(0.1)
-                synth.sustain(0.9)
-                synth.release_ms(2400)
-            else:
-                synth.waveform(1)
-                synth.attack_ms(500)
-                synth.volume(0.03)
-                synth.sustain(0.9)
-                synth.release_ms(800)
+            synth.decay(500)
+            synth.waveform(-32767)
+            synth.attack(50)
+            synth.volume(0.3)
+            synth.sustain(0.9)
+            synth.release(800)
+            synth.fm_waveform(-32767)
+            synth.fm(1.5)
+
         self._set_chord(3)
+        self.prev_captouch = [0] * 10
 
     def _set_chord(self, i: int) -> None:
         hue = int(72 * (i + 0.5)) % 360
@@ -71,22 +66,17 @@ class HarmonicApp(Application):
             self.color_intensity -= self.color_intensity / 20
         cts = captouch.read()
         for i in range(10):
-            if cts.petals[i].pressed:
+            if cts.petals[i].pressed and (not self.cp_prev.petals[i].pressed):
                 if i % 2:
                     k = int((i - 1) / 2)
                     self._set_chord(k)
                 else:
                     k = int(i / 2)
                     self.synths[k].tone(self.chord[k])
-                    self.synths[k + 5].tone(12 + self.chord[k])
-                    self.synths[k + 10].tone(7 + self.chord[k])
                     self.synths[k].start()
-                    self.synths[k + 5].start()
-                    self.synths[k + 10].start()
                     self.color_intensity = 1.0
-            else:
+            elif (not cts.petals[i].pressed) and self.cp_prev.petals[i].pressed:
                 if (1 + i) % 2:
                     k = int(i / 2)
                     self.synths[k].stop()
-                    self.synths[k + 5].stop()
-                    self.synths[k + 10].stop()
+        self.cp_prev = cts
diff --git a/python_payload/apps/demo_melodic/__init__.py b/python_payload/apps/demo_melodic/__init__.py
index 26e3fc8435ee00f7ce67d436cbe9873a543fea1f..f0dfcb6c3eab838c9242a7738a63f2a2a074cc23 100644
--- a/python_payload/apps/demo_melodic/__init__.py
+++ b/python_payload/apps/demo_melodic/__init__.py
@@ -1,4 +1,7 @@
-from bl00mbox import tinysynth
+import bl00mbox
+
+blm = bl00mbox.Channel()
+
 from hardware import *
 import captouch
 import leds
@@ -9,7 +12,7 @@ from st3m.ui.view import ViewManager
 from st3m.ui.ctx import Ctx
 
 octave = 0
-synths: List[tinysynth] = []
+synths = []
 scale = [0, 2, 4, 5, 7, 9, 11]
 
 
@@ -71,9 +74,9 @@ def run(ins: InputState) -> None:
 def init() -> None:
     global synths
     for i in range(1):
-        synths += [tinysynth(440)]
+        synths += [blm.new_patch(bl00mbox.patches.tinysynth_fm)]
     for synth in synths:
-        synth.decay_ms(100)
+        synth.decay(100)
 
 
 def foreground() -> None:
diff --git a/python_payload/apps/simple_drums/__init__.py b/python_payload/apps/simple_drums/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3cf02db4e19e83d8f0974a48451b0cb150b54632
--- /dev/null
+++ b/python_payload/apps/simple_drums/__init__.py
@@ -0,0 +1,127 @@
+import bl00mbox
+import hardware
+import captouch
+import leds
+
+from st3m.application import Application
+from st3m.ui.ctx import Ctx
+from st3m.input import InputState
+
+
+class Dot:
+    def __init__(self, sizex, sizey, imag, real, col):
+        self.sizex = sizex
+        self.sizey = sizey
+        self.imag = imag
+        self.real = real
+        self.col = col
+
+    def draw(self, i, ctx):
+        imag = self.imag
+        real = self.real
+        sizex = self.sizex
+        sizey = self.sizey
+        col = self.col
+
+        ctx.rgb(*col).rectangle(
+            -int(imag + (sizex / 2)), -int(real + (sizey / 2)), sizex, sizey
+        ).fill()
+
+
+class SimpleDrums(Application):
+    def __init__(self, name: str) -> None:
+        super().__init__(name)
+        # ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
+        self.blm = bl00mbox.Channel()
+        self.seq = self.blm.new(bl00mbox.patches.step_sequencer)
+        self.hat = self.blm.new(bl00mbox.patches.sampler, "hihat.wav")
+        # Dot(10, 10, -30, 0, self._track_col(0)).draw(0,ctx)
+        self.kick = self.blm.new(bl00mbox.patches.sampler, "kick.wav")
+        # Dot(20, 20, 0, 40, self._track_col(1)).draw(0,ctx)
+        self.snare = self.blm.new(bl00mbox.patches.sampler, "snare.wav")
+        # Dot(30, 30, 2, -20, self._track_col(2)).draw(0,ctx)
+        self.kick.sampler.signals.trigger = self.seq.seqs[0].signals.output
+        self.hat.sampler.signals.trigger = self.seq.seqs[1].signals.output
+        self.snare.sampler.signals.trigger = self.seq.seqs[2].signals.output
+        self.ct_prev = captouch.read()
+        self.track = 0
+        self.seq.bpm = 80
+        self.blm.background_mute_override = True
+
+    def _highlight_petal(self, num, r, g, b):
+        for i in range(5):
+            leds.set_rgb((4 * num - i + 2) % 40, r, g, b)
+
+    def on_foreground(self):
+        pass
+
+    def _track_col(self, track, smol=False):
+        rgb = (20, 20, 20)
+        if track == 0:
+            rgb = (0, 255, 0)
+        elif track == 1:
+            rgb = (0, 0, 255)
+        elif track == 2:
+            rgb = (255, 0, 0)
+        if smol:
+            rgb = [x / 256 for x in rgb]
+        return rgb
+
+    def draw(self, ctx):
+        dots = []
+        groupgap = 4
+        for i in range(4):
+            if self.ct_prev.petals[4 - i].pressed:
+                dots.append(
+                    Dot(
+                        48 + groupgap,
+                        40,
+                        int((12 * 4 + groupgap) * (1.5 - i)),
+                        0,
+                        (0.1, 0.1, 0.1),
+                    )
+                )
+
+        for track in range(3):
+            rgb = self._track_col(track, smol=True)
+            y = 12 * (track - 1)
+            for i in range(16):
+                trigger_state = self.seq.trigger_state(track, i)
+                size = 2
+                if trigger_state:
+                    size = 8
+                x = 12 * (7.5 - i)
+                x += groupgap * (1.5 - (i // 4))
+                x = int(x)
+                dots.append(Dot(size, size, x, y, rgb))
+
+        dots.append(Dot(1, 40, 0, 0, (0.5, 0.5, 0.5)))
+        dots.append(Dot(1, 40, 4 * 12 + groupgap, 0, (0.5, 0.5, 0.5)))
+        dots.append(Dot(1, 40, -4 * 12 - groupgap, 0, (0.5, 0.5, 0.5)))
+
+        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
+        for i, dot in enumerate(dots):
+            dot.draw(i, ctx)
+        return
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        super().think(ins, delta_ms)
+        st = self.seq.seqs[0].signals.step.value
+        leds.set_all_rgb(0, 0, 0)
+        rgb = self._track_col(self.track)
+        self._highlight_petal(4 - (st // 4), *rgb)
+        self._highlight_petal(6 + (st % 4), *rgb)
+        leds.update()
+        ct = captouch.read()
+        for i in range(4):
+            if ct.petals[4 - i].pressed:
+                for j in range(4):
+                    if ct.petals[6 + j].pressed and not (
+                        self.ct_prev.petals[6 + j].pressed
+                    ):
+                        self.seq.trigger_toggle(self.track, i * 4 + j)
+        if ct.petals[5].pressed and not (self.ct_prev.petals[5].pressed):
+            self.track = (self.track + 1) % 3
+        if ct.petals[0].pressed and not (self.ct_prev.petals[0].pressed):
+            self.track = (self.track + 1) % 3
+        self.ct_prev = ct
diff --git a/python_payload/apps/simple_drums/flow3r.toml b/python_payload/apps/simple_drums/flow3r.toml
new file mode 100644
index 0000000000000000000000000000000000000000..6d2308c5ddaa060695b701b3384fac7529c02779
--- /dev/null
+++ b/python_payload/apps/simple_drums/flow3r.toml
@@ -0,0 +1,11 @@
+[app]
+name = "Simple Drums"
+menu = "Music"
+
+[entry]
+class = "SimpleDrums"
+
+[metadata]
+author = "Flow3r Badge Authors"
+license = "LGPL-3.0-only"
+url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
diff --git a/python_payload/bl00mbox/LICENSE b/python_payload/bl00mbox/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..0e259d42c996742e9e3cba14c677129b2c1b6311
--- /dev/null
+++ b/python_payload/bl00mbox/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/python_payload/bl00mbox/__init__.py b/python_payload/bl00mbox/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff0f2e2fa831135e0e7e5667f4faa1e785087001
--- /dev/null
+++ b/python_payload/bl00mbox/__init__.py
@@ -0,0 +1,515 @@
+# SPDX-License-Identifier: CC0-1.0
+
+import sys_bl00mbox
+
+# note: consider the 'sys_bl00mbox' api super unstable for now pls :3
+import math
+
+import bl00mbox._patches as patches
+import bl00mbox._helpers as helpers
+from bl00mbox._patches import _Patch as PatchType
+from bl00mbox._plugins import plugins
+from bl00mbox._plugins import _Plugin as PluginType
+
+
+class Bl00mboxError(Exception):
+    pass
+
+
+def _makeSignal(bud, signal_num):
+    hints = sys_bl00mbox.channel_bud_get_signal_hints(
+        bud.channel_num, bud.bud_num, signal_num
+    )
+    if hints & 2:
+        signal = SignalOutput(bud, signal_num)
+        signal._hints = "output"
+    elif hints & 1:
+        if hints & 4:
+            signal = SignalInputTrigger(bud, signal_num)
+            signal._hints = "input/trigger"
+        elif hints & 32:
+            signal = SignalInputPitch(bud, signal_num)
+            signal._hints = "input/pitch"
+        else:
+            signal = SignalInput(bud, signal_num)
+            signal._hints = "input"
+    return signal
+
+
+class ChannelMixer:
+    def __init__(self, channel):
+        self._channel = channel
+        pass
+
+    def __repr__(self):
+        ret = "[channel mixer]"
+        ret += " (" + str(len(self.connections)) + " connections)"
+        for con in self.connections:
+            ret += "\n  " + con.name
+            ret += " in [bud " + str(con._bud.bud_num) + "] " + con._bud.name
+        return ret
+
+    def _unplug_all(self):
+        # TODO
+        pass
+
+    @property
+    def connections(self):
+        ret = []
+        for i in range(sys_bl00mbox.channel_mixer_num(self._channel.channel_num)):
+            b = sys_bl00mbox.channel_get_bud_by_mixer_list_pos(
+                self._channel.channel_num, i
+            )
+            s = sys_bl00mbox.channel_get_signal_by_mixer_list_pos(
+                self._channel.channel_num, i
+            )
+            sig = Signal(Bud(self._channel, 0, bud_num=b), s)
+            ret += [sig]
+        return ret
+
+
+class Signal:
+    def __init__(self, bud, signal_num):
+        self._bud = bud
+        self._signal_num = signal_num
+        self._name = sys_bl00mbox.channel_bud_get_signal_name(
+            bud.channel_num, bud.bud_num, signal_num
+        )
+        self._description = sys_bl00mbox.channel_bud_get_signal_description(
+            bud.channel_num, bud.bud_num, signal_num
+        )
+        self._unit = sys_bl00mbox.channel_bud_get_signal_unit(
+            bud.channel_num, bud.bud_num, signal_num
+        )
+        self._hints = ""
+
+    def __repr__(self):
+        self._bud._check_existence()
+
+        ret = self.name
+        if len(self.unit):
+            ret += " [" + self.unit + "]"
+        ret += " [" + self.hints + "]: "
+        conret = []
+        direction = " <?> "
+        if isinstance(self, SignalInput):
+            direction = " <= "
+            if len(self.connections):
+                ret += str(self.connections[0].value)
+            else:
+                ret += str(self.value)
+        elif isinstance(self, SignalOutput):
+            direction = " => "
+            ret += str(self.value)
+
+        for con in self.connections:
+            if isinstance(con, Signal):
+                conret += [
+                    direction
+                    + con.name
+                    + " in [bud "
+                    + str(con._bud.bud_num)
+                    + "] "
+                    + con._bud.name
+                ]
+            if isinstance(con, ChannelMixer):
+                conret += [" ==> [channel mixer]"]
+        nl = "\n"
+        if len(conret) > 1:
+            ret += "\n"
+            nl += "  "
+        ret += nl.join(conret)
+        return ret
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def description(self):
+        return self._description
+
+    @property
+    def unit(self):
+        return self._unit
+
+    @property
+    def hints(self):
+        return self._hints
+
+    @property
+    def connections(self):
+        return []
+
+    @property
+    def value(self):
+        self._bud._check_existence()
+        return sys_bl00mbox.channel_bud_get_signal_value(
+            self._bud.channel_num, self._bud.bud_num, self._signal_num
+        )
+
+
+class SignalOutput(Signal):
+    @Signal.value.setter
+    def value(self, val):
+        if val == None:
+            sys_bl00mbox.channel_disconnect_signal_tx(
+                self._bud.channel_num, self._bud.bud_num, self._signal_num
+            )
+        elif isinstance(val, SignalInput):
+            val.value = self
+        elif isinstance(val, ChannelMixer):
+            if val._channel.channel_num == self._bud.channel_num:
+                sys_bl00mbox.channel_connect_signal_to_output_mixer(
+                    self._bud.channel_num, self._bud.bud_num, self._signal_num
+                )
+
+    @property
+    def connections(self):
+        cons = []
+        chan = self._bud.channel_num
+        bud = self._bud.bud_num
+        sig = self._signal_num
+        for i in range(sys_bl00mbox.channel_subscriber_num(chan, bud, sig)):
+            b = sys_bl00mbox.channel_get_bud_by_subscriber_list_pos(chan, bud, sig, i)
+            s = sys_bl00mbox.channel_get_signal_by_subscriber_list_pos(
+                chan, bud, sig, i
+            )
+            if (s >= 0) and (b > 0):
+                cons += [_makeSignal(Bud(Channel(chan), 0, bud_num=b), s)]
+            elif s == -1:
+                cons += [ChannelMixer(Channel(chan))]
+        return cons
+
+
+class SignalInput(Signal):
+    @Signal.value.setter
+    def value(self, val):
+        self._bud._check_existence()
+        if isinstance(val, SignalOutput):
+            if len(self.connections):
+                if not sys_bl00mbox.channel_disconnect_signal_rx(
+                    self._bud.channel_num, self._bud.bud_num, self._signal_num
+                ):
+                    return
+            sys_bl00mbox.channel_connect_signal(
+                self._bud.channel_num,
+                self._bud.bud_num,
+                self._signal_num,
+                val._bud.bud_num,
+                val._signal_num,
+            )
+        elif isinstance(val, SignalInput):
+            # TODO
+            pass
+        elif (type(val) == int) or (type(val) == float):
+            if len(self.connections):
+                if not sys_bl00mbox.channel_disconnect_signal_rx(
+                    self._bud.channel_num, self._bud.bud_num, self._signal_num
+                ):
+                    return
+            sys_bl00mbox.channel_bud_set_signal_value(
+                self._bud.channel_num, self._bud.bud_num, self._signal_num, int(val)
+            )
+
+    @property
+    def connections(self):
+        cons = []
+        chan = self._bud.channel_num
+        bud = self._bud.bud_num
+        sig = self._signal_num
+        b = sys_bl00mbox.channel_get_source_bud(chan, bud, sig)
+        s = sys_bl00mbox.channel_get_source_signal(chan, bud, sig)
+        if (s >= 0) and (b > 0):
+            cons += [_makeSignal(Bud(Channel(chan), 0, bud_num=b), s)]
+        return cons
+
+
+class SignalInputTrigger(SignalInput):
+    def start(self, velocity=32767):
+        if self.value > 0:
+            self.value = -velocity
+        else:
+            self.value = velocity
+
+    def stop(self):
+        self.value = 0
+
+
+class SignalInputPitch(SignalInput):
+    def __init__(self, bud, signal_num):
+        SignalInput.__init__(self, bud, signal_num)
+
+    @property
+    def tone(self):
+        return (self.value - (32767 - 2400 * 6)) / 200
+
+    @tone.setter
+    def tone(self, val):
+        if (type(val) == int) or (type(val) == float):
+            self.value = (32767 - 2400 * 6) + 200 * val
+        if type(val) == str:
+            self.value = helpers.note_name_to_sct(val)
+
+    @property
+    def freq(self):
+        tone = (self.value - (32767 - 2400 * 6)) / 200
+        return 440 * (2 ** (tone / 12))
+
+    @freq.setter
+    def freq(self, val):
+        tone = 12 * math.log(val / 440, 2)
+        self.value = (32767 - 2400 * 6) + 200 * tone
+
+    def __repr__(self):
+        ret = SignalInput.__repr__(self)
+        ret += " / " + str(self.tone) + " semitones / " + str(self.freq) + "Hz"
+        return ret
+
+
+class SignalList:
+    def __init__(self, bud):
+        self._list = []
+        for signal_num in range(
+            sys_bl00mbox.channel_bud_get_num_signals(bud.channel_num, bud.bud_num)
+        ):
+            hints = sys_bl00mbox.channel_bud_get_signal_hints(
+                bud.channel_num, bud.bud_num, signal_num
+            )
+            if hints & 2:
+                signal = SignalOutput(bud, signal_num)
+                signal._hints = "output"
+            elif hints & 1:
+                if hints & 4:
+                    signal = SignalInputTrigger(bud, signal_num)
+                    signal._hints = "input/trigger"
+                elif hints & 32:
+                    signal = SignalInputPitch(bud, signal_num)
+                    signal._hints = "input/pitch"
+                else:
+                    signal = SignalInput(bud, signal_num)
+                    signal._hints = "input"
+            self._list += [signal]
+            setattr(self, signal.name.split(" ")[0], signal)
+
+    def __setattr__(self, key, value):
+        current_value = getattr(self, key, None)
+        if isinstance(current_value, Signal):
+            current_value.value = value
+            return
+        super().__setattr__(key, value)
+
+
+class Bud:
+    def __init__(self, channel, plugin_id, init_var=0, bud_num=None):
+        self._channel_num = channel.channel_num
+        if bud_num == None:
+            self._plugin_id = plugin_id
+            self._bud_num = sys_bl00mbox.channel_new_bud(
+                self.channel_num, self.plugin_id, init_var
+            )
+            if self._bud_num == None:
+                raise Bl00mboxError("bud init failed")
+        else:
+            self._bud_num = bud_num
+            self._check_existence()
+            self._name = sys_bl00mbox.channel_bud_get_plugin_id(
+                self.channel_num, self.bud_num
+            )
+        self._name = sys_bl00mbox.channel_bud_get_name(self.channel_num, self.bud_num)
+        self._signals = SignalList(self)
+
+    def __repr__(self):
+        self._check_existence()
+        ret = "[bud " + str(self.bud_num) + "] " + self.name
+        for sig in self.signals._list:
+            ret += "\n  " + "\n  ".join(repr(sig).split("\n"))
+        return ret
+
+    def __del__(self):
+        self._check_existence()
+        sys_bl00mbox.channel_delete_bud(self.channel_num, self.bud_num)
+
+    def _check_existence(self):
+        if not sys_bl00mbox.channel_bud_exists(self.channel_num, self.bud_num):
+            raise Bl00mboxError("bud has been deleted")
+
+    @property
+    def channel_num(self):
+        return self._channel_num
+
+    @property
+    def plugin_id(self):
+        return self._plugin_id
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def bud_num(self):
+        return self._bud_num
+
+    @property
+    def signals(self):
+        return self._signals
+
+    @property
+    def table(self):
+        ret = []
+        for x in range(
+            sys_bl00mbox.channel_bud_get_table_len(self.channel_num, self.bud_num)
+        ):
+            ret += [
+                sys_bl00mbox.channel_bud_get_table_value(
+                    self.channel_num, self.bud_num, x
+                )
+            ]
+        return ret
+
+    @table.setter
+    def table(self, stuff):
+        max_len = sys_bl00mbox.channel_bud_get_table_len(self.channel_num, self.bud_num)
+        if len(stuff) > max_len:
+            stuff = stuff[:max_len]
+        for x, y in enumerate(stuff):
+            sys_bl00mbox.channel_bud_set_table_value(
+                self.channel_num, self.bud_num, x, y
+            )
+
+
+class ChannelOverview:
+    def __repr__(self, nonempty=False):
+        ret = (
+            "[channel list]\n  foreground: [channel "
+            + str(sys_bl00mbox.channel_get_foreground())
+            + "]"
+        )
+        for i in range(sys_bl00mbox.NUM_CHANNELS):
+            c = Channel(i)
+            if nonempty:
+                if not len(c.buds):
+                    continue
+            ret += "\n" + repr(c)
+        return ret
+
+
+class Channel:
+    show_all = ChannelOverview()
+    show_nonempty = ChannelOverview(nonempty=True)
+
+    def __init__(self, num=None):
+        if num == None:
+            self._channel_num = sys_bl00mbox.channel_get_free()
+        elif (int(num) < sys_bl00mbox.NUM_CHANNELS) and (int(num >= 0)):
+            self._channel_num = int(num)
+
+    def __repr__(self):
+        ret = "[channel " + str(self.channel_num) + "]"
+        if self.foreground:
+            ret += " (foreground)"
+        if self.background_mute_override:
+            ret += " (background mute override)"
+        ret += "\n  volume: " + str(self.volume)
+        b = sys_bl00mbox.channel_buds_num(self.channel_num)
+        ret += "\n  buds: " + str(b)
+        if len(self.buds) != b:
+            ret += " (desync" + str(len(self.buds)) + ")"
+        ret += "\n  " + "\n  ".join(repr(self.mixer).split("\n"))
+        return ret
+
+    def clear(self):
+        sys_bl00mbox.channel_clear(self.channel_num)
+
+    def new_bud(self, thing, init_var=None):
+        bud_init_var = 0
+        if (type(init_var) == int) or (type(init_var) == float):
+            bud_init_var = int(init_var)
+        bud = None
+        if isinstance(thing, PluginType):
+            bud = Bud(self, thing.plugin_id, bud_init_var)
+        if type(thing) == int:
+            bud = Bud(self, thing, bud_init_var)
+        return bud
+
+    def new_patch(self, patch, init_var=None):
+        if init_var == None:
+            return patch(self)
+        else:
+            return patch(self, init_var)
+
+    @staticmethod
+    def all():
+        ret = (
+            "[channel list]\n  foreground: [channel "
+            + str(sys_bl00mbox.channel_get_foreground())
+            + "]"
+        )
+        for i in range(sys_bl00mbox.NUM_CHANNELS):
+            c = Channel(i)
+            ret += "\n" + repr(c)
+        return ret
+
+    def new(self, thing, init_var=None):
+        if type(thing) == type:
+            if issubclass(thing, PatchType):
+                return self.new_patch(thing, init_var)
+        if isinstance(thing, PluginType) or (type(thing) == int):
+            return self.new_bud(thing, init_var)
+
+    @property
+    def buds(self):
+        buds = []
+        for i in range(sys_bl00mbox.channel_buds_num(self.channel_num)):
+            b = sys_bl00mbox.channel_get_bud_by_list_pos(self.channel_num, i)
+            bud = Bud(self, 0, bud_num=b)
+            buds += [bud]
+        return buds
+
+    @property
+    def channel_num(self):
+        return self._channel_num
+
+    @property
+    def connections(self):
+        return sys_bl00mbox.channel_conns_num(self.channel_num)
+
+    @property
+    def volume(self):
+        return sys_bl00mbox.channel_get_volume(self.channel_num)
+
+    @volume.setter
+    def volume(self, value):
+        sys_bl00mbox.channel_set_volume(self.channel_num, value)
+
+    @property
+    def mixer(self):
+        return ChannelMixer(self)
+
+    @mixer.setter
+    def mixer(self, val):
+        if isinstance(val, SignalOutput):
+            val.value = self.mixer
+        elif val is None:
+            ChannelMixer(self)._unplug_all
+        else:
+            raise Bl00mboxError("can't connect this")
+
+    @property
+    def background_mute_override(self):
+        return sys_bl00mbox.channel_get_background_mute_override(self.channel_num)
+
+    @background_mute_override.setter
+    def background_mute_override(self, val):
+        sys_bl00mbox.channel_set_background_mute_override(self.channel_num, val)
+
+    @property
+    def foreground(self):
+        return sys_bl00mbox.channel_get_foreground() == self.channel_num
+
+    @foreground.setter
+    def foreground(self, val):
+        if val:
+            sys_bl00mbox.channel_set_foreground(self.channel_num)
+        elif sys_bl00mbox.channel_get_foreground() == self.channel_num:
+            sys_bl00mbox.channel_set_foreground(0)
diff --git a/python_payload/bl00mbox/_helpers.py b/python_payload/bl00mbox/_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..2359652b26f4d1b888a1a2ec136055f6859e487f
--- /dev/null
+++ b/python_payload/bl00mbox/_helpers.py
@@ -0,0 +1,70 @@
+# SPDX-License-Identifier: CC0-1.0
+import time
+
+
+def terminal_scope(
+    signal,
+    signal_min=-32767,
+    signal_max=32767,
+    delay_ms=20,
+    width=80,
+    fun=None,
+    fun_ms=None,
+):
+    """give it a signal and show it on terminal uwu"""
+    if signal_max <= signal_min:
+        return
+    ms_counter = 0
+    fun_counter = 0
+    if fun != None:
+        fun()
+    while True:
+        if fun != None:
+            if fun_ms != None:
+                if fun_ms <= fun_counter:
+                    fun()
+                    fun_counter = fun_counter % fun_ms
+        val = int((width * (signal.value - signal_min)) / (signal_max - signal_min))
+        if val > width:
+            val = width
+        if val < 0:
+            val = 0
+        ret = f"{ms_counter:06d}"
+        ret += " [" + "X" * val + "." * (width - val) + "]"
+        percent = int(100 * val / width)
+        ret += f" {percent:02d}%"
+        print(ret)
+        time.sleep_ms(delay_ms)
+        ms_counter += delay_ms
+        fun_counter += delay_ms
+
+
+# terminal_scope(a.env.signals.output, 0, fun = a.start, fun_ms = 1000)
+
+
+def sct_to_note_name(sct):
+    sct = sct - 18367 + 100
+    octave = ((sct + 9 * 200) // 2400) + 4
+    tones = ["A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"]
+    tone = tones[(sct // 200) % 12]
+    return tone + str(octave)
+
+
+def note_name_to_sct(name):
+    tones = ["A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"]
+    semitones = tones.index(name[0])
+    if semitones > 2:
+        semitones -= 12
+    if name[1] == "b":
+        octave = int(name[2:])
+        semitones -= 1
+    elif name[1] == "#":
+        octave = int(name[2:])
+        semitones += 1
+    else:
+        octave = int(name[1:])
+    return 18367 + (octave - 4) * 2400 + (200 * semitones)
+
+
+def sct_to_freq(sct):
+    return 440 * 2 ** ((sct - 18367) / 2400)
diff --git a/python_payload/bl00mbox/_patches.py b/python_payload/bl00mbox/_patches.py
new file mode 100644
index 0000000000000000000000000000000000000000..02956f51f813fad50c6b1bd8155516bcef93b624
--- /dev/null
+++ b/python_payload/bl00mbox/_patches.py
@@ -0,0 +1,210 @@
+# SPDX-License-Identifier: CC0-1.0
+import math
+
+try:
+    import cpython.wave as wave
+except ImportError:
+    wave = None
+
+
+class _Patch:
+    def bl00mbox_patch_marker(self):
+        return True
+
+
+class tinysynth(_Patch):
+    def __init__(self, chan):
+        self.channel = chan
+        self.SINE = -32767
+        self.TRI = -1
+        self.SQUARE = 1
+        self.SAW = 32767
+
+        self.osc = chan.new_bud(420)
+        self.env = chan.new_bud(42)
+        self.amp = chan.new_bud(69)
+        self.amp.signals.output.value = chan.mixer
+        self.amp.signals.gain.value = self.env.signals.output
+        self.amp.signals.input.value = self.osc.signals.output
+        self.env.signals.sustain.value = 0
+        self.env.signals.decay.value = 500
+        self.release(100)
+
+    def __repr__(self):
+        ret = "[patch] tinysynth"
+        ret += "\n  " + "\n  ".join(repr(self.osc).split("\n"))
+        ret += "\n  " + "\n  ".join(repr(self.env).split("\n"))
+        ret += "\n  " + "\n  ".join(repr(self.amp).split("\n"))
+        return ret
+
+    def tone(self, val):
+        self.osc.signals.pitch.tone = val
+
+    def freq(self, val):
+        self.osc.signals.pitch.freq = val
+
+    def volume(self, val):
+        self.env.signals.input.value = 32767 * val
+
+    def start(self):
+        self.env.signals.trigger.start()
+
+    def stop(self):
+        self.env.signals.trigger.stop()
+
+    def waveform(self, val):
+        self.osc.signals.waveform.value = val
+
+    def attack(self, val):
+        self.env.signals.attack.value = val
+
+    def decay(self, val):
+        self.env.signals.decay.value = val
+
+    def sustain(self, val):
+        self.env.signals.sustain.value = val * 32767
+
+    def release(self, val):
+        self.env.signals.release.value = val
+
+
+class tinysynth_fm(tinysynth):
+    def __init__(self, chan):
+        tinysynth.__init__(self, chan)
+        self.mod_osc = chan.new_bud(420)
+        self.fm_mult = 2.5
+        self.mod_osc.signals.output.value = self.osc.signals.lin_fm
+        self.decay(1000)
+        self.attack(20)
+        self._update_mod_osc()
+        self.fm_waveform(self.SQUARE)
+        self.waveform(self.TRI)
+
+    def fm_waveform(self, val):
+        self.mod_osc.signals.waveform.value = val
+
+    def __repr__(self):
+        ret = tinysynth.__repr__(self)
+        ret = ret.split("\n")
+        ret[0] += "_fm"
+        ret = "\n".join(ret)
+        ret += "\n  " + "\n  ".join(repr(self.mod_osc).split("\n"))
+        return ret
+
+    def fm(self, val):
+        self.fm_mult = val
+        self._update_mod_osc()
+
+    def tone(self, val):
+        self.osc.signals.pitch.tone = val
+        self._update_mod_osc()
+
+    def freq(self, val):
+        self.osc.signals.pitch.freq = val
+        self._update_mod_osc()
+
+    def _update_mod_osc(self):
+        self.mod_osc.signals.pitch.freq = self.fm_mult * self.osc.signals.pitch.freq
+
+
+class sampler(_Patch):
+    """needs a wave file with path relative to samples/"""
+
+    def __init__(self, chan, filename):
+        if wave is None:
+            pass
+            # raise Bl00mboxError("wave library not found")
+        f = wave.open("/flash/sys/samples/" + filename, "r")
+        len_frames = f.getnframes()
+        self.sampler = chan.new_bud(696969, len_frames)
+        table = [0] * len_frames
+        for i in range(len_frames):
+            frame = f.readframes(1)
+            value = int.from_bytes(frame[0:2], "little")
+            table[i] = value
+        f.close()
+        self._filename = filename
+        self.sampler.table = table
+        self.sampler.signals.output = chan.mixer
+
+    def start(self):
+        self.sampler.signals.trigger.start()
+
+    def stop(self):
+        self.sampler.signals.trigger.stop()
+
+    @property
+    def filename(self):
+        return self._filename
+
+
+class step_sequencer(_Patch):
+    def __init__(self, chan):
+        self.seqs = []
+        for i in range(4):
+            seq = chan.new_bud(56709)
+            seq.table = [-32767] + ([0] * 16)
+            if len(self.seqs):
+                self.seqs[-1].signals.sync_out = seq.signals.sync_in
+            self.seqs += [seq]
+        self._bpm = 120
+
+    def __repr__(self):
+        ret = "[patch] step sequencer"
+        # ret += "\n  " + "\n  ".join(repr(self.seqs[0]).split("\n"))
+        ret += (
+            "\n  bpm: "
+            + str(self.seqs[0].signals.bpm.value)
+            + " @ 1/"
+            + str(self.seqs[0].signals.beat_div.value)
+        )
+        ret += (
+            "\n  step: "
+            + str(self.seqs[0].signals.step.value)
+            + "/"
+            + str(self.seqs[0].signals.step_len.value)
+        )
+        ret += "\n  [tracks]"
+        for x, seq in enumerate(self.seqs):
+            ret += (
+                "\n    "
+                + str(x)
+                + " [  "
+                + "".join(["X  " if x > 0 else ".  " for x in seq.table[1:]])
+                + "]"
+            )
+        return ret
+
+    @property
+    def bpm(self):
+        return self._bpm
+
+    @bpm.setter
+    def bpm(self, bpm):
+        for seq in self.seqs:
+            seq.signals.bpm.value = bpm
+        self._bpm = bpm
+
+    def trigger_start(self, track, step):
+        a = self.seqs[track].table
+        a[step + 1] = 32767
+        self.seqs[track].table = a
+
+    def trigger_stop(self, track, step):
+        a = self.seqs[track].table
+        a[step + 1] = 0
+        self.seqs[track].table = a
+
+    def trigger_state(self, track, step):
+        a = self.seqs[track].table
+        return a[step + 1]
+
+    def trigger_toggle(self, track, step):
+        if self.trigger_state(track, step) == 0:
+            self.trigger_start(track, step)
+        else:
+            self.trigger_stop(track, step)
+
+    @property
+    def step(self):
+        return self.seqs[0].signals.step
diff --git a/python_payload/bl00mbox/_plugins.py b/python_payload/bl00mbox/_plugins.py
new file mode 100644
index 0000000000000000000000000000000000000000..c164b3312a54f7b0f00bb0851aa5c9ff21f0ef42
--- /dev/null
+++ b/python_payload/bl00mbox/_plugins.py
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: CC0-1.0
+
+import sys_bl00mbox
+
+
+class _Plugin:
+    def __init__(self, index):
+        self.index = index
+        self.plugin_id = sys_bl00mbox.plugin_index_get_id(self.index)
+        self.name = sys_bl00mbox.plugin_index_get_name(self.index)
+        self.description = sys_bl00mbox.plugin_index_get_description(self.index)
+
+    def __repr__(self):
+        return (
+            "[plugin "
+            + str(self.plugin_id)
+            + "] "
+            + self.name
+            + ": "
+            + self.description
+        )
+
+
+class _Plugins:
+    pass
+
+
+plugins = _Plugins()
+
+
+def _fill():
+    plugins_list = {}
+    for i in range(sys_bl00mbox.plugin_registry_num_plugins()):
+        plugins_list[sys_bl00mbox.plugin_index_get_name(i).replace(" ", "_")] = i
+
+    for name, value in plugins_list.items():
+        setattr(plugins, name, _Plugin(value))
+
+
+_fill()
diff --git a/python_payload/cpython/wave.py b/python_payload/cpython/wave.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0c535c7f4d06c9a4397c397cbab533ad623878f
--- /dev/null
+++ b/python_payload/cpython/wave.py
@@ -0,0 +1,705 @@
+# SPDX-License-Identifier: PSF-2.0
+# from https://github.com/python/cpython/blob/main/Lib/wave.py
+"""Stuff to parse WAVE files.
+
+Usage.
+
+Reading WAVE files:
+      f = wave.open(file, 'r')
+where file is either the name of a file or an open file pointer.
+The open file pointer must have methods read(), seek(), and close().
+When the setpos() and rewind() methods are not used, the seek()
+method is not  necessary.
+
+This returns an instance of a class with the following public methods:
+      getnchannels()  -- returns number of audio channels (1 for
+                         mono, 2 for stereo)
+      getsampwidth()  -- returns sample width in bytes
+      getframerate()  -- returns sampling frequency
+      getnframes()    -- returns number of audio frames
+      getcomptype()   -- returns compression type ('NONE' for linear samples)
+      getcompname()   -- returns human-readable version of
+                         compression type ('not compressed' linear samples)
+      getparams()     -- returns a namedtuple consisting of all of the
+                         above in the above order
+      getmarkers()    -- returns None (for compatibility with the
+                         old aifc module)
+      getmark(id)     -- raises an error since the mark does not
+                         exist (for compatibility with the old aifc module)
+      readframes(n)   -- returns at most n frames of audio
+      rewind()        -- rewind to the beginning of the audio stream
+      setpos(pos)     -- seek to the specified position
+      tell()          -- return the current position
+      close()         -- close the instance (make it unusable)
+The position returned by tell() and the position given to setpos()
+are compatible and have nothing to do with the actual position in the
+file.
+The close() method is called automatically when the class instance
+is destroyed.
+
+Writing WAVE files:
+      f = wave.open(file, 'w')
+where file is either the name of a file or an open file pointer.
+The open file pointer must have methods write(), tell(), seek(), and
+close().
+
+This returns an instance of a class with the following public methods:
+      setnchannels(n) -- set the number of channels
+      setsampwidth(n) -- set the sample width
+      setframerate(n) -- set the frame rate
+      setnframes(n)   -- set the number of frames
+      setcomptype(type, name)
+                      -- set the compression type and the
+                         human-readable compression type
+      setparams(tuple)
+                      -- set all parameters at once
+      tell()          -- return current position in output file
+      writeframesraw(data)
+                      -- write audio frames without patching up the
+                         file header
+      writeframes(data)
+                      -- write audio frames and patch up the file header
+      close()         -- patch up the file header and close the
+                         output file
+You should set the parameters before the first writeframesraw or
+writeframes.  The total number of frames does not need to be set,
+but when it is set to the correct value, the header does not have to
+be patched up.
+It is best to first set all parameters, perhaps possibly the
+compression type, and then write audio frames using writeframesraw.
+When all frames have been written, either call writeframes(b'') or
+close() to patch up the sizes in the header.
+The close() method is called automatically when the class instance
+is destroyed.
+"""
+
+from collections import namedtuple
+import builtins
+import struct
+import sys
+
+
+__all__ = ["open", "Error", "Wave_read", "Wave_write"]
+
+
+class Error(Exception):
+    pass
+
+
+WAVE_FORMAT_PCM = 0x0001
+WAVE_FORMAT_EXTENSIBLE = 0xFFFE
+# Derived from uuid.UUID("00000001-0000-0010-8000-00aa00389b71").bytes_le
+KSDATAFORMAT_SUBTYPE_PCM = b"\x01\x00\x00\x00\x00\x00\x10\x00\x80\x00\x00\xaa\x008\x9bq"
+
+_array_fmts = None, "b", "h", None, "i"
+
+_wave_params = namedtuple(
+    "_wave_params", "nchannels sampwidth framerate nframes comptype compname"
+)
+
+
+def _byteswap(data, width):
+    swapped_data = bytearray(len(data))
+
+    for i in range(0, len(data), width):
+        for j in range(width):
+            swapped_data[i + width - 1 - j] = data[i + j]
+
+    return bytes(swapped_data)
+
+
+class _Chunk:
+    def __init__(self, file, align=True, bigendian=True, inclheader=False):
+        self.closed = False
+        self.align = align  # whether to align to word (2-byte) boundaries
+        if bigendian:
+            strflag = ">"
+        else:
+            strflag = "<"
+        self.file = file
+        self.chunkname = file.read(4)
+        if len(self.chunkname) < 4:
+            raise EOFError
+        try:
+            self.chunksize = struct.unpack_from(strflag + "L", file.read(4))[0]
+        except struct.error:
+            raise EOFError from None
+        if inclheader:
+            self.chunksize = self.chunksize - 8  # subtract header
+        self.size_read = 0
+        try:
+            self.offset = self.file.tell()
+        except (AttributeError, OSError):
+            self.seekable = False
+        else:
+            self.seekable = True
+
+    def getname(self):
+        """Return the name (ID) of the current chunk."""
+        return self.chunkname
+
+    def close(self):
+        if not self.closed:
+            try:
+                self.skip()
+            finally:
+                self.closed = True
+
+    def seek(self, pos, whence=0):
+        """Seek to specified position into the chunk.
+        Default position is 0 (start of chunk).
+        If the file is not seekable, this will result in an error.
+        """
+
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        if not self.seekable:
+            raise OSError("cannot seek")
+        if whence == 1:
+            pos = pos + self.size_read
+        elif whence == 2:
+            pos = pos + self.chunksize
+        if pos < 0 or pos > self.chunksize:
+            raise RuntimeError
+        self.file.seek(self.offset + pos, 0)
+        self.size_read = pos
+
+    def tell(self):
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        return self.size_read
+
+    def read(self, size=-1):
+        """Read at most size bytes from the chunk.
+        If size is omitted or negative, read until the end
+        of the chunk.
+        """
+
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        if self.size_read >= self.chunksize:
+            return b""
+        if size < 0:
+            size = self.chunksize - self.size_read
+        if size > self.chunksize - self.size_read:
+            size = self.chunksize - self.size_read
+        data = self.file.read(size)
+        self.size_read = self.size_read + len(data)
+        if self.size_read == self.chunksize and self.align and (self.chunksize & 1):
+            dummy = self.file.read(1)
+            self.size_read = self.size_read + len(dummy)
+        return data
+
+    def skip(self):
+        """Skip the rest of the chunk.
+        If you are not interested in the contents of the chunk,
+        this method should be called so that the file points to
+        the start of the next chunk.
+        """
+
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        if self.seekable:
+            try:
+                n = self.chunksize - self.size_read
+                # maybe fix alignment
+                if self.align and (self.chunksize & 1):
+                    n = n + 1
+                self.file.seek(n, 1)
+                self.size_read = self.size_read + n
+                return
+            except OSError:
+                pass
+        while self.size_read < self.chunksize:
+            n = min(8192, self.chunksize - self.size_read)
+            dummy = self.read(n)
+            if not dummy:
+                raise EOFError
+
+
+class Wave_read:
+    """Variables used in this class:
+
+    These variables are available to the user though appropriate
+    methods of this class:
+    _file -- the open file with methods read(), close(), and seek()
+              set through the __init__() method
+    _nchannels -- the number of audio channels
+              available through the getnchannels() method
+    _nframes -- the number of audio frames
+              available through the getnframes() method
+    _sampwidth -- the number of bytes per audio sample
+              available through the getsampwidth() method
+    _framerate -- the sampling frequency
+              available through the getframerate() method
+    _comptype -- the AIFF-C compression type ('NONE' if AIFF)
+              available through the getcomptype() method
+    _compname -- the human-readable AIFF-C compression type
+              available through the getcomptype() method
+    _soundpos -- the position in the audio stream
+              available through the tell() method, set through the
+              setpos() method
+
+    These variables are used internally only:
+    _fmt_chunk_read -- 1 iff the FMT chunk has been read
+    _data_seek_needed -- 1 iff positioned correctly in audio
+              file for readframes()
+    _data_chunk -- instantiation of a chunk class for the DATA chunk
+    _framesize -- size of one frame in the file
+    """
+
+    def initfp(self, file):
+        self._convert = None
+        self._soundpos = 0
+        self._file = _Chunk(file, bigendian=0)
+        if self._file.getname() != b"RIFF":
+            raise Error("file does not start with RIFF id")
+        if self._file.read(4) != b"WAVE":
+            raise Error("not a WAVE file")
+        self._fmt_chunk_read = 0
+        self._data_chunk = None
+        while 1:
+            self._data_seek_needed = 1
+            try:
+                chunk = _Chunk(self._file, bigendian=0)
+            except EOFError:
+                break
+            chunkname = chunk.getname()
+            if chunkname == b"fmt ":
+                self._read_fmt_chunk(chunk)
+                self._fmt_chunk_read = 1
+            elif chunkname == b"data":
+                if not self._fmt_chunk_read:
+                    raise Error("data chunk before fmt chunk")
+                self._data_chunk = chunk
+                self._nframes = chunk.chunksize // self._framesize
+                self._data_seek_needed = 0
+                break
+            chunk.skip()
+        if not self._fmt_chunk_read or not self._data_chunk:
+            raise Error("fmt chunk and/or data chunk missing")
+
+    def __init__(self, f):
+        self._i_opened_the_file = None
+        if isinstance(f, str):
+            f = builtins.open(f, "rb")
+            self._i_opened_the_file = f
+        # else, assume it is an open file object already
+        try:
+            self.initfp(f)
+        except:
+            if self._i_opened_the_file:
+                f.close()
+            raise
+
+    def __del__(self):
+        self.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    #
+    # User visible methods.
+    #
+    def getfp(self):
+        return self._file
+
+    def rewind(self):
+        self._data_seek_needed = 1
+        self._soundpos = 0
+
+    def close(self):
+        self._file = None
+        file = self._i_opened_the_file
+        if file:
+            self._i_opened_the_file = None
+            file.close()
+
+    def tell(self):
+        return self._soundpos
+
+    def getnchannels(self):
+        return self._nchannels
+
+    def getnframes(self):
+        return self._nframes
+
+    def getsampwidth(self):
+        return self._sampwidth
+
+    def getframerate(self):
+        return self._framerate
+
+    def getcomptype(self):
+        return self._comptype
+
+    def getcompname(self):
+        return self._compname
+
+    def getparams(self):
+        return _wave_params(
+            self.getnchannels(),
+            self.getsampwidth(),
+            self.getframerate(),
+            self.getnframes(),
+            self.getcomptype(),
+            self.getcompname(),
+        )
+
+    def getmarkers(self):
+        import warnings
+
+        warnings._deprecated("Wave_read.getmarkers", remove=(3, 15))
+        return None
+
+    def getmark(self, id):
+        import warnings
+
+        warnings._deprecated("Wave_read.getmark", remove=(3, 15))
+        raise Error("no marks")
+
+    def setpos(self, pos):
+        if pos < 0 or pos > self._nframes:
+            raise Error("position not in range")
+        self._soundpos = pos
+        self._data_seek_needed = 1
+
+    def readframes(self, nframes):
+        if self._data_seek_needed:
+            self._data_chunk.seek(0, 0)
+            pos = self._soundpos * self._framesize
+            if pos:
+                self._data_chunk.seek(pos, 0)
+            self._data_seek_needed = 0
+        if nframes == 0:
+            return b""
+        data = self._data_chunk.read(nframes * self._framesize)
+        if self._sampwidth != 1 and sys.byteorder == "big":
+            data = _byteswap(data, self._sampwidth)
+        if self._convert and data:
+            data = self._convert(data)
+        self._soundpos = self._soundpos + len(data) // (
+            self._nchannels * self._sampwidth
+        )
+        return data
+
+    #
+    # Internal methods.
+    #
+
+    def _read_fmt_chunk(self, chunk):
+        try:
+            (
+                wFormatTag,
+                self._nchannels,
+                self._framerate,
+                dwAvgBytesPerSec,
+                wBlockAlign,
+            ) = struct.unpack_from("<HHLLH", chunk.read(14))
+        except struct.error:
+            raise EOFError from None
+        if wFormatTag != WAVE_FORMAT_PCM and wFormatTag != WAVE_FORMAT_EXTENSIBLE:
+            raise Error("unknown format: %r" % (wFormatTag,))
+        try:
+            sampwidth = struct.unpack_from("<H", chunk.read(2))[0]
+        except struct.error:
+            raise EOFError from None
+        if wFormatTag == WAVE_FORMAT_EXTENSIBLE:
+            try:
+                cbSize, wValidBitsPerSample, dwChannelMask = struct.unpack_from(
+                    "<HHL", chunk.read(8)
+                )
+                # Read the entire UUID from the chunk
+                SubFormat = chunk.read(16)
+                if len(SubFormat) < 16:
+                    raise EOFError
+            except struct.error:
+                raise EOFError from None
+            if SubFormat != KSDATAFORMAT_SUBTYPE_PCM:
+                try:
+                    import uuid
+
+                    subformat_msg = (
+                        f"unknown extended format: {uuid.UUID(bytes_le=SubFormat)}"
+                    )
+                except Exception:
+                    subformat_msg = "unknown extended format"
+                raise Error(subformat_msg)
+        self._sampwidth = (sampwidth + 7) // 8
+        if not self._sampwidth:
+            raise Error("bad sample width")
+        if not self._nchannels:
+            raise Error("bad # of channels")
+        self._framesize = self._nchannels * self._sampwidth
+        self._comptype = "NONE"
+        self._compname = "not compressed"
+
+
+class Wave_write:
+    """Variables used in this class:
+
+    These variables are user settable through appropriate methods
+    of this class:
+    _file -- the open file with methods write(), close(), tell(), seek()
+              set through the __init__() method
+    _comptype -- the AIFF-C compression type ('NONE' in AIFF)
+              set through the setcomptype() or setparams() method
+    _compname -- the human-readable AIFF-C compression type
+              set through the setcomptype() or setparams() method
+    _nchannels -- the number of audio channels
+              set through the setnchannels() or setparams() method
+    _sampwidth -- the number of bytes per audio sample
+              set through the setsampwidth() or setparams() method
+    _framerate -- the sampling frequency
+              set through the setframerate() or setparams() method
+    _nframes -- the number of audio frames written to the header
+              set through the setnframes() or setparams() method
+
+    These variables are used internally only:
+    _datalength -- the size of the audio samples written to the header
+    _nframeswritten -- the number of frames actually written
+    _datawritten -- the size of the audio samples actually written
+    """
+
+    def __init__(self, f):
+        self._i_opened_the_file = None
+        if isinstance(f, str):
+            f = builtins.open(f, "wb")
+            self._i_opened_the_file = f
+        try:
+            self.initfp(f)
+        except:
+            if self._i_opened_the_file:
+                f.close()
+            raise
+
+    def initfp(self, file):
+        self._file = file
+        self._convert = None
+        self._nchannels = 0
+        self._sampwidth = 0
+        self._framerate = 0
+        self._nframes = 0
+        self._nframeswritten = 0
+        self._datawritten = 0
+        self._datalength = 0
+        self._headerwritten = False
+
+    def __del__(self):
+        self.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    #
+    # User visible methods.
+    #
+    def setnchannels(self, nchannels):
+        if self._datawritten:
+            raise Error("cannot change parameters after starting to write")
+        if nchannels < 1:
+            raise Error("bad # of channels")
+        self._nchannels = nchannels
+
+    def getnchannels(self):
+        if not self._nchannels:
+            raise Error("number of channels not set")
+        return self._nchannels
+
+    def setsampwidth(self, sampwidth):
+        if self._datawritten:
+            raise Error("cannot change parameters after starting to write")
+        if sampwidth < 1 or sampwidth > 4:
+            raise Error("bad sample width")
+        self._sampwidth = sampwidth
+
+    def getsampwidth(self):
+        if not self._sampwidth:
+            raise Error("sample width not set")
+        return self._sampwidth
+
+    def setframerate(self, framerate):
+        if self._datawritten:
+            raise Error("cannot change parameters after starting to write")
+        if framerate <= 0:
+            raise Error("bad frame rate")
+        self._framerate = int(round(framerate))
+
+    def getframerate(self):
+        if not self._framerate:
+            raise Error("frame rate not set")
+        return self._framerate
+
+    def setnframes(self, nframes):
+        if self._datawritten:
+            raise Error("cannot change parameters after starting to write")
+        self._nframes = nframes
+
+    def getnframes(self):
+        return self._nframeswritten
+
+    def setcomptype(self, comptype, compname):
+        if self._datawritten:
+            raise Error("cannot change parameters after starting to write")
+        if comptype not in ("NONE",):
+            raise Error("unsupported compression type")
+        self._comptype = comptype
+        self._compname = compname
+
+    def getcomptype(self):
+        return self._comptype
+
+    def getcompname(self):
+        return self._compname
+
+    def setparams(self, params):
+        nchannels, sampwidth, framerate, nframes, comptype, compname = params
+        if self._datawritten:
+            raise Error("cannot change parameters after starting to write")
+        self.setnchannels(nchannels)
+        self.setsampwidth(sampwidth)
+        self.setframerate(framerate)
+        self.setnframes(nframes)
+        self.setcomptype(comptype, compname)
+
+    def getparams(self):
+        if not self._nchannels or not self._sampwidth or not self._framerate:
+            raise Error("not all parameters set")
+        return _wave_params(
+            self._nchannels,
+            self._sampwidth,
+            self._framerate,
+            self._nframes,
+            self._comptype,
+            self._compname,
+        )
+
+    def setmark(self, id, pos, name):
+        import warnings
+
+        warnings._deprecated("Wave_write.setmark", remove=(3, 15))
+        raise Error("setmark() not supported")
+
+    def getmark(self, id):
+        import warnings
+
+        warnings._deprecated("Wave_write.getmark", remove=(3, 15))
+        raise Error("no marks")
+
+    def getmarkers(self):
+        import warnings
+
+        warnings._deprecated("Wave_write.getmarkers", remove=(3, 15))
+        return None
+
+    def tell(self):
+        return self._nframeswritten
+
+    def writeframesraw(self, data):
+        if not isinstance(data, (bytes, bytearray)):
+            data = memoryview(data).cast("B")
+        self._ensure_header_written(len(data))
+        nframes = len(data) // (self._sampwidth * self._nchannels)
+        if self._convert:
+            data = self._convert(data)
+        if self._sampwidth != 1 and sys.byteorder == "big":
+            data = _byteswap(data, self._sampwidth)
+        self._file.write(data)
+        self._datawritten += len(data)
+        self._nframeswritten = self._nframeswritten + nframes
+
+    def writeframes(self, data):
+        self.writeframesraw(data)
+        if self._datalength != self._datawritten:
+            self._patchheader()
+
+    def close(self):
+        try:
+            if self._file:
+                self._ensure_header_written(0)
+                if self._datalength != self._datawritten:
+                    self._patchheader()
+                self._file.flush()
+        finally:
+            self._file = None
+            file = self._i_opened_the_file
+            if file:
+                self._i_opened_the_file = None
+                file.close()
+
+    #
+    # Internal methods.
+    #
+
+    def _ensure_header_written(self, datasize):
+        if not self._headerwritten:
+            if not self._nchannels:
+                raise Error("# channels not specified")
+            if not self._sampwidth:
+                raise Error("sample width not specified")
+            if not self._framerate:
+                raise Error("sampling rate not specified")
+            self._write_header(datasize)
+
+    def _write_header(self, initlength):
+        assert not self._headerwritten
+        self._file.write(b"RIFF")
+        if not self._nframes:
+            self._nframes = initlength // (self._nchannels * self._sampwidth)
+        self._datalength = self._nframes * self._nchannels * self._sampwidth
+        try:
+            self._form_length_pos = self._file.tell()
+        except (AttributeError, OSError):
+            self._form_length_pos = None
+        self._file.write(
+            struct.pack(
+                "<L4s4sLHHLLHH4s",
+                36 + self._datalength,
+                b"WAVE",
+                b"fmt ",
+                16,
+                WAVE_FORMAT_PCM,
+                self._nchannels,
+                self._framerate,
+                self._nchannels * self._framerate * self._sampwidth,
+                self._nchannels * self._sampwidth,
+                self._sampwidth * 8,
+                b"data",
+            )
+        )
+        if self._form_length_pos is not None:
+            self._data_length_pos = self._file.tell()
+        self._file.write(struct.pack("<L", self._datalength))
+        self._headerwritten = True
+
+    def _patchheader(self):
+        assert self._headerwritten
+        if self._datawritten == self._datalength:
+            return
+        curpos = self._file.tell()
+        self._file.seek(self._form_length_pos, 0)
+        self._file.write(struct.pack("<L", 36 + self._datawritten))
+        self._file.seek(self._data_length_pos, 0)
+        self._file.write(struct.pack("<L", self._datawritten))
+        self._file.seek(curpos, 0)
+        self._datalength = self._datawritten
+
+
+def open(f, mode=None):
+    if mode is None:
+        if hasattr(f, "mode"):
+            mode = f.mode
+        else:
+            mode = "rb"
+    if mode in ("r", "rb"):
+        return Wave_read(f)
+    elif mode in ("w", "wb"):
+        return Wave_write(f)
+    else:
+        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
diff --git a/python_payload/samples/README.md b/python_payload/samples/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ad43546b016fb477a237328c7729492d21589035
--- /dev/null
+++ b/python_payload/samples/README.md
@@ -0,0 +1,4 @@
+Credits:
+https://freesound.org/people/sandyrb/sounds/35633/
+https://freesound.org/people/kaonaya/sounds/131363/
+https://freesound.org/people/Vrezerino/sounds/16709/
diff --git a/python_payload/samples/hihat.wav b/python_payload/samples/hihat.wav
new file mode 100644
index 0000000000000000000000000000000000000000..01800f99b24f309d7f24ebb6a8feafea69d76938
Binary files /dev/null and b/python_payload/samples/hihat.wav differ
diff --git a/python_payload/samples/kick.wav b/python_payload/samples/kick.wav
new file mode 100644
index 0000000000000000000000000000000000000000..47232f348dfc7f21bd8f6179ae4281d6c4b42bfa
Binary files /dev/null and b/python_payload/samples/kick.wav differ
diff --git a/python_payload/samples/snare.wav b/python_payload/samples/snare.wav
new file mode 100644
index 0000000000000000000000000000000000000000..696c806a22ae191e82d418fb43a9a99dbda7bc2c
Binary files /dev/null and b/python_payload/samples/snare.wav differ