diff --git a/CHANGELOG.md b/CHANGELOG.md
index daa1853..9575353 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,24 @@
# Change Log
-## v2.0.0 (unreleased)
+## v2.1.0
+ * Noise Plethora
+ * Initial release
+ * Chopping Kinky
+ * Upgraded to use improved DC blocker
+ * Spring Reverb
+ * Added bypass
+ * Kickall
+ * Allow trigger input and button to work independently
+ * EvenVCO
+ * Fix to remove pop when number of polyphony engines changes
+ * Muxlicer
+ * Chaining using reset now works correctly
+
+## v2.0.0
* update to Rack 2 API (added tooltips, bypass, removed boilerplate etc)
+ * UI overhaul
-## v1.2.0 (unreleased)
+## v1.2.0
* Released new modules: Muxlicer, Mex, Morphader, VC ADSR, Sampling Modulator, ST Mix
* Removed DC offset from EvenVCO pulse output
diff --git a/LICENSE-dist.md b/LICENSE-dist.md
new file mode 100644
index 0000000..9e70f02
--- /dev/null
+++ b/LICENSE-dist.md
@@ -0,0 +1,35 @@
+# Licenses of libraries used in Befaco for VCV Rack
+
+## Teensy
+
+```
+ Audio Library for Teensy 3.X
+ Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
+
+ Development of this audio library was funded by PJRC.COM, LLC by sales of
+ Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop
+ open source software by purchasing Teensy or other PJRC products.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice, development funding notice, and this permission
+ notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ ```
+
+## Befaco
+
+Original Noise Plethora plugins were licensed under GPL v3 or later, see LICENSE-GPLv3.txt.
diff --git a/Makefile b/Makefile
index 38d52b6..5ee49dc 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
RACK_DIR ?= ../..
SOURCES += $(wildcard src/*.cpp)
+SOURCES += $(wildcard src/noise-plethora/*/*.cpp)
DISTRIBUTABLES += $(wildcard LICENSE*) res
diff --git a/plugin.json b/plugin.json
index 5abef87..c34486f 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,231 +1,242 @@
-{
- "slug": "Befaco",
- "version": "2.0.0",
- "license": "GPL-3.0-or-later",
- "name": "Befaco",
- "brand": "Befaco",
- "author": "VCV, Ewan Hemingway",
- "authorEmail": "support@vcvrack.com",
- "pluginUrl": "https://vcvrack.com/Befaco",
- "authorUrl": "https://vcvrack.com/",
- "sourceUrl": "https://github.com/VCVRack/Befaco",
- "changelogUrl": "https://github.com/VCVRack/Befaco/blob/v1/CHANGELOG.md",
- "modules": [
- {
- "slug": "EvenVCO",
- "name": "Even VCO",
- "description": "Oscillator including even-harmonic waveform",
- "manualUrl": "https://www.befaco.org/even-vco/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
- "tags": [
- "VCO",
- "Hardware clone",
- "Polyphonic"
- ]
- },
- {
- "slug": "Rampage",
- "name": "Rampage",
- "description": "Dual ramp generator",
- "manualUrl": "https://www.befaco.org/rampage-2/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-rampage",
- "tags": [
- "Function Generator",
- "Logic",
- "Slew Limiter",
- "Envelope Follower",
- "Dual",
- "Hardware clone",
- "Polyphonic"
- ]
- },
- {
- "slug": "ABC",
- "name": "A*B+C",
- "description": "Dual four-quadrant multiplier with VC offset",
- "manualUrl": "https://www.befaco.org/abc/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-a-b-c",
- "tags": [
- "Ring Modulator",
- "Attenuator",
- "Dual",
- "Hardware clone",
- "Polyphonic"
- ]
- },
- {
- "slug": "SpringReverb",
- "name": "Spring Reverb",
- "description": "Spring reverb tank driver",
- "manualUrl": "https://www.befaco.org/spring-reverb/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-spring-reverb-",
- "tags": [
- "Reverb",
- "Hardware clone"
- ]
- },
- {
- "slug": "Mixer",
- "name": "Mixer",
- "description": "Four-channel mixer for audio or CV",
- "manualUrl": "https://www.befaco.org/mixer-2/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-mixer-",
- "tags": [
- "Mixer",
- "Hardware clone",
- "Polyphonic"
- ]
- },
- {
- "slug": "SlewLimiter",
- "name": "Slew Limiter",
- "description": "Voltage controlled slew limiter, AKA lag processor",
- "manualUrl": "https://www.befaco.org/slew-limiter/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-slew-limiter-",
- "tags": [
- "Slew Limiter",
- "Envelope Follower",
- "Hardware clone",
- "Polyphonic"
- ]
- },
- {
- "slug": "DualAtenuverter",
- "name": "Dual Atenuverter",
- "description": "Attenuates, inverts, and applies offset to a signal",
- "manualUrl": "https://www.befaco.org/dual-atenuverter/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-dual-atenuverter-",
- "tags": [
- "Attenuator",
- "Dual",
- "Hardware clone",
- "Polyphonic"
- ]
- },
- {
- "slug": "Percall",
- "name": "Percall",
- "description": "Percussive Envelope Generator",
- "manualUrl": "http://www.befaco.org/percall/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-percall",
- "tags": [
- "Envelope generator",
- "Mixer",
- "Polyphonic",
- "Hardware clone",
- "Quad"
- ]
- },
- {
- "slug": "HexmixVCA",
- "name": "Hex Mix VCA",
- "description": "Six channel VCA with response curve range from logarithmic to linear and to exponential",
- "manualUrl": "https://www.befaco.org/hexmix-vca/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-hexmix-vca",
- "tags": [
- "Mixer",
- "Hardware clone",
- "Polyphonic",
- "VCA"
- ]
- },
- {
- "slug": "ChoppingKinky",
- "name": "Chopping Kinky",
- "description": "Voltage controllable, dual channel wavefolder",
- "manualUrl": "https://www.befaco.org/chopping-kinky/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-chopping-kinky",
- "tags": [
- "Dual",
- "Hardware clone",
- "Voltage-controlled amplifier",
- "Waveshaper"
- ]
- },
- {
- "slug": "Kickall",
- "name": "Kickall",
- "description": "Bassdrum module, with pitch and volume envelopes",
- "manualUrl": "https://www.befaco.org/kickall-2/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-kickall",
- "tags": [
- "Drum",
- "Hardware clone",
- "Synth voice"
- ]
- },
- {
- "slug": "SamplingModulator",
- "name": "Sampling Modulator",
- "description": "Multi-function module that lies somewhere between a VCO, a Sample & Hold, and an 8 step trigger sequencer",
- "manualUrl": "https://www.befaco.org/sampling-modulator/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-sampling-modulator-",
- "tags": [
- "Clock generator",
- "Hardware clone",
- "Oscillator",
- "Sample and hold"
- ]
- },
- {
- "slug": "Morphader",
- "name": "Morphader",
- "description": "Multichannel CV/Audio crossfader",
- "manualUrl": "https://www.befaco.org/morphader-2/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-morphader",
- "tags": [
- "Controller",
- "Hardware clone",
- "Mixer",
- "Polyphonic",
- "Quad"
- ]
- },
- {
- "slug": "ADSR",
- "name": "ADSR",
- "description": "ADSR envelope generator with gate output on each stage, plus variable shape",
- "manualUrl": "https://www.befaco.org/vc-adsr/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-adsr",
- "tags": [
- "Envelope generator",
- "Hardware clone"
- ]
- },
- {
- "slug": "STMix",
- "name": "STMix",
- "description": "A compact 4 channel stereo mixer with an auxiliary input",
- "manualUrl": "https://www.befaco.org/stmix/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-stmix-",
- "tags": [
- "Hardware clone",
- "Mixer",
- "Polyphonic"
- ]
- },
- {
- "slug": "Muxlicer",
- "name": "Muxlicer",
- "description": "VC adressable sequential switch and sequencer",
- "manualUrl": "https://www.befaco.org/muxlicer-2/",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-muxlicer",
- "tags": [
- "Clock generator",
- "Hardware clone",
- "Polyphonic",
- "Sequencer",
- "Switch"
- ]
- },
- {
- "slug": "Mex",
- "name": "Mex",
- "description": "Gate Expander for Befaco Muxlicer",
- "tags": [
- "Expander",
- "Hardware clone"
- ]
- }
- ]
+{
+ "slug": "Befaco",
+ "version": "2.1.0",
+ "license": "GPL-3.0-or-later",
+ "name": "Befaco",
+ "brand": "Befaco",
+ "author": "VCV, Ewan Hemingway",
+ "authorEmail": "support@vcvrack.com",
+ "pluginUrl": "https://vcvrack.com/Befaco",
+ "authorUrl": "https://vcvrack.com/",
+ "sourceUrl": "https://github.com/VCVRack/Befaco",
+ "changelogUrl": "https://github.com/VCVRack/Befaco/blob/v2/CHANGELOG.md",
+ "modules": [
+ {
+ "slug": "EvenVCO",
+ "name": "Even VCO",
+ "description": "Oscillator including even-harmonic waveform",
+ "manualUrl": "https://www.befaco.org/even-vco/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
+ "tags": [
+ "VCO",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "Rampage",
+ "name": "Rampage",
+ "description": "Dual ramp generator",
+ "manualUrl": "https://www.befaco.org/rampage-2/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-rampage",
+ "tags": [
+ "Function Generator",
+ "Logic",
+ "Slew Limiter",
+ "Envelope Follower",
+ "Dual",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "ABC",
+ "name": "A*B+C",
+ "description": "Dual four-quadrant multiplier with VC offset",
+ "manualUrl": "https://www.befaco.org/abc/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-a-b-c",
+ "tags": [
+ "Ring Modulator",
+ "Attenuator",
+ "Dual",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "SpringReverb",
+ "name": "Spring Reverb",
+ "description": "Spring reverb tank driver",
+ "manualUrl": "https://www.befaco.org/spring-reverb/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-spring-reverb-",
+ "tags": [
+ "Reverb",
+ "Hardware clone"
+ ]
+ },
+ {
+ "slug": "Mixer",
+ "name": "Mixer",
+ "description": "Four-channel mixer for audio or CV",
+ "manualUrl": "https://www.befaco.org/mixer-2/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-mixer-",
+ "tags": [
+ "Mixer",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "SlewLimiter",
+ "name": "Slew Limiter",
+ "description": "Voltage controlled slew limiter, AKA lag processor",
+ "manualUrl": "https://www.befaco.org/slew-limiter/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-slew-limiter-",
+ "tags": [
+ "Slew Limiter",
+ "Envelope Follower",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "DualAtenuverter",
+ "name": "Dual Atenuverter",
+ "description": "Attenuates, inverts, and applies offset to a signal",
+ "manualUrl": "https://www.befaco.org/dual-atenuverter/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-dual-atenuverter-",
+ "tags": [
+ "Attenuator",
+ "Dual",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "Percall",
+ "name": "Percall",
+ "description": "Percussive Envelope Generator",
+ "manualUrl": "http://www.befaco.org/percall/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-percall",
+ "tags": [
+ "Envelope generator",
+ "Mixer",
+ "Polyphonic",
+ "Hardware clone",
+ "Quad"
+ ]
+ },
+ {
+ "slug": "HexmixVCA",
+ "name": "Hex Mix VCA",
+ "description": "Six channel VCA with response curve range from logarithmic to linear and to exponential",
+ "manualUrl": "https://www.befaco.org/hexmix-vca/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-hexmix-vca",
+ "tags": [
+ "Mixer",
+ "Hardware clone",
+ "Polyphonic",
+ "VCA"
+ ]
+ },
+ {
+ "slug": "ChoppingKinky",
+ "name": "Chopping Kinky",
+ "description": "Voltage controllable, dual channel wavefolder",
+ "manualUrl": "https://www.befaco.org/chopping-kinky/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-chopping-kinky",
+ "tags": [
+ "Dual",
+ "Hardware clone",
+ "Voltage-controlled amplifier",
+ "Waveshaper"
+ ]
+ },
+ {
+ "slug": "Kickall",
+ "name": "Kickall",
+ "description": "Bassdrum module, with pitch and volume envelopes",
+ "manualUrl": "https://www.befaco.org/kickall-2/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-kickall",
+ "tags": [
+ "Drum",
+ "Hardware clone",
+ "Synth voice"
+ ]
+ },
+ {
+ "slug": "SamplingModulator",
+ "name": "Sampling Modulator",
+ "description": "Multi-function module that lies somewhere between a VCO, a Sample & Hold, and an 8 step trigger sequencer",
+ "manualUrl": "https://www.befaco.org/sampling-modulator/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-sampling-modulator-",
+ "tags": [
+ "Clock generator",
+ "Hardware clone",
+ "Oscillator",
+ "Sample and hold"
+ ]
+ },
+ {
+ "slug": "Morphader",
+ "name": "Morphader",
+ "description": "Multichannel CV/Audio crossfader",
+ "manualUrl": "https://www.befaco.org/morphader-2/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-morphader",
+ "tags": [
+ "Controller",
+ "Hardware clone",
+ "Mixer",
+ "Polyphonic",
+ "Quad"
+ ]
+ },
+ {
+ "slug": "ADSR",
+ "name": "ADSR",
+ "description": "ADSR envelope generator with gate output on each stage, plus variable shape",
+ "manualUrl": "https://www.befaco.org/vc-adsr/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-adsr",
+ "tags": [
+ "Envelope generator",
+ "Hardware clone"
+ ]
+ },
+ {
+ "slug": "STMix",
+ "name": "STMix",
+ "description": "A compact 4 channel stereo mixer with an auxiliary input",
+ "manualUrl": "https://www.befaco.org/stmix/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-stmix-",
+ "tags": [
+ "Hardware clone",
+ "Mixer",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "Muxlicer",
+ "name": "Muxlicer",
+ "description": "VC adressable sequential switch and sequencer",
+ "manualUrl": "https://www.befaco.org/muxlicer-2/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-muxlicer",
+ "tags": [
+ "Clock generator",
+ "Hardware clone",
+ "Polyphonic",
+ "Sequencer",
+ "Switch"
+ ]
+ },
+ {
+ "slug": "Mex",
+ "name": "Mex",
+ "description": "Gate Expander for Befaco Muxlicer",
+ "tags": [
+ "Expander",
+ "Hardware clone"
+ ]
+ },
+ {
+ "slug": "NoisePlethora",
+ "name": "Noise Plethora",
+ "description": "Multitimbral noise monster",
+ "tags": [
+ "Dual",
+ "Filter",
+ "Hardware clone",
+ "Noise"
+ ]
+ }
+ ]
}
\ No newline at end of file
diff --git a/res/components/Davies1900hLargeLightGrey.svg b/res/components/Davies1900hLargeLightGrey.svg
new file mode 100644
index 0000000..4964f7d
--- /dev/null
+++ b/res/components/Davies1900hLargeLightGrey.svg
@@ -0,0 +1,95 @@
+
+
diff --git a/res/components/Davies1900hLargeLightGrey_bg.svg b/res/components/Davies1900hLargeLightGrey_bg.svg
new file mode 100644
index 0000000..0ea8575
--- /dev/null
+++ b/res/components/Davies1900hLargeLightGrey_bg.svg
@@ -0,0 +1,98 @@
+
+
diff --git a/res/components/SwitchNarrowHoriz_0.svg b/res/components/SwitchNarrowHoriz_0.svg
new file mode 100644
index 0000000..01add0b
--- /dev/null
+++ b/res/components/SwitchNarrowHoriz_0.svg
@@ -0,0 +1,154 @@
+
+
diff --git a/res/components/SwitchNarrowHoriz_1.svg b/res/components/SwitchNarrowHoriz_1.svg
new file mode 100644
index 0000000..35ba7d4
--- /dev/null
+++ b/res/components/SwitchNarrowHoriz_1.svg
@@ -0,0 +1,149 @@
+
+
diff --git a/res/components/SwitchNarrow_1.svg b/res/components/SwitchNarrow_1.svg
index 3326265..695c0ae 100644
--- a/res/components/SwitchNarrow_1.svg
+++ b/res/components/SwitchNarrow_1.svg
@@ -1,42 +1,151 @@
-
-
-
+
+
diff --git a/res/components/SwitchNarrow_2.svg b/res/components/SwitchNarrow_2.svg
new file mode 100644
index 0000000..3326265
--- /dev/null
+++ b/res/components/SwitchNarrow_2.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/res/fonts/OFL.txt b/res/fonts/OFL.txt
new file mode 100644
index 0000000..8e46b9b
--- /dev/null
+++ b/res/fonts/OFL.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2014, Cedric Knight ,
+with Reserved Font Name "Segment7".
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/res/fonts/Segment7Standard.otf b/res/fonts/Segment7Standard.otf
new file mode 100644
index 0000000..7429b0d
Binary files /dev/null and b/res/fonts/Segment7Standard.otf differ
diff --git a/res/panels/NoisePlethora.svg b/res/panels/NoisePlethora.svg
new file mode 100644
index 0000000..a0e2728
--- /dev/null
+++ b/res/panels/NoisePlethora.svg
@@ -0,0 +1,2564 @@
+
+
diff --git a/src/ABC.cpp b/src/ABC.cpp
index ec9aa17..56ce2f7 100644
--- a/src/ABC.cpp
+++ b/src/ABC.cpp
@@ -45,6 +45,23 @@ struct ABC : Module {
configParam(C1_LEVEL_PARAM, -1.0, 1.0, 0.0, "C1 Level");
configParam(B2_LEVEL_PARAM, -1.0, 1.0, 0.0, "B2 Level");
configParam(C2_LEVEL_PARAM, -1.0, 1.0, 0.0, "C2 Level");
+
+ configInput(A1_INPUT, "A1");
+ configInput(B1_INPUT, "B1");
+ configInput(C1_INPUT, "C1");
+ configInput(A2_INPUT, "A2");
+ configInput(B2_INPUT, "B2");
+ configInput(C2_INPUT, "C2");
+
+ getInputInfo(B1_INPUT)->description = "Normalled to 5V";
+ getInputInfo(C1_INPUT)->description = "Normalled to 10V";
+ getInputInfo(B2_INPUT)->description = "Normalled to 5V";
+ getInputInfo(C2_INPUT)->description = "Normalled to 10V";
+
+ configOutput(OUT1_OUTPUT, "Out 1");
+ configOutput(OUT2_OUTPUT, "Out 2");
+
+ getOutputInfo(OUT1_OUTPUT)->description = "Normalled to Out 2";
}
void processSection(const ProcessArgs& args, int& lastChannels, float_4* lastOut, ParamIds levelB, ParamIds levelC, InputIds inputA, InputIds inputB, InputIds inputC, OutputIds output, LightIds outLight) {
diff --git a/src/ChoppingKinky.cpp b/src/ChoppingKinky.cpp
index b66481f..67b1e5d 100644
--- a/src/ChoppingKinky.cpp
+++ b/src/ChoppingKinky.cpp
@@ -50,7 +50,7 @@ struct ChoppingKinky : Module {
chowdsp::VariableOversampling<> oversampler[NUM_CHANNELS];
int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling
- dsp::BiquadFilter blockDCFilter;
+ DCBlocker blockDCFilter;
bool blockDC = false;
ChoppingKinky() {
@@ -68,6 +68,8 @@ struct ChoppingKinky : Module {
configInput(CV_B_INPUT, "CV B (with attenuator)");
configInput(VCA_CV_B_INPUT, "CV B");
+ getInputInfo(CV_B_INPUT)->description = "Normalled to CV A (with attenuator) Input";
+
configOutput(OUT_CHOPP_OUTPUT, "Chopp");
configOutput(OUT_A_OUTPUT, "A");
configOutput(OUT_B_OUTPUT, "B");
@@ -81,7 +83,7 @@ struct ChoppingKinky : Module {
void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
- blockDCFilter.setParameters(dsp::BiquadFilter::HIGHPASS, 10.3f / sampleRate, M_SQRT1_2, 1.0f);
+ blockDCFilter.setFrequency(22.05 / sampleRate);
for (int channel_idx = 0; channel_idx < NUM_CHANNELS; channel_idx++) {
oversampler[channel_idx].setOversamplingIndex(oversamplingIndex);
diff --git a/src/EvenVCO.cpp b/src/EvenVCO.cpp
index 4a05058..afff33a 100644
--- a/src/EvenVCO.cpp
+++ b/src/EvenVCO.cpp
@@ -120,8 +120,10 @@ struct EvenVCO : Module {
phase[c / 4] += deltaPhase[c / 4];
}
- // the next block can't be done with SIMD instructions:
- for (int c = 0; c < channels; c++) {
+ // the next block can't be done with SIMD instructions, but should at least be completed with
+ // blocks of 4 (otherwise popping artfifacts are generated from invalid phase/oldPhase/deltaPhase)
+ const int channelsRoundedUpNearestFour = (1 + (channels - 1) / 4) * 4;
+ for (int c = 0; c < channelsRoundedUpNearestFour; c++) {
if (oldPhase[c / 4].s[c % 4] < 0.5 && phase[c / 4].s[c % 4] >= 0.5) {
float crossing = -(phase[c / 4].s[c % 4] - 0.5) / deltaPhase[c / 4].s[c % 4];
@@ -161,20 +163,13 @@ struct EvenVCO : Module {
float_4 square[4] = {};
float_4 triOut[4] = {};
- for (int c = 0; c < channels; c++) {
+ for (int c = 0; c < channelsRoundedUpNearestFour; c++) {
triSquareMinBlepOut[c / 4].s[c % 4] = triSquareMinBlep[c].process();
doubleSawMinBlepOut[c / 4].s[c % 4] = doubleSawMinBlep[c].process();
sawMinBlepOut[c / 4].s[c % 4] = sawMinBlep[c].process();
squareMinBlepOut[c / 4].s[c % 4] = squareMinBlep[c].process();
}
- // Outputs
- outputs[TRI_OUTPUT].setChannels(channels);
- outputs[SINE_OUTPUT].setChannels(channels);
- outputs[EVEN_OUTPUT].setChannels(channels);
- outputs[SAW_OUTPUT].setChannels(channels);
- outputs[SQUARE_OUTPUT].setChannels(channels);
-
for (int c = 0; c < channels; c += 4) {
triSquare[c / 4] = simd::ifelse((phase[c / 4] < 0.5f), -1.f, +1.f);
@@ -208,6 +203,13 @@ struct EvenVCO : Module {
outputs[SAW_OUTPUT].setVoltageSimd(saw[c / 4], c);
outputs[SQUARE_OUTPUT].setVoltageSimd(square[c / 4], c);
}
+
+ // Outputs
+ outputs[TRI_OUTPUT].setChannels(channels);
+ outputs[SINE_OUTPUT].setChannels(channels);
+ outputs[EVEN_OUTPUT].setChannels(channels);
+ outputs[SAW_OUTPUT].setChannels(channels);
+ outputs[SQUARE_OUTPUT].setChannels(channels);
}
};
diff --git a/src/HexmixVCA.cpp b/src/HexmixVCA.cpp
index eeb2c1d..e35de99 100644
--- a/src/HexmixVCA.cpp
+++ b/src/HexmixVCA.cpp
@@ -42,8 +42,16 @@ struct HexmixVCA : Module {
HexmixVCA() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
for (int i = 0; i < numRows; ++i) {
- configParam(SHAPE_PARAM + i, -1.f, 1.f, 0.f, string::f("Channel %d VCA response", i));
- configParam(VOL_PARAM + i, 0.f, 1.f, 1.f, string::f("Channel %d output level", i));
+ configParam(SHAPE_PARAM + i, -1.f, 1.f, 0.f, string::f("Channel %d VCA response", i + 1));
+ configParam(VOL_PARAM + i, 0.f, 1.f, 1.f, string::f("Channel %d output level", i + 1));
+
+ configInput(IN_INPUT + i, string::f("Channel %d", i + 1));
+ configInput(CV_INPUT + i, string::f("Gain %d", i + 1));
+ configOutput(OUT_OUTPUT + i, string::f("Channel %d", i + 1));
+
+ getInputInfo(CV_INPUT + i)->description = "Normalled to 10V";
+
+ configBypass(IN_INPUT + i, OUT_OUTPUT + i);
}
cvDivider.setDivision(16);
diff --git a/src/Kickall.cpp b/src/Kickall.cpp
index ba4067c..fc48fea 100644
--- a/src/Kickall.cpp
+++ b/src/Kickall.cpp
@@ -40,7 +40,8 @@ struct Kickall : Module {
ADEnvelope volume;
ADEnvelope pitch;
- dsp::SchmittTrigger trigger;
+ dsp::SchmittTrigger gateTrigger;
+ dsp::BooleanTrigger buttonTrigger;
static const int UPSAMPLE = 8;
chowdsp::Oversampling oversampler;
@@ -69,7 +70,7 @@ struct Kickall : Module {
configOutput(OUT_OUTPUT, "Kick");
configLight(ENV_LIGHT, "Volume envelope");
-
+
// calculate up/downsampling rates
onSampleRateChange();
}
@@ -80,7 +81,10 @@ struct Kickall : Module {
void process(const ProcessArgs& args) override {
// TODO: check values
- if (trigger.process(inputs[TRIGG_INPUT].getVoltage() / 2.0f + params[TRIGG_BUTTON_PARAM].getValue() * 10.0)) {
+ const bool risingEdgeGate = gateTrigger.process(inputs[TRIGG_INPUT].getVoltage() / 2.0f);
+ const bool buttonTriggered = buttonTrigger.process(params[TRIGG_BUTTON_PARAM].getValue());
+ // can be triggered by either rising edge on trigger in, or a button press
+ if (risingEdgeGate || buttonTriggered) {
volume.trigger();
pitch.trigger();
}
diff --git a/src/Muxlicer.cpp b/src/Muxlicer.cpp
index 8f24a0e..1cc2172 100644
--- a/src/Muxlicer.cpp
+++ b/src/Muxlicer.cpp
@@ -254,9 +254,21 @@ struct Muxlicer : Module {
uint32_t runIndex; // which step are we on (0 to 7)
uint32_t addressIndex = 0;
- bool playHeadHasReset = false;
bool tapped = false;
+ enum ResetStatus {
+ RESET_NOT_REQUESTED,
+ RESET_AND_PLAY_ONCE,
+ RESET_AND_PLAY
+ };
+ // Used to track if a reset has been triggered. Can be from the CV input, or the momentary switch. Note
+ // that behaviour depends on if the Muxlicer is clocked or not. If clocked, the playhead resets but waits
+ // for the next clock tick to start. If not clocked, then the sequence will start immediately (i.e. the
+ // internal clock will be synced at the point where `resetIsRequested` is first true.
+ ResetStatus resetRequested = RESET_NOT_REQUESTED;
+ // used to detect when `resetRequested` first becomes active
+ dsp::BooleanTrigger detectResetTrigger;
+
// used to track the clock (e.g. if external clock is not connected). NOTE: this clock
// is defined _prior_ to any clock division/multiplication logic
float internalClockProgress = 0.f;
@@ -266,7 +278,6 @@ struct Muxlicer : Module {
dsp::SchmittTrigger inputClockTrigger; // to detect incoming clock pulses
dsp::BooleanTrigger mainClockTrigger; // to detect when divided/multiplied version of the clock signal has rising edge
dsp::SchmittTrigger resetTrigger; // to detect the reset signal
- dsp::PulseGenerator resetTimer; // leave a grace period before advancing the step
dsp::PulseGenerator endOfCyclePulse; // fire a signal at the end of cycle
dsp::BooleanTrigger tapTempoTrigger; // to only trigger tap tempo when push is first detected
MultDivClock mainClockMultDiv; // to produce a divided/multiplied version of the (internal or external) clock signal
@@ -430,7 +441,21 @@ struct Muxlicer : Module {
internalClockLength = tapTime;
}
tapTime = 0;
- internalClockProgress = 0;
+ internalClockProgress = 0.f;
+ }
+
+ // If we get a reset signal (which can come from CV or various modes of the switch), and the clock has only
+ // just started to tick (internalClockProgress < 1ms), we assume that the reset signal is slightly delayed
+ // due to the 1 sample delay that Rack introduces. If this is the case, the internal clock trigger detector,
+ // `detectResetTrigger`, which advances the sequence, will not be "primed" to detect a rising edge for another
+ // whole clock tick, meaning the first step is repeated. See: https://github.com/VCVRack/Befaco/issues/32
+ // Also see https://vcvrack.com/manual/VoltageStandards#Timing for 0.001 seconds justification.
+ if (detectResetTrigger.process(resetRequested != RESET_NOT_REQUESTED) && internalClockProgress < 1e-3) {
+ // NOTE: the sequence must also be stopped for this to come into effect. In hardware, if the Nth step Gate Out
+ // is patched back into the reset, that step should complete before the sequence restarts.
+ if (playState == STATE_STOPPED) {
+ mainClockTrigger.state = false;
+ }
}
tapTime += args.sampleTime;
internalClockProgress += args.sampleTime;
@@ -451,20 +476,33 @@ struct Muxlicer : Module {
// so we must use a BooleanTrigger on the divided/mult'd signal in order to detect rising edge / when to advance the sequence
const bool dividedMultipliedClockPulseReceived = mainClockTrigger.process(mainClockMultDiv.process(args.sampleTime, clockPulseReceived));
- // see https://vcvrack.com/manual/VoltageStandards#Timing
- const bool resetGracePeriodActive = resetTimer.process(args.sampleTime);
-
if (dividedMultipliedClockPulseReceived) {
- if (isAddressInRunMode && !resetGracePeriodActive && playState != STATE_STOPPED) {
+
+ if (resetRequested != RESET_NOT_REQUESTED) {
+ runIndex = 7;
+
+ if (resetRequested == RESET_AND_PLAY) {
+ playState = STATE_PLAY;
+ }
+ else if (resetRequested == RESET_AND_PLAY_ONCE) {
+ playState = STATE_PLAY_ONCE;
+
+ }
+ }
+
+ if (isAddressInRunMode && playState != STATE_STOPPED) {
+
runIndex++;
+
if (runIndex >= SEQUENCE_LENGTH) {
+ runIndex = 0;
+
// the sequence resets by placing the play head at the final step (so that the next clock pulse
// ticks over onto the first step) - so if we are on the final step _because_ we've reset,
- // then don't fire EOC
- if (playHeadHasReset) {
- playHeadHasReset = false;
- runIndex = 0;
+ // then don't fire EOC, just clear the reset status
+ if (resetRequested != RESET_NOT_REQUESTED) {
+ resetRequested = RESET_NOT_REQUESTED;
}
// otherwise we've naturally arrived at the last step so do fire EOC etc
else {
@@ -474,12 +512,10 @@ struct Muxlicer : Module {
if (playState == STATE_PLAY_ONCE) {
playState = STATE_STOPPED;
}
- else {
- runIndex = 0;
- }
}
}
}
+
multiClock.reset(mainClockMultDiv.getEffectiveClockLength());
if (isAddressInRunMode) {
@@ -569,13 +605,12 @@ struct Muxlicer : Module {
const bool resetPulseInReceived = resetTrigger.process(rescale(inputs[RESET_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f));
if (resetPulseInReceived) {
- playHeadHasReset = true;
- runIndex = 8;
- if (playState == STATE_STOPPED) {
- playState = STATE_PLAY_ONCE;
+ switch (playState) {
+ case STATE_STOPPED: resetRequested = RESET_AND_PLAY_ONCE; break;
+ case STATE_PLAY_ONCE: resetRequested = RESET_AND_PLAY_ONCE; break;
+ case STATE_PLAY: resetRequested = RESET_AND_PLAY; break;
}
- resetTimer.trigger();
}
// if the play switch has effectively been activated for the first time,
@@ -583,19 +618,17 @@ struct Muxlicer : Module {
const bool switchIsActive = params[PLAY_PARAM].getValue() != STATE_STOPPED;
if (playStateTrigger.process(switchIsActive) && switchIsActive) {
- // if we were stopped, check for activation (normal or one-shot)
+ // if we were stopped, check for activation (normal, up or one-shot, down)
if (playState == STATE_STOPPED) {
if (params[PLAY_PARAM].getValue() == STATE_PLAY) {
- playState = STATE_PLAY;
+ resetRequested = RESET_AND_PLAY;
}
else if (params[PLAY_PARAM].getValue() == STATE_PLAY_ONCE) {
- playState = STATE_PLAY_ONCE;
- runIndex = 8;
- playHeadHasReset = true;
+ resetRequested = RESET_AND_PLAY_ONCE;
}
}
// otherwise we are in play mode (and we've not just held onto the play switch),
- // so check for stop or reset
+ // so check for stop (switch up) or reset (switch down)
else {
// top switch will stop
@@ -604,8 +637,7 @@ struct Muxlicer : Module {
}
// bottom will reset
else if (params[PLAY_PARAM].getValue() == STATE_PLAY_ONCE) {
- playHeadHasReset = true;
- runIndex = 8;
+ resetRequested = RESET_AND_PLAY_ONCE;
}
}
}
diff --git a/src/NoisePlethora.cpp b/src/NoisePlethora.cpp
new file mode 100644
index 0000000..e42e460
--- /dev/null
+++ b/src/NoisePlethora.cpp
@@ -0,0 +1,905 @@
+#include "plugin.hpp"
+#include "noise-plethora/plugins/NoisePlethoraPlugin.hpp"
+#include "noise-plethora/plugins/ProgramSelector.hpp"
+
+enum FilterMode {
+ LOWPASS,
+ HIGHPASS,
+ BANDPASS,
+ NUM_TYPES
+};
+
+
+// Zavalishin 2018, "The Art of VA Filter Design", http://www.native-instruments.com/fileadmin/ni_media/downloads/pdf/VAFilterDesign_2.0.0a.pdf
+// Section 6.7, adopted from BogAudio Saturator https://github.com/bogaudio/BogaudioModules/blob/master/src/dsp/signal.cpp
+struct Saturator {
+
+ // saturate input at around ~[-1, +1] with soft clipping
+ static float process(float sample) {
+
+ if (sample < 0.0f) {
+ return -saturation(-sample);
+ }
+ return saturation(sample);
+ }
+private:
+
+ static float saturation(float sample) {
+
+ const float limit = 1.05f;
+ const float y1 = 0.98765f; // (2*x - 1)/x**2 where x is 0.9.
+ // correction so that saturation(0) = 0
+ const float offset = 0.0062522; // -0.5f + sqrtf(0.5f * 0.5f) / y1;
+
+ float x = sample / limit;
+ float x1 = (x + 1.0f) * 0.5f;
+
+ return limit * (offset + x1 - std::sqrt(x1 * x1 - y1 * x) * (1.0f / y1));
+ }
+};
+
+// based on Chapter 4 of THE ART OF VA FILTER DESIGN and
+// Chap 12.4 of "Designing Audio Effect Plugins in C++" Will Pirkle
+class StateVariableFilter2ndOrder {
+public:
+
+ StateVariableFilter2ndOrder() {
+ setParameters(0.f, M_SQRT1_2);
+ }
+
+ void setParameters(float fc, float q) {
+ // avoid repeated evaluations of tanh if not needed
+ if (fc != fcCached || q != qCached) {
+
+ fcCached = fc;
+ qCached = q;
+
+ const double g = std::tan(M_PI * fc);
+ const double R = 1.0f / (2 * q);
+
+ alpha0 = 1.0 / (1.0 + 2.0 * R * g + g * g);
+ alpha = g;
+ rho = 2.0 * R + g;
+ }
+ }
+
+ void process(float input) {
+ hp = (input - rho * mem1 - mem2) * alpha0;
+ bp = alpha * hp + mem1;
+ lp = alpha * bp + mem2;
+ mem1 = alpha * hp + bp;
+ mem2 = alpha * bp + lp;
+ }
+
+ float output(FilterMode mode) {
+ switch (mode) {
+ case LOWPASS: return lp;
+ case HIGHPASS: return hp;
+ case BANDPASS: return bp;
+ default: return 0.0;
+ }
+ }
+
+private:
+ float alpha, alpha0, rho;
+
+ float fcCached = -1.f, qCached = -1.f;
+
+ float hp = 0.0f, bp = 0.0f, lp = 0.0f, mem1 = 0.0f, mem2 = 0.0f;
+};
+
+class StateVariableFilter4thOrder {
+
+public:
+ StateVariableFilter4thOrder() {
+
+ }
+
+ void setParameters(float fc, float q) {
+ float rootQ = std::sqrt(q);
+ stage1.setParameters(fc, rootQ);
+ stage2.setParameters(fc, rootQ);
+ }
+
+ float process(float input, FilterMode mode) {
+ stage1.process(input);
+ float s1 = stage1.output(mode);
+
+ stage2.process(s1);
+ float s2 = stage2.output(mode);
+
+ return s2;
+ }
+
+private:
+ StateVariableFilter2ndOrder stage1, stage2;
+};
+
+
+struct NoisePlethora : Module {
+ enum ParamIds {
+ // A params
+ X_A_PARAM,
+ Y_A_PARAM,
+ CUTOFF_A_PARAM,
+ RES_A_PARAM,
+ CUTOFF_CV_A_PARAM,
+ FILTER_TYPE_A_PARAM,
+ // misc
+ PROGRAM_PARAM,
+ // B params
+ X_B_PARAM,
+ Y_B_PARAM,
+ CUTOFF_B_PARAM,
+ RES_B_PARAM,
+ CUTOFF_CV_B_PARAM,
+ FILTER_TYPE_B_PARAM,
+ // C params
+ GRIT_PARAM,
+ RES_C_PARAM,
+ CUTOFF_C_PARAM,
+ CUTOFF_CV_C_PARAM,
+ FILTER_TYPE_C_PARAM,
+ SOURCE_C_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ X_A_INPUT,
+ Y_A_INPUT,
+ CUTOFF_A_INPUT,
+ PROG_A_INPUT,
+ PROG_B_INPUT,
+ CUTOFF_B_INPUT,
+ X_B_INPUT,
+ Y_B_INPUT,
+ GRIT_INPUT,
+ CUTOFF_C_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ A_OUTPUT,
+ B_OUTPUT,
+ GRITTY_OUTPUT,
+ FILTERED_OUTPUT,
+ WHITE_OUTPUT,
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ BANK_LIGHT,
+ NUM_LIGHTS
+ };
+
+ enum Section {
+ SECTION_A,
+ SECTION_B,
+ SECTION_C,
+ NUM_SECTIONS
+ };
+
+ enum ProgramKnobMode {
+ PROGRAM_MODE,
+ BANK_MODE
+ };
+ ProgramKnobMode programKnobMode = PROGRAM_MODE;
+ // one full turn of the program knob corresponds to an increment of dialResolution to the bank/program
+ static constexpr int dialResolution = 8;
+ // variable to store what the program knob was prior to the start of dragging (used to calculate deltas)
+ float programKnobReferenceState = 0.f;
+
+ // section A/B
+ bool bypassFilters = false;
+ std::shared_ptr algorithm[2]; // pointer to actual algorithm
+ std::string algorithmName[2]; // variable to cache which algorithm is active (after program CV applied)
+
+ // filters for A/B
+ StateVariableFilter2ndOrder svfFilter[2];
+ bool blockDC = true;
+ DCBlocker blockDCFilter[3];
+
+ ProgramSelector programSelector; // tracks banks and programs for both sections A/B, including which is the "active" section
+ ProgramSelector programSelectorWithCV; // as above, but also with CV for program applied as an offset - works like Plaits Model CV input
+ // UI / UX for A/B
+ std::string textDisplayA = " ", textDisplayB = " ";
+ bool isDisplayActiveA = false, isDisplayActiveB = false;
+ bool programButtonHeld = false;
+ bool programButtonDragged = false;
+ dsp::BooleanTrigger programHoldTrigger;
+ dsp::Timer programHoldTimer;
+
+ dsp::PulseGenerator updateParamsTimer;
+ const float updateTimeSecs = 0.0029f;
+
+ // section C
+ AudioSynthNoiseWhiteFloat whiteNoiseSource;
+ AudioSynthNoiseGritFloat gritNoiseSource;
+ StateVariableFilter4thOrder svfFilterC;
+ FilterMode typeMappingSVF[3] = {LOWPASS, BANDPASS, HIGHPASS};
+
+ NoisePlethora() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ configParam(X_A_PARAM, 0.f, 1.f, 0.5f, "XA");
+ configParam(RES_A_PARAM, 0.f, 1.f, 0.f, "Resonance A");
+ configParam(CUTOFF_A_PARAM, 0.f, 1.f, 1.f, "Cutoff A");
+ configParam(Y_A_PARAM, 0.f, 1.f, 0.5f, "YA");
+ configParam(CUTOFF_CV_A_PARAM, 0.f, 1.f, 0.f, "Cutoff CV A");
+ configSwitch(FILTER_TYPE_A_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
+ configParam(PROGRAM_PARAM, -INFINITY, +INFINITY, 0.f, "Program/Bank selection");
+ configSwitch(FILTER_TYPE_B_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
+ configParam(CUTOFF_CV_B_PARAM, 0.f, 1.f, 0.f, "Cutoff B");
+ configParam(X_B_PARAM, 0.f, 1.f, 0.5f, "XB");
+ configParam(CUTOFF_B_PARAM, 0.f, 1.f, 1.f, "Cutoff CV B");
+ configParam(RES_B_PARAM, 0.f, 1.f, 0.f, "Resonance B");
+ configParam(Y_B_PARAM, 0.f, 1.f, 0.5f, "YB");
+ configSwitch(FILTER_TYPE_C_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
+ configParam(CUTOFF_C_PARAM, 0.f, 1.f, 1.f, "Cutoff C");
+ configParam(GRIT_PARAM, 0.f, 1.f, 0.f, "Grit Quantity");
+ configParam(RES_C_PARAM, 0.f, 1.f, 0.f, "Resonance C");
+ configParam(CUTOFF_CV_C_PARAM, 0.f, 1.f, 0.f, "Cutoff CV C");
+ configSwitch(SOURCE_C_PARAM, 0.f, 1.f, 0.f, "Filter source", {"Gritty", "White"});
+
+ configInput(X_A_INPUT, "XA CV");
+ configInput(Y_A_INPUT, "YA CV");
+ configInput(CUTOFF_A_INPUT, "Cutoff CV A");
+ configInput(PROG_A_INPUT, "Program select A");
+ configInput(PROG_B_INPUT, "Program select B");
+ configInput(CUTOFF_B_INPUT, "Cutoff CV B");
+ configInput(X_B_INPUT, "XB CV");
+ configInput(Y_B_INPUT, "YB CV");
+ configInput(GRIT_INPUT, "Grit Quantity CV");
+ configInput(CUTOFF_C_INPUT, "Cutoff CV C");
+
+ configOutput(A_OUTPUT, "Algorithm A");
+ configOutput(B_OUTPUT, "Algorithm B");
+ configOutput(GRITTY_OUTPUT, "Gritty noise");
+ configOutput(FILTERED_OUTPUT, "Filtered noise");
+ configOutput(WHITE_OUTPUT, "White noise");
+
+ configLight(BANK_LIGHT, "Bank mode");
+
+ getInputInfo(PROG_A_INPUT)->description = "CV sums with active program (0.5V increments)";
+ getInputInfo(PROG_B_INPUT)->description = "CV sums with active program (0.5V increments)";
+
+ setAlgorithm(SECTION_B, "radioOhNo");
+ setAlgorithm(SECTION_A, "radioOhNo");
+ onSampleRateChange();
+ }
+
+ void onReset(const ResetEvent& e) override {
+ setAlgorithm(SECTION_B, "radioOhNo");
+ setAlgorithm(SECTION_A, "radioOhNo");
+ Module::onReset(e);
+ }
+
+ void onSampleRateChange() override {
+ // set ~20Hz DC blocker
+ const float fc = 22.05f / APP->engine->getSampleRate();
+
+ blockDCFilter[SECTION_A].setFrequency(fc);
+ blockDCFilter[SECTION_B].setFrequency(fc);
+ blockDCFilter[SECTION_C].setFrequency(fc);
+
+ if (algorithm[SECTION_A]) {
+ algorithm[SECTION_A]->init();
+ }
+ if (algorithm[SECTION_B]) {
+ algorithm[SECTION_B]->init();
+ }
+ }
+
+ void process(const ProcessArgs& args) override {
+
+ // we only periodically update parameters of each algorithm (once per block, ~2.9ms at 44100Hz)
+ bool updateParams = false;
+ if (!updateParamsTimer.process(args.sampleTime)) {
+ updateParams = true;
+ updateParamsTimer.trigger(updateTimeSecs);
+ }
+
+ // process A, B and C
+ processTopSection(SECTION_A, X_A_PARAM, Y_A_PARAM,
+ FILTER_TYPE_A_PARAM, CUTOFF_A_PARAM, CUTOFF_CV_A_PARAM, RES_A_PARAM,
+ PROG_A_INPUT, X_A_INPUT, Y_A_INPUT, CUTOFF_A_INPUT, A_OUTPUT, args, updateParams);
+ processTopSection(SECTION_B, X_B_PARAM, Y_B_PARAM,
+ FILTER_TYPE_B_PARAM, CUTOFF_B_PARAM, CUTOFF_CV_B_PARAM, RES_B_PARAM,
+ PROG_B_INPUT, X_B_INPUT, Y_B_INPUT, CUTOFF_B_INPUT, B_OUTPUT, args, updateParams);
+ processBottomSection(args);
+
+ // UI
+ updateDataForLEDDisplay();
+ processProgramBankKnobLogic(args);
+ }
+
+ // process CV for section, specifically: work out the offset relative to the current
+ // program and see if this is a new algorithm
+ void processCVOffsets(Section SECTION, InputIds PROG_INPUT) {
+
+ const int offset = 2 * inputs[PROG_INPUT].getVoltage();
+
+ const int bank = programSelector.getSection(SECTION).getBank();
+ const int numProgramsForBank = getBankForIndex(bank).getSize();
+
+ const int programWithoutCV = programSelector.getSection(SECTION).getProgram();
+ const int programWithCV = unsigned_modulo(programWithoutCV + offset, numProgramsForBank);
+
+ // duplicate key settings to programSelectorWithCV (expect modified program)
+ programSelectorWithCV.setMode(programSelector.getMode());
+ programSelectorWithCV.getSection(SECTION).setBank(bank);
+ programSelectorWithCV.getSection(SECTION).setProgram(programWithCV);
+
+ const std::string newAlgorithmName = programSelectorWithCV.getSection(SECTION).getCurrentProgramName();
+
+ // this is just a caching check to avoid constantly re-initialisating the algorithms
+ if (newAlgorithmName != algorithmName[SECTION]) {
+
+ algorithm[SECTION] = MyFactory::Instance()->Create(newAlgorithmName);
+ algorithmName[SECTION] = newAlgorithmName;
+
+ if (algorithm[SECTION]) {
+ algorithm[SECTION]->init();
+ }
+ else {
+ DEBUG("WARNING: Failed to initialise %s in programSelector", newAlgorithmName.c_str());
+ }
+ }
+ }
+
+ // exactly the same for A and B
+ void processTopSection(Section SECTION, ParamIds X_PARAM, ParamIds Y_PARAM, ParamIds FILTER_TYPE_PARAM,
+ ParamIds CUTOFF_PARAM, ParamIds CUTOFF_CV_PARAM, ParamIds RES_PARAM,
+ InputIds PROG_INPUT, InputIds X_INPUT, InputIds Y_INPUT, InputIds CUTOFF_INPUT, OutputIds OUTPUT,
+ const ProcessArgs& args, bool updateParams) {
+
+ // periodically work out how CV should modify the current sections algorithm
+ if (updateParams) {
+ processCVOffsets(SECTION, PROG_INPUT);
+ }
+
+ float out = 0.f;
+ if (algorithm[SECTION] && outputs[OUTPUT].isConnected()) {
+ float cvX = params[X_PARAM].getValue() + rescale(inputs[X_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f);
+ float cvY = params[Y_PARAM].getValue() + rescale(inputs[Y_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f);
+
+ // update parameters of the algorithm
+ if (updateParams) {
+ algorithm[SECTION]->process(clamp(cvX, 0.f, 1.f), clamp(cvY, 0.f, 1.f));
+ }
+ // process the audio graph
+ out = algorithm[SECTION]->processGraph();
+ // each algorithm has a specific gain factor
+ out = out * programSelectorWithCV.getSection(SECTION).getCurrentProgramGain();
+
+ // if filters are active
+ if (!bypassFilters) {
+
+ // set parameters
+ const float freqCV = std::pow(params[CUTOFF_CV_PARAM].getValue(), 2) * inputs[CUTOFF_INPUT].getVoltage();
+ const float pitch = rescale(params[CUTOFF_PARAM].getValue(), 0, 1, -5.5, +5.5) + freqCV;
+ const float cutoff = clamp(dsp::FREQ_C4 * std::pow(2.f, pitch), 1.f, 20000.);
+ const float cutoffNormalised = clamp(cutoff / args.sampleRate, 0.f, 0.49f);
+ const float q = M_SQRT1_2 + std::pow(params[RES_PARAM].getValue(), 2) * 10.f;
+ const FilterMode mode = typeMappingSVF[(int) params[FILTER_TYPE_PARAM].getValue()];
+ svfFilter[SECTION].setParameters(cutoffNormalised, q);
+
+ // apply filter
+ svfFilter[SECTION].process(out);
+ // and retrieve relevant output
+ out = svfFilter[SECTION].output(mode);
+ }
+
+ if (blockDC) {
+ // cascaded Biquad (4th order highpass at ~20Hz)
+ out = blockDCFilter[SECTION].process(out);
+ }
+ }
+
+ outputs[OUTPUT].setVoltage(Saturator::process(out) * 5.f);
+ }
+
+ // process section C
+ void processBottomSection(const ProcessArgs& args) {
+
+ float gritCv = rescale(clamp(inputs[GRIT_INPUT].getVoltage(), -10.f, 10.f), -10.f, 10.f, -1.f, 1.f);
+ float gritAmount = clamp(1.f - params[GRIT_PARAM].getValue() - gritCv, 0.f, 1.f);
+ float gritFrequency = 0.1 + std::pow(gritAmount, 2) * 20000;
+ gritNoiseSource.setDensity(gritFrequency);
+ float gritNoise = gritNoiseSource.process(args.sampleTime);
+ outputs[GRITTY_OUTPUT].setVoltage(gritNoise * 5.f);
+
+ float whiteNoise = whiteNoiseSource.process();
+ outputs[WHITE_OUTPUT].setVoltage(whiteNoise * 5.f);
+
+ float out = 0.f;
+ if (outputs[FILTERED_OUTPUT].isConnected() && !bypassFilters) {
+
+ const float freqCV = std::pow(params[CUTOFF_CV_C_PARAM].getValue(), 2) * inputs[CUTOFF_C_INPUT].getVoltage();
+ const float pitch = rescale(params[CUTOFF_C_PARAM].getValue(), 0, 1, -5.f, +6.4f) + freqCV;
+ const float cutoff = clamp(dsp::FREQ_C4 * std::pow(2.f, pitch), 1.f, 44100. / 2.f);
+ const float cutoffNormalised = clamp(cutoff / args.sampleRate, 0.f, 0.49f);
+ const float Q = 0.5 + std::pow(params[RES_C_PARAM].getValue(), 2) * 20.f;
+ const FilterMode mode = typeMappingSVF[(int) params[FILTER_TYPE_C_PARAM].getValue()];
+ svfFilterC.setParameters(cutoffNormalised, Q);
+
+ float toFilter = params[SOURCE_C_PARAM].getValue() ? whiteNoise : gritNoise;
+ out = svfFilterC.process(toFilter, mode);
+
+ // assymetric saturator, to get those lovely even harmonics
+ out = Saturator::process(out + 0.33);
+
+ if (blockDC) {
+ // cascaded Biquad (4th order highpass at ~20Hz)
+ out = blockDCFilter[SECTION_C].process(out);
+ }
+ }
+ else if (bypassFilters) {
+ out = params[SOURCE_C_PARAM].getValue() ? whiteNoise : gritNoise;
+ }
+
+ outputs[FILTERED_OUTPUT].setVoltage(out * 5.f);
+ }
+
+ // set which text NoisePlethoraWidget should display on the 7 segment display
+ void updateDataForLEDDisplay() {
+
+ if (programKnobMode == PROGRAM_MODE) {
+ textDisplayA = std::to_string(programSelectorWithCV.getA().getProgram());
+ }
+ else if (programKnobMode == BANK_MODE) {
+ textDisplayA = 'A' + programSelectorWithCV.getA().getBank();
+ }
+ isDisplayActiveA = programSelectorWithCV.getMode() == SECTION_A;
+
+ if (programKnobMode == PROGRAM_MODE) {
+ textDisplayB = std::to_string(programSelectorWithCV.getB().getProgram());
+ }
+ else if (programKnobMode == BANK_MODE) {
+ textDisplayB = 'A' + programSelectorWithCV.getB().getBank();
+ }
+ isDisplayActiveB = programSelectorWithCV.getMode() == SECTION_B;
+ }
+
+ // handle convoluted logic for the multifunction Program knob
+ void processProgramBankKnobLogic(const ProcessArgs& args) {
+
+ // program knob will either change program for current bank...
+ if (programButtonDragged) {
+ // work out the change (in discrete increments) since the program/bank knob started being dragged
+ const int delta = (int)(dialResolution * (params[PROGRAM_PARAM].getValue() - programKnobReferenceState));
+
+ if (programKnobMode == PROGRAM_MODE) {
+ const int numProgramsForCurrentBank = getBankForIndex(programSelector.getCurrent().getBank()).getSize();
+
+ if (delta != 0) {
+ const int newProgramFromKnob = unsigned_modulo(programSelector.getCurrent().getProgram() + delta, numProgramsForCurrentBank);
+ programKnobReferenceState = params[PROGRAM_PARAM].getValue();
+ setAlgorithmViaProgram(newProgramFromKnob);
+ }
+ }
+ // ...or change bank, (trying to) keep program the same
+ else {
+
+ if (delta != 0) {
+ const int newBankFromKnob = unsigned_modulo(programSelector.getCurrent().getBank() + delta, numBanks);
+ programKnobReferenceState = params[PROGRAM_PARAM].getValue();
+ setAlgorithmViaBank(newBankFromKnob);
+ }
+ }
+ }
+
+ // if we have a new "press" on the knob, time it
+ if (programHoldTrigger.process(programButtonHeld)) {
+ programHoldTimer.reset();
+ }
+
+ // but cancel if it's actually a knob drag
+ if (programButtonDragged) {
+ programHoldTimer.reset();
+ programButtonHeld = false;
+ }
+ else {
+ if (programButtonHeld) {
+ programHoldTimer.process(args.sampleTime);
+
+ // if we've held for at least 0.5 seconds, switch into "bank mode"
+ if (programHoldTimer.time > 0.5f) {
+ programButtonHeld = false;
+ programHoldTimer.reset();
+
+ if (programKnobMode == PROGRAM_MODE) {
+ programKnobMode = BANK_MODE;
+ }
+ else {
+ programKnobMode = PROGRAM_MODE;
+ }
+
+ lights[BANK_LIGHT].setBrightness(programKnobMode == BANK_MODE);
+ }
+ }
+ // no longer held, but has been held for non-zero time (without being dragged), toggle "active" section (A or B),
+ // this is effectively just a "click"
+ else if (programHoldTimer.time > 0.f) {
+ programSelector.setMode(!programSelector.getMode());
+ programHoldTimer.reset();
+ }
+ }
+
+ if (!programButtonDragged) {
+ programKnobReferenceState = params[PROGRAM_PARAM].getValue();
+ }
+ }
+
+ void setAlgorithmViaProgram(int newProgram) {
+
+ const int currentBank = programSelector.getCurrent().getBank();
+ const std::string algorithmName = getBankForIndex(currentBank).getProgramName(newProgram);
+ const int section = programSelector.getMode();
+
+ setAlgorithm(section, algorithmName);
+ }
+
+ void setAlgorithmViaBank(int newBank) {
+ newBank = clamp(newBank, 0, numBanks - 1);
+ const int currentProgram = programSelector.getCurrent().getProgram();
+ // the new bank may not have as many algorithms
+ const int currentProgramInNewBank = clamp(currentProgram, 0, getBankForIndex(newBank).getSize() - 1);
+ const std::string algorithmName = getBankForIndex(newBank).getProgramName(currentProgramInNewBank);
+ const int section = programSelector.getMode();
+
+ setAlgorithm(section, algorithmName);
+ }
+
+ void setAlgorithm(int section, std::string algorithmName) {
+
+ if (section > 1) {
+ return;
+ }
+
+ for (int bank = 0; bank < numBanks; ++bank) {
+ for (int program = 0; program < getBankForIndex(bank).getSize(); ++program) {
+ if (getBankForIndex(bank).getProgramName(program) == algorithmName) {
+ programSelector.setMode(section);
+ programSelector.getCurrent().setBank(bank);
+ programSelector.getCurrent().setProgram(program);
+
+ return;
+ }
+ }
+ }
+
+ DEBUG("WARNING: Didn't find %s in programSelector", algorithmName.c_str());
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* bankAJ = json_object_get(rootJ, "algorithmA");
+ if (bankAJ) {
+ setAlgorithm(SECTION_A, json_string_value(bankAJ));
+ }
+
+ json_t* bankBJ = json_object_get(rootJ, "algorithmB");
+ if (bankBJ) {
+ setAlgorithm(SECTION_B, json_string_value(bankBJ));
+ }
+
+ json_t* bypassFiltersJ = json_object_get(rootJ, "bypassFilters");
+ if (bypassFiltersJ) {
+ bypassFilters = json_boolean_value(bypassFiltersJ);
+ }
+
+ json_t* blockDCJ = json_object_get(rootJ, "blockDC");
+ if (blockDCJ) {
+ blockDC = json_boolean_value(blockDCJ);
+ }
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+
+ json_object_set_new(rootJ, "algorithmA", json_string(programSelector.getA().getCurrentProgramName().c_str()));
+ json_object_set_new(rootJ, "algorithmB", json_string(programSelector.getB().getCurrentProgramName().c_str()));
+
+ json_object_set_new(rootJ, "bypassFilters", json_boolean(bypassFilters));
+ json_object_set_new(rootJ, "blockDC", json_boolean(blockDC));
+
+ return rootJ;
+ }
+};
+
+
+struct BefacoTinyKnobSnapPress : BefacoTinyKnobBlack {
+ BefacoTinyKnobSnapPress() { }
+
+ // this seems convoluted but I can't see how to achieve the following (say) with onAction:
+ // a) need to support standard knob dragging behaviour
+ // b) need to support click detection (not drag)
+ // c) need to distinguish short click from long click
+ //
+ // To achieve this we have 2 state variables, held and dragged. The Module thread will
+ // time how long programButtonHeld is "true" and switch between bank and program mode if
+ // longer than X secs. A drag, as measured here as a displacement greater than Y, will cancel
+ // this timer and we're back to normal param mode. See processProgramBankKnobLogic(...) for details
+ void onDragStart(const DragStartEvent& e) override {
+ NoisePlethora* noisePlethoraModule = static_cast(module);
+ if (noisePlethoraModule) {
+ noisePlethoraModule->programButtonHeld = true;
+ noisePlethoraModule->programButtonDragged = false;
+ }
+
+ pos = Vec(0, 0);
+ Knob::onDragStart(e);
+ }
+
+ void onDragMove(const DragMoveEvent& e) override {
+ pos += e.mouseDelta;
+ NoisePlethora* noisePlethoraModule = static_cast(module);
+
+ // cancel if we're doing a drag
+ if (noisePlethoraModule && std::sqrt(pos.square()) > 5) {
+ noisePlethoraModule->programButtonHeld = false;
+ noisePlethoraModule->programButtonDragged = true;
+ }
+
+ Knob::onDragMove(e);
+ }
+
+ void onDragEnd(const DragEndEvent& e) override {
+
+ NoisePlethora* noisePlethoraModule = static_cast(module);
+ if (noisePlethoraModule) {
+ noisePlethoraModule->programButtonHeld = false;
+ }
+
+ Knob::onDragEnd(e);
+ }
+
+ // suppress double click
+ void onDoubleClick(const DoubleClickEvent& e) override {}
+
+ Vec pos;
+};
+
+// dervied from https://github.com/countmodula/VCVRackPlugins/blob/v2.0.0/src/components/CountModulaLEDDisplay.hpp
+struct NoisePlethoraLEDDisplay : LightWidget {
+ float fontSize = 28;
+ Vec textPos = Vec(2, 25);
+ int numChars = 1;
+ bool activeDisplay = true;
+ NoisePlethora* module;
+ NoisePlethora::Section section = NoisePlethora::SECTION_A;
+ ui::Tooltip* tooltip;
+
+ NoisePlethoraLEDDisplay() {
+ box.size = mm2px(Vec(7.236, 10));
+ }
+
+ void onEnter(const EnterEvent& e) override {
+ LightWidget::onEnter(e);
+ setTooltip();
+ }
+
+ void setTooltip() {
+ std::string activeName = module->programSelector.getSection(section).getCurrentProgramName();
+ tooltip = new ui::Tooltip;
+ tooltip->text = activeName;
+ APP->scene->addChild(tooltip);
+ }
+
+ void onHover(const event::Hover& e) override {
+ LightWidget::onHover(e);
+ e.consume(this);
+ }
+
+ void onLeave(const event::Leave& e) override {
+ LightWidget::onLeave(e);
+
+ APP->scene->removeChild(tooltip);
+ this->tooltip->requestDelete();
+ this->tooltip = NULL;
+ }
+
+ void setCentredPos(Vec pos) {
+ box.pos.x = pos.x - box.size.x / 2;
+ box.pos.y = pos.y - box.size.y / 2;
+ }
+
+ void drawBackground(const DrawArgs& args) override {
+ // Background
+ NVGcolor backgroundColor = nvgRGB(0x24, 0x14, 0x14);
+ NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
+ nvgBeginPath(args.vg);
+ nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
+ nvgFillColor(args.vg, backgroundColor);
+ nvgFill(args.vg);
+ nvgStrokeWidth(args.vg, 1.0);
+ nvgStrokeColor(args.vg, borderColor);
+ nvgStroke(args.vg);
+ }
+
+ void drawLight(const DrawArgs& args) override {
+ // Background
+ NVGcolor backgroundColor = nvgRGB(0x24, 0x14, 0x14);
+ NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
+ NVGcolor textColor = nvgRGB(0xaa, 0x20, 0x20);
+
+ // use slightly different LED colour if CV is connected as visual cue
+ if (module) {
+ NoisePlethora::InputIds inputId = (section == NoisePlethora::SECTION_A) ? NoisePlethora::PROG_A_INPUT : NoisePlethora::PROG_B_INPUT;
+ if (module->inputs[inputId].isConnected()) {
+ textColor = nvgRGB(0xff, 0x40, 0x40);
+ }
+ }
+
+ nvgBeginPath(args.vg);
+ nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
+ nvgFillColor(args.vg, backgroundColor);
+ nvgFill(args.vg);
+ nvgStrokeWidth(args.vg, 1.0);
+ nvgStrokeColor(args.vg, borderColor);
+ nvgStroke(args.vg);
+
+ std::shared_ptr font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/Segment7Standard.otf"));
+
+ if (font && font->handle >= 0) {
+
+ std::string text = "A"; // fallback if module not yet defined
+ if (module) {
+ text = (section == NoisePlethora::SECTION_A) ? module->textDisplayA : module->textDisplayB;
+ }
+ char buffer[numChars + 1];
+ int l = text.size();
+ if (l > numChars)
+ l = numChars;
+
+ nvgGlobalTint(args.vg, color::WHITE);
+
+ text.copy(buffer, l);
+ buffer[numChars] = '\0';
+
+ nvgFontSize(args.vg, fontSize);
+ nvgFontFaceId(args.vg, font->handle);
+ nvgTextLetterSpacing(args.vg, 1);
+
+ // render the "off" segments
+ nvgFillColor(args.vg, nvgTransRGBA(textColor, 18));
+ nvgText(args.vg, textPos.x, textPos.y, "8", NULL);
+
+ // render the "on segments"
+ nvgFillColor(args.vg, textColor);
+
+ nvgText(args.vg, textPos.x, textPos.y, buffer, NULL);
+ }
+
+ if (module) {
+ const bool isSectionDisplayActive = (section == NoisePlethora::SECTION_A) ? module->isDisplayActiveA : module->isDisplayActiveB;
+
+ // active bank dot
+ nvgBeginPath(args.vg);
+ nvgCircle(args.vg, 18, 26, 1.5);
+ nvgFillColor(args.vg, isSectionDisplayActive ? textColor : nvgTransRGBA(textColor, 18));
+ nvgFill(args.vg);
+ }
+ }
+};
+
+struct NoisePlethoraWidget : ModuleWidget {
+ NoisePlethoraWidget(NoisePlethora* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/NoisePlethora.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ // A params
+ addParam(createParamCentered(mm2px(Vec(22.325, 16.09)), module, NoisePlethora::X_A_PARAM));
+ addParam(createParamCentered(mm2px(Vec(22.325, 30.595)), module, NoisePlethora::Y_A_PARAM));
+ addParam(createParamCentered(mm2px(Vec(43.248, 23.058)), module, NoisePlethora::CUTOFF_A_PARAM));
+ addParam(createParamCentered(mm2px(Vec(63.374, 16.09)), module, NoisePlethora::RES_A_PARAM));
+ addParam(createParamCentered(mm2px(Vec(63.374, 30.595)), module, NoisePlethora::CUTOFF_CV_A_PARAM));
+ addParam(createParam(mm2px(Vec(41.494, 38.579)), module, NoisePlethora::FILTER_TYPE_A_PARAM));
+
+ // (bank)
+ addParam(createParamCentered(mm2px(Vec(30.866, 49.503)), module, NoisePlethora::PROGRAM_PARAM));
+
+ // B params
+ addParam(createParamCentered(mm2px(Vec(22.345, 68.408)), module, NoisePlethora::X_B_PARAM));
+ addParam(createParamCentered(mm2px(Vec(22.345, 82.695)), module, NoisePlethora::Y_B_PARAM));
+ addParam(createParamCentered(mm2px(Vec(43.248, 75.551)), module, NoisePlethora::CUTOFF_B_PARAM));
+ addParam(createParamCentered(mm2px(Vec(63.383, 82.686)), module, NoisePlethora::RES_B_PARAM));
+ addParam(createParamCentered(mm2px(Vec(63.36, 68.388)), module, NoisePlethora::CUTOFF_CV_B_PARAM));
+ addParam(createParam(mm2px(Vec(41.494, 53.213)), module, NoisePlethora::FILTER_TYPE_B_PARAM));
+
+ // C params
+ addParam(createParamCentered(mm2px(Vec(7.6, 99.584)), module, NoisePlethora::GRIT_PARAM));
+ addParam(createParamCentered(mm2px(Vec(22.366, 99.584)), module, NoisePlethora::RES_C_PARAM));
+ addParam(createParamCentered(mm2px(Vec(47.536, 98.015)), module, NoisePlethora::CUTOFF_C_PARAM));
+ addParam(createParamCentered(mm2px(Vec(63.36, 99.584)), module, NoisePlethora::CUTOFF_CV_C_PARAM));
+ addParam(createParam(mm2px(Vec(33.19, 92.506)), module, NoisePlethora::FILTER_TYPE_C_PARAM));
+ addParam(createParam(mm2px(Vec(31.707, 104.225)), module, NoisePlethora::SOURCE_C_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(6.431, 16.102)), module, NoisePlethora::X_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.431, 29.959)), module, NoisePlethora::Y_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(52.845, 42.224)), module, NoisePlethora::CUTOFF_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.431, 43.212)), module, NoisePlethora::PROG_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.431, 55.801)), module, NoisePlethora::PROG_B_INPUT));
+ addInput(createInputCentered(mm2px(Vec(52.845, 56.816)), module, NoisePlethora::CUTOFF_B_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.431, 68.398)), module, NoisePlethora::X_B_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.431, 82.729)), module, NoisePlethora::Y_B_INPUT));
+ addInput(createInputCentered(mm2px(Vec(7.555, 114.8)), module, NoisePlethora::GRIT_INPUT));
+ addInput(createInputCentered(mm2px(Vec(63.36, 114.8)), module, NoisePlethora::CUTOFF_C_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(64.909, 44.397)), module, NoisePlethora::A_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(64.915, 54.608)), module, NoisePlethora::B_OUTPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(22.345, 114.852)), module, NoisePlethora::GRITTY_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(34.981, 114.852)), module, NoisePlethora::FILTERED_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(47.536, 114.852)), module, NoisePlethora::WHITE_OUTPUT));
+
+ addChild(createLightCentered>(mm2px(Vec(30.866, 37.422)), module, NoisePlethora::BANK_LIGHT));
+
+
+ NoisePlethoraLEDDisplay* displayA = createWidget(mm2px(Vec(13.106, 38.172)));
+ displayA->module = module;
+ displayA->section = NoisePlethora::SECTION_A;
+ addChild(displayA);
+
+ NoisePlethoraLEDDisplay* displayB = createWidget(mm2px(Vec(13.106, 50.712)));
+ displayB->module = module;
+ displayB->section = NoisePlethora::SECTION_B;
+ addChild(displayB);
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ NoisePlethora* module = dynamic_cast(this->module);
+ assert(module);
+
+ // build the two algorithm selection menus programmatically
+ menu->addChild(createMenuLabel("Algorithms"));
+ std::vector bankAliases = {"Textures", "HH Clusters", "Harsh & Wild", "Test"};
+ char programNames[] = "AB";
+ for (int sectionId = 0; sectionId < 2; ++sectionId) {
+
+ menu->addChild(createSubmenuItem(string::f("Program %c", programNames[sectionId]), "",
+ [ = ](Menu * menu) {
+ for (int i = 0; i < numBanks; i++) {
+ const int currentBank = module->programSelector.getSection(sectionId).getBank();
+ const int currentProgram = module->programSelector.getSection(sectionId).getProgram();
+
+ menu->addChild(createSubmenuItem(string::f("Bank %d: %s", i + 1, bankAliases[i].c_str()), currentBank == i ? CHECKMARK_STRING : "", [ = ](Menu * menu) {
+ for (int j = 0; j < getBankForIndex(i).getSize(); ++j) {
+ const bool currentProgramAndBank = (currentProgram == j) && (currentBank == i);
+ const std::string algorithmName = getBankForIndex(i).getProgramName(j);
+
+ bool implemented = false;
+ for (auto item : MyFactory::Instance()->factoryFunctionRegistry) {
+ if (item.first == algorithmName) {
+ implemented = true;
+ break;
+ }
+ }
+
+ if (implemented) {
+ menu->addChild(createMenuItem(algorithmName, currentProgramAndBank ? CHECKMARK_STRING : "",
+ [ = ]() {
+ module->setAlgorithm(sectionId, algorithmName);
+ }));
+ }
+ else {
+ // placeholder text (greyed out)
+ menu->addChild(createMenuLabel(algorithmName));
+ }
+ }
+ }));
+ }
+ }));
+
+
+ }
+
+ menu->addChild(createMenuLabel("Filters"));
+ menu->addChild(createBoolPtrMenuItem("Remove DC", "", &module->blockDC));
+ menu->addChild(createBoolPtrMenuItem("Bypass Filters", "", &module->bypassFilters));
+ }
+};
+
+
+Model* modelNoisePlethora = createModel("NoisePlethora");
\ No newline at end of file
diff --git a/src/PulseGenerator_4.hpp b/src/PulseGenerator_4.hpp
deleted file mode 100644
index f3229ad..0000000
--- a/src/PulseGenerator_4.hpp
+++ /dev/null
@@ -1,28 +0,0 @@
-#pragma once
-#include
-
-
-/** When triggered, holds a high value for a specified time before going low again */
-struct PulseGenerator_4 {
- simd::float_4 remaining = 0.f;
-
- /** Immediately disables the pulse */
- void reset() {
- remaining = 0.f;
- }
-
- /** Advances the state by `deltaTime`. Returns whether the pulse is in the HIGH state. */
- simd::float_4 process(float deltaTime) {
-
- simd::float_4 mask = (remaining > 0.f);
-
- remaining -= ifelse(mask, deltaTime, 0.f);
- return ifelse(mask, simd::float_4::mask(), 0.f);
- }
-
- /** Begins a trigger with the given `duration`. */
- void trigger(simd::float_4 mask, float duration = 1e-3f) {
- // Keep the previous pulse if the existing pulse will be held longer than the currently requested one.
- remaining = ifelse(mask & (duration > remaining), duration, remaining);
- }
-};
diff --git a/src/Rampage.cpp b/src/Rampage.cpp
index 8b2f959..b204b84 100644
--- a/src/Rampage.cpp
+++ b/src/Rampage.cpp
@@ -1,5 +1,4 @@
#include "plugin.hpp"
-#include "PulseGenerator_4.hpp"
using simd::float_4;
diff --git a/src/SpringReverb.cpp b/src/SpringReverb.cpp
index 1194787..59dd5b8 100644
--- a/src/SpringReverb.cpp
+++ b/src/SpringReverb.cpp
@@ -71,6 +71,16 @@ struct SpringReverb : Module {
delete convolver;
}
+ void processBypass(const ProcessArgs& args) override {
+ float in1 = inputs[IN1_INPUT].getVoltageSum();
+ float in2 = inputs[IN2_INPUT].getVoltageSum();
+
+ float dry = clamp(in1 + in2, -10.0f, 10.0f);
+
+ outputs[WET_OUTPUT].setVoltage(dry);
+ outputs[MIX_OUTPUT].setVoltage(dry);
+ }
+
void process(const ProcessArgs& args) override {
float in1 = inputs[IN1_INPUT].getVoltageSum();
float in2 = inputs[IN2_INPUT].getVoltageSum();
@@ -129,9 +139,9 @@ struct SpringReverb : Module {
outputs[WET_OUTPUT].setVoltage(clamp(wet, -10.0f, 10.0f));
outputs[MIX_OUTPUT].setVoltage(clamp(mix, -10.0f, 10.0f));
- // process VU lights
+ // process VU lights
vuFilter.process(args.sampleTime, wet);
- // process peak light
+ // process peak light
lightFilter.process(args.sampleTime, dry * 50.0);
if (lightRefreshClock.process()) {
diff --git a/src/noise-plethora/LICENSE.md b/src/noise-plethora/LICENSE.md
new file mode 100644
index 0000000..33c208d
--- /dev/null
+++ b/src/noise-plethora/LICENSE.md
@@ -0,0 +1,3 @@
+Plugins in subdirectory `./plugins` have been derived from source files from https://github.com/Befaco/Noise_plethora/ (prefix `P_*.hpp`) licensed under GPL-3.0-or-later (see https://github.com/Befaco/Noise_plethora/blob/master/README.md).
+
+The audio components in subdirectory `./teensy` include modified versions of the [Teensy Audio library](https://github.com/PaulStoffregen/Audio), licensed under the MIT License.
diff --git a/src/noise-plethora/README.md b/src/noise-plethora/README.md
new file mode 100644
index 0000000..32f366a
--- /dev/null
+++ b/src/noise-plethora/README.md
@@ -0,0 +1,17 @@
+Technical Notes
+===============
+
+The Noise Plethora module consists of a number of plugins designed to run on the Teensy microcontroller, and specifically using the powerful [Teensy audio library](https://www.pjrc.com/teensy/td_libs_Audio.html). This consists of a number of oscillators, filters, noise sources, effects etc. Unfortunately these are designed to run directly on the Teensy, and target ARM processors. A small subset of the library that is required for NoisePlethora has been ported (see `./teensy`), with minor adaptions:
+
+* any parts that only have ARM instructions have been reimplemented (generally with slower versions)
+* Teensy fixes the sample rate at 44100Hz, so parts have been updated to allow arbitrary sample rates
+* Teensy uses a simple graph based processing approach, where blocks are processed in the order in which they are _declared_ in the C/C++ code. The code to produce and process this graph is too complex to port, so I've instead modify each Teensy unit's `process()` call to take audio blocks as input/output where appropriate, and manually constructed the computation graph by inspection. (In both the original library and the port, every element in the graph is processed once per loop, and some results stored for the next computation where needed).
+
+The VCV plugin will generate 1 block's worth of audio (~2.9ms at 44100Hz), store this in a buffer, and play back sample by sample until the buffer empties and the process is repeated.
+
+An example Teensy plugin is shown below:
+
+
+I used my own Teensy to debug and try out things quickly in VCV. Teensy provides a usb audio device which Rack trivially recognises (this is useful to allow rapid dev + it decouples Teensy logic from Noise Plethora's filters):
+
+
diff --git a/src/noise-plethora/plugins/Banks.cpp b/src/noise-plethora/plugins/Banks.cpp
new file mode 100644
index 0000000..30f1a17
--- /dev/null
+++ b/src/noise-plethora/plugins/Banks.cpp
@@ -0,0 +1,112 @@
+#include "Banks.hpp"
+#include "Banks_Def.hpp"
+
+const Bank::BankElem Bank::defaultElem = {"", 1.0};
+
+Bank::Bank() {
+ programs.fill(defaultElem);
+}
+
+Bank::Bank(const BankElem& p1, const BankElem& p2, const BankElem& p3,
+ const BankElem& p4, const BankElem& p5, const BankElem& p6,
+ const BankElem& p7, const BankElem& p8, const BankElem& p9,
+ const BankElem& p10)
+ : programs{p1, p2, p3, p4, p5, p6, p7, p8, p9, p10}
+{ }
+
+const std::string Bank::getProgramName(int i) {
+ if (i >= 0 && i < programsPerBank) {
+ return programs[i].name;
+ }
+ return "";
+}
+
+float Bank::getProgramGain(int i) {
+ if (i >= 0 && i < programsPerBank) {
+ return programs[i].gain;
+ }
+ return 1.0;
+}
+
+int Bank::getSize() {
+ int size = 0;
+ for (auto it = programs.begin(); it != programs.end(); it++) {
+ if ((*it).name == "") {
+ break;
+ }
+ size++;
+ }
+ return size;
+}
+
+
+
+// Bank A:
+#include "P_radioOhNo.hpp"
+#include "P_Rwalk_SineFMFlange.hpp"
+#include "P_xModRingSqr.hpp"
+#include "P_XModRingSine.hpp"
+#include "P_CrossModRing.hpp"
+#include "P_resonoise.hpp"
+#include "P_grainGlitch.hpp"
+#include "P_grainGlitchII.hpp"
+#include "P_grainGlitchIII.hpp"
+#include "P_basurilla.hpp"
+
+// Bank B: HH clusters
+#include "P_clusterSaw.hpp"
+#include "P_pwCluster.hpp"
+#include "P_crCluster2.hpp"
+#include "P_sineFMcluster.hpp"
+#include "P_TriFMcluster.hpp"
+#include "P_PrimeCluster.hpp"
+#include "P_PrimeCnoise.hpp"
+#include "P_FibonacciCluster.hpp"
+#include "P_partialCluster.hpp"
+#include "P_phasingCluster.hpp"
+
+// Bank C: harsh and wild
+#include "P_BasuraTotal.hpp"
+#include "P_Atari.hpp"
+#include "P_WalkingFilomena.hpp"
+#include "P_S_H.hpp"
+#include "P_arrayOnTheRocks.hpp"
+#include "P_existencelsPain.hpp"
+#include "P_whoKnows.hpp"
+#include "P_satanWorkout.hpp"
+#include "P_Rwalk_BitCrushPW.hpp"
+#include "P_Rwalk_LFree.hpp"
+
+// Bank D: Test / other
+//#include "P_TestPlugin.hpp"
+//#include "P_TeensyAlt.hpp"
+//#include "P_WhiteNoise.hpp"
+//#include "P_Rwalk_LBit.hpp"
+//#include "P_Rwalk_SineFM.hpp"
+//#include "P_VarWave.hpp"
+//#include "P_RwalkVarWave.hpp"
+//#include "P_Rwalk_ModWave.hpp"
+//#include "P_Rwalk_WaveTwist.hpp"
+
+
+static const Bank bank1 BANKS_DEF_1; // Banks_Def.hpp
+static const Bank bank2 BANKS_DEF_2;
+static const Bank bank3 BANKS_DEF_3;
+//static const Bank bank4 BANKS_DEF_4;
+//static const Bank bank5 BANKS_DEF_5;
+static std::array banks { bank1, bank2, bank3 }; //, bank5 };
+
+// static const Bank bank6 BANKS_DEF_6;
+// static const Bank bank7 BANKS_DEF_7;
+// static const Bank bank8 BANKS_DEF_8;
+// static const Bank bank9 BANKS_DEF_9;
+// static const Bank bank10 BANKS_DEF_10;
+// static std::array banks { bank1, bank2, bank3, bank4, bank5, bank6, bank7, bank8, bank9, bank10 };
+
+Bank& getBankForIndex(int i) {
+ if (i < 0)
+ i = 0;
+ if (i >= programsPerBank)
+ i = (programsPerBank - 1);
+ return banks[i];
+}
diff --git a/src/noise-plethora/plugins/Banks.hpp b/src/noise-plethora/plugins/Banks.hpp
new file mode 100644
index 0000000..4896a0c
--- /dev/null
+++ b/src/noise-plethora/plugins/Banks.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include
+#include
+#include
+
+static const int programsPerBank = 10;
+static const int numBanks = 3;
+
+struct Bank {
+
+ struct BankElem {
+ BankElem() {};
+
+ BankElem(std::string n, float g = 1.0)
+ : name{n}
+ , gain{g}
+ {}
+
+ std::string name = "";
+ float gain = 1.0;
+ };
+
+ static const BankElem defaultElem;
+
+ Bank();
+ Bank(const BankElem& p1, const BankElem& p2 = defaultElem,
+ const BankElem& p3 = defaultElem, const BankElem& p4 = defaultElem,
+ const BankElem& p5 = defaultElem, const BankElem& p6 = defaultElem,
+ const BankElem& p7 = defaultElem, const BankElem& p8 = defaultElem,
+ const BankElem& p9 = defaultElem, const BankElem& p10 = defaultElem);
+
+ const std::string getProgramName(int i);
+ float getProgramGain(int i);
+
+ int getSize();
+
+private:
+
+ std::array programs;
+
+};
+
+Bank& getBankForIndex(int i);
diff --git a/src/noise-plethora/plugins/Banks_Def.hpp b/src/noise-plethora/plugins/Banks_Def.hpp
new file mode 100644
index 0000000..41c1f63
--- /dev/null
+++ b/src/noise-plethora/plugins/Banks_Def.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#define BANKS_DEF_1 { \
+ { "radioOhNo", 1.0 }, \
+ { "Rwalk_SineFMFlange", 1.0 }, \
+ { "xModRingSqr", 1.0 }, \
+ { "XModRingSine", 1.0 }, \
+ { "CrossModRing", 1.0 }, \
+ { "resonoise", 1.0 }, \
+ { "grainGlitch", 1.0 }, \
+ { "grainGlitchII", 1.0 }, \
+ { "grainGlitchIII", 1.0 }, \
+ { "basurilla", 1.0 } \
+ }
+
+#define BANKS_DEF_2 { \
+ { "clusterSaw", 1.0 }, \
+ { "pwCluster", 1.0 }, \
+ { "crCluster2", 1.0 }, \
+ { "sineFMcluster", 1.0 }, \
+ { "TriFMcluster", 1.0 }, \
+ { "PrimeCluster", 0.8 }, \
+ { "PrimeCnoise", 0.8 }, \
+ { "FibonacciCluster", 1.0 }, \
+ { "partialCluster", 1.0 }, \
+ { "phasingCluster", 1.0 } \
+ }
+
+#define BANKS_DEF_3 { \
+ { "BasuraTotal", 1.0 }, \
+ { "Atari", 1.0 }, \
+ { "WalkingFilomena", 1.0 }, \
+ { "S_H", 1.0 }, \
+ { "arrayOnTheRocks", 1.0 }, \
+ { "existencelsPain", 1.0 }, \
+ { "whoKnows", 1.0 }, \
+ { "satanWorkout", 1.0 }, \
+ { "Rwalk_BitCrushPW", 1.0 }, \
+ { "Rwalk_LFree", 1.0 } \
+ }
+
+#define BANKS_DEF_4 { \
+ { "TestPlugin", 1.0 }, \
+ { "WhiteNoise", 1.0 }, \
+ { "TeensyAlt", 1.0 } \
+ }
+#define BANKS_DEF_5
+
+// #define BANKS_DEF_6
+// #define BANKS_DEF_7
+// #define BANKS_DEF_8
+// #define BANKS_DEF_9
+// #define BANKS_DEF_10
diff --git a/src/noise-plethora/plugins/NoisePlethoraPlugin.hpp b/src/noise-plethora/plugins/NoisePlethoraPlugin.hpp
new file mode 100644
index 0000000..63e8e3a
--- /dev/null
+++ b/src/noise-plethora/plugins/NoisePlethoraPlugin.hpp
@@ -0,0 +1,90 @@
+#pragma once
+
+#include
+#include
+#include // string might not be allowed
+#include
+#include