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(&liverter_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( + &liverter_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