diff --git a/README.md b/README.md index f21aadf..569d529 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,36 @@ # Venom -Venom modules version 2.6.0 for VCV Rack 2 are copyright 2023, 2024 Dave Benham and licensed under GNU General Public License version 3. +Venom modules version 2.7.0 for VCV Rack 2 are copyright 2023, 2024 Dave Benham and licensed under GNU General Public License version 3. [Color Coded Ports](#color-coded-ports) [Themes](#themes) [Custom Names](#custom-names) [Parameter Locks and Custom Defaults](#parameter-locks-and-custom-defaults) +[Venom Expander Modules](#venom-expander-modules) [Acknowledgments](#acknowledgments) -|[BENJOLIN
OSCILLATOR](#benjolin-oscillator)|[BERNOULLI
SWITCH](#bernoulli-switch)|[BERNOULLI
SWITCH
EXPANDER](#bernoulli-switch-expander)|[CLONE
MERGE](#clone-merge)|[HARMONIC
QUANTIZER](#harmonic-quantizer)|[KNOB 5](#knob-5)|[LINEAR
BEATS](#linear-beats)|[LINEAR
BEATS
EXPANDER](#linear-beats-expander)| +|[AUXILLIARY
CLONE
EXPANDER](#auxilliary-clone-expander)|[BENJOLIN
OSCILLATOR](#benjolin-oscillator)|[BERNOULLI
SWITCH](#bernoulli-switch)|[BERNOULLI
SWITCH
EXPANDER](#bernoulli-switch-expander)|[CLONE
MERGE](#clone-merge)|[HARMONIC
QUANTIZER](#harmonic-quantizer)|[KNOB 5](#knob-5)|[LINEAR
BEATS](#linear-beats)| |----|----|----|----|----|----|----|----| -|![Benjolin Oscillator module image](doc/BenjolinOsc.png)|![Bernoulli Switch module image](doc/BernoulliSwitch.png)|![Bernoulli Switch Expander image](doc/BernoulliSwitchExpander.png)|![Clone Merge module image](doc/CloneMerge.png)|![Harmonic Quantizer module image](doc/HQ.PNG)|![Knob 5 module image](doc/Knob5.png)|![Linear Beats module image](doc/LinearBeats.png)|![Linear Beats Expander module image](doc/LinearBeatsExpander.png)| +|![Auxilliary Clone Expander module image](doc/AuxClone.png)|![Benjolin Oscillator module image](doc/BenjolinOsc.png)|![Bernoulli Switch module image](doc/BernoulliSwitch.png)|![Bernoulli Switch Expander image](doc/BernoulliSwitchExpander.png)|![Clone Merge module image](doc/CloneMerge.png)|![Harmonic Quantizer module image](doc/HQ.PNG)|![Knob 5 module image](doc/Knob5.png)|![Linear Beats module image](doc/LinearBeats.png)| -|[LOGIC](#logic)|[MIX 4](#mix-4)|[MIX 4
STEREO](#mix-4-stereo)|[MIX EXPANDERS](#mix-expanders)| -|----|----|----|----| -|![Logic module image](doc/Logic.png)|![Mix 4 module image](doc/Mix4.png)|![Mix 4 Stereo module image](doc/Mix4Stereo.png)|![Mix Offset Expander module image](doc/MixOffset.png)  ![Mix Mute Expander module image](doc/MixMute.png)  ![Mix Solo Expander module image](doc/MixSolo.png)  ![Mix Fade Expander module image](doc/MixFade.png)  ![Mix Fade2 Expander module image](doc/MixFade2.png)  ![Mix Pan Expander module image](doc/MixPan.png)  ![Mix Send Expander module image](doc/MixSend.png)| +|[LINEAR
BEATS
EXPANDER](#linear-beats-expander)|[LOGIC](#logic)|[MIX 4](#mix-4)|[MIX 4
STEREO](#mix-4-stereo)|[MIX EXPANDERS](#mix-expanders)| +|----|----|----|----|----| +|![Linear Beats Expander module image](doc/LinearBeatsExpander.png)|![Logic module image](doc/Logic.png)|![Mix 4 module image](doc/Mix4.png)|![Mix 4 Stereo module image](doc/Mix4Stereo.png)|![Mix Offset Expander module image](doc/MixOffset.png)  ![Mix Mute Expander module image](doc/MixMute.png)  ![Mix Solo Expander module image](doc/MixSolo.png)  ![Mix Fade Expander module image](doc/MixFade.png)  ![Mix Fade2 Expander module image](doc/MixFade2.png)  ![Mix Pan Expander module image](doc/MixPan.png)  ![Mix Send Expander module image](doc/MixSend.png)| + +|[MULTI
MERGE](#multi-merge)|[MULTI
SPLIT](#multi-split)|[NON-OCTAVE REPEATING SCALE
INTERVALLIC QUANTIZER](#non-octave-repeating-scale-intervallic-quantizer)|[NORSIQ
CHORD
TO
SCALE](#norsiq-chord-to-scale)|[POLY
CLONE](#poly-clone)| +|----|----|----|----|----| +|![Multi Merge module image](doc/MultiMerge.png)|![Multi Split module image](doc/MultiSplit.png)|![Non-Octave Repeating Scale Intervallic Quantizer image](doc/NORS_IQ.png)|![NORSIQ Chord To Scale module image](doc/NORSIQChord2Scale.png)|![Poly Clone module image](doc/PolyClone.png)| -|[NON-OCTAVE REPEATING SCALE
INTERVALLIC QUANTIZER](#non-octave-repeating-scale-intervallic-quantizer)|[NORSIQ
CHORD
TO
SCALE](#norsiq-chord-to-scale)|[POLY
CLONE](#poly-clone)|[POLY
SAMPLE & HOLD
ANALOG SHIFT
REGISTER](#poly-sample--hold-analog-shift-register)|[POLY
UNISON](#poly-unison)|[PUSH 5](#push-5)| -|----|----|----|----|----|----| -|![Non-Octave Repeating Scale Intervallic Quantizer image](doc/NORS_IQ.png)|![NORSIQ Chord To Scale module image](doc/NORSIQChord2Scale.png)|![Poly Clone module image](doc/PolyClone.png)|![Poly Sample & Hold Analog Shift Register module image](doc/PolySHASR.png)|![Poly Unison module image](doc/PolyUnison.PNG)|![Push 5 module image](doc/Push5.png)| +|[POLY
OFFSET](#poly-offset)|[POLY
SAMPLE & HOLD
ANALOG SHIFT
REGISTER](#poly-sample--hold-analog-shift-register)|[POLY
SCALE](#poly-scale)|[POLY
UNISON](#poly-unison)|[PUSH 5](#push-5)|[RECURSE](#recurse)|[RECURSE
STEREO](#recurse-stereo)|[REFORMATION](#reformation)| +|----|----|----|----|----|----|----|----| +|![Poly Offset module image](doc/PolyOffset.png)|![Poly Sample & Hold Analog Shift Register module image](doc/PolySHASR.png)|![Poly Scale module image](doc/PolyScale.png)|![Poly Unison module image](doc/PolyUnison.PNG)|![Push 5 module image](doc/Push5.png)|![RECURSE module image](doc/Recurse.PNG)|![RECURSE STEREO module image](doc/RecurseStereo.PNG)|![Reformation module image](doc/Reformation.PNG)| -|[RECURSE](#recurse)|[RECURSE
STEREO](#recurse-stereo)|[REFORMATION](#reformation)|[RHYTHM EXPLORER](#rhythm-explorer)| +|[RHYTHM EXPLORER](#rhythm-explorer)|[SHAPED
VCA](#shaped-vca)|[VCA MIX 4](#vca-mix-4)|[VCA MIX 4 STEREO](#vca-mix-4-stereo)| |----|----|----|----| -|![RECURSE module image](doc/Recurse.PNG)|![RECURSE STEREO module image](doc/RecurseStereo.PNG)|![Reformation module image](doc/Reformation.PNG)|![Rhthm Explorer module image](doc/RhythmExplorer.PNG)| +|![Rhthm Explorer module image](doc/RhythmExplorer.PNG)|![SHAPED VCA module image](doc/ShapedVCA.png)|![VCA MIX 4 module image](doc/VCAMix4.png)|![VCA Mix 4 Stereo module image](doc/VCAMix4Stereo.png)| -|[SHAPED
VCA](#shaped-vca)|[VCA MIX 4](#vca-mix-4)|[VCA MIX 4 STEREO](#vca-mix-4-stereo)|[VENOM
BLANK](#venom-blank)|[WIDGET
MENU
EXTENDER](#widget-menu-extender)|[WINCOMP](#wincomp)| -|----|----|----|----|----|----| -|![SHAPED VCA module image](doc/ShapedVCA.png)|![VCA MIX 4 module image](doc/VCAMix4.png)|![VCA Mix 4 Stereo module image](doc/VCAMix4Stereo.png)|![VENOM BLANK module image](doc/VenomBlank.PNG)|![WIDGET MENU EXTENDER module imiage](doc/WidgetMenuExtender.png)|![WINCOMP module image](doc/WinComp.PNG)| +|[VENOM
BLANK](#venom-blank)|[WIDGET
MENU
EXTENDER](#widget-menu-extender)|[WINCOMP](#wincomp)| +|----|----|----| +|![VENOM BLANK module image](doc/VenomBlank.PNG)|![WIDGET MENU EXTENDER module imiage](doc/WidgetMenuExtender.png)|![WINCOMP module image](doc/WinComp.PNG)| ## Color Coded Ports All polyphonic ports use brass cores, while monophonic ports use steel cores. @@ -77,6 +82,15 @@ A custom default value overrides the factory default whenever a parameter is ini [Return to Table Of Contents](#venom) +## Venom Expander Modules +A number of Venom modules do not do anything on their own, but rather augment the functionality of another module when placed beside it. + +VCV Rack supports two different mechanisms for implementinig expander modules: +- Both the parent (base) module and the expander perform work, and they communicate with each other via messages that introduce sample delays, much as cables do in VCV Rack. +- The base module does all the work, accessing the expander inputs, outputs, and controls directly. This does not introduce any sample delays. + +All Venom expanders are implemented using the second method where the base module directly accesses the expander, so Venom expanders do not introduce sample delays. + ## Acknowledgments Special thanks to Andrew Hanson of [PathSet modules](https://library.vcvrack.com/?brand=Path%20Set) for setting up my GitHub repository, providing advice and ideas for the Rhythm Explorer and plugins in general, and for writing the initial prototype code for the Rhythm Explorer. @@ -90,6 +104,33 @@ Finally a thanks to Paul Dempsey for his MenuTextField struct from the pachde1 p [Return to Table Of Contents](#venom) +## AUXILLIARY CLONE EXPANDER +![Auxilliary Clone Expander module image](doc/AuxClone.png) +This expander module adds additional cloned poly input/output pairs to [Clone Merge](#clone-merge), [Poly Clone](#poly-clone), or [Poly Unison](#poly-unison). + +The expander must be placed immediately to the right of a Clone Merge, Poly Merge, or Poly Unison. The yellow LED in the upper left indicates whether the expander has successfully connected to a parent module. + +Each set of polyphonic input channels is cloned to match the clone count of the parent module, and sent to the output. The number of polyphonic channels at the input should either match the number of input channels at the parent, or else 1. If the input is unpatched it is treated as a mono input with a single chanel at constant 0 volts. + +The number of polyphonic channels at each output will always match the number of poly output channels at the parent. The LED to the right of each output indicates whether the output was able to properly clone all input channels. + +If the input poly count matches the parent, then each of the input channels is cloned as per the parent, and the LED is yellow. + +If the input poly count is 1, then the input is replicated to match the parent input channel count, and then each of those channels is cloned. The LED is yellow. + +If the input poly count is less than the parent and greater than 1, then the missing channels are treated as constant 0V, and the LED is orange. + +If the input poly count is greater than the parent, then excess channels are ignored, and the LED is red. + +All outputs will be constant 0V and all port LEDS will be black under any of the following conditions: +- The expander is not connected to a parent. +- The expander is bypassed. +- The parent module is bypassed. + +The names of each input/output pair are linked. Changing the name of one will automatically change the name of the other. + +[Return to Table Of Contents](#venom) + ## BENJOLIN OSCILLATOR ![Benjolin Oscillator module image](doc/BenjolinOsc.png) A complex chaotic oscillator emulating the oscillator and rungler components of a Benjolin. It produces 7 outputs: two pairs of triangle and pulse waves with exponential FM, two varying width pulse outputs, and a stepped voltage output similar to a random sample & hold. Frequency range is very wide, from slow LFO rates to high audio rates. Connect a resonant filter with excellent ping characteristics, and you have a complete functional Benjolin. @@ -318,6 +359,8 @@ All expander inputs as well as the probability CV attenuator are ignored when th ![Clone Merge module image](doc/CloneMerge.png) Clone Merge clones up to 8 monophonic inputs and merges the resultant channels into a single polyphonic output. It is especially useful with the Recurse modules when using polyphonic inputs. Clone Merge provides a convenient way to replicate CV inputs to match the recursion count. +Up to four auxilliary poly inputs may also be cloned via the [Auxilliary Clone Expander](#auxilliary-clone-expander). + ### CLONE knob Selects the number of times to clone or replicate each input. Possible values range from 1 to 16. @@ -580,7 +623,7 @@ All outputs are monophonic 0V if LOGIC is bypassed. ## MIX 4 ![Mix 4 module image](doc/Mix4.png) -A compact polyphonic mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. +A compact polyphonic mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. Module functionality can be extended by a set of [Mix Expanders](#mix-expanders). ### General Operation There are four numbered inputs, each of which can be attenuated, inverted, and/or amplified by a level knob. The level knobs can be configured to different scales. The modulated inputs are then summed to create a mix that can also be attenuated, inverted, and/or amplified by a mix level knob. Finally there are options to hard or soft clip the mix and/or remove DC offset, before sending the final mix to the Mix output. Oversampling is available for soft clipping to control any aliasing that might otherwise be introduced. @@ -638,9 +681,9 @@ The MIX output is monophonic 0V if MIX 4 is bypassed. ## MIX 4 STEREO ![Mix 4 Stereo module image](doc/Mix4Stereo.png) -A stereo compact polyphonic mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. +A stereo compact polyphonic mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. Module functionality can be extended by a set of [Mix Expanders](#mix-expanders). -Mix 4 Stereo is identical to Mix 4 except each of the inputs and outputs is doubled to support left and right channels so as to support stereo signals. A single input level knob controls each stereo input pair, and a single Mix level knob controls the stereo output pair. +Mix 4 Stereo is identical to [Mix 4](#mix-4) except each of the inputs and outputs is doubled to support left and right channels so as to support stereo signals. A single input level knob controls each stereo input pair, and a single Mix level knob controls the stereo output pair. Each right input is normaled to the corresponding left input. When in CV mode, each input level knob produces constant CV only if both the left and right input are unpatched. @@ -655,7 +698,7 @@ All other behaviors are the same as for Mix 4. ## MIX EXPANDERS ![Mix Offset Expander module image](doc/MixOffset.png)  ![Mix Mute Expander module image](doc/MixMute.png)  ![Mix Solo Expander module image](doc/MixSolo.png)  ![Mix Fade Expander module image](doc/MixFade.png)  ![Mix Fade2 Expander module image](doc/MixFade2.png)  ![Mix Pan Expander module image](doc/MixPan.png)  ![Mix Send Expander module image](doc/MixSend.png) -A collection of expander modules that extend the functionality of the four Mix modules: Mix 4, Mix 4 Stereo, VCA Mix 4, and VCA Mix 4 Stereo +A collection of expander modules that extend the functionality of the four Mix modules: [Mix 4](#mix-4), [Mix 4 Stereo](#mix-4-stereo), [VCA Mix 4](#vca-mix-4), and [VCA Mix 4 Stereo](#vca-mix-4-stereo). Mix expanders must be placed to the right of the main mix module. Multiple expanders can be used for one mix module as long as they form a contiguous chain to the right. Each expander has an LED in the upper left that glows yellow if successfully connected to a mix module. @@ -698,7 +741,7 @@ Controls how left and right channels are attenuated/amplified as a mono input is * **-4.5 dB center (compromise: side overpowered)** Same as +4.5 dB side except the center is attenuated rather than amplify the side. * **-6 dB center (linear: side overpowered)** - Same as +6 dB side except the center is attenuated rather than ammplify the side. + Same as +6 dB side except the center is attenuated rather than amplify the side. #### Stereo input pan law  (Pan expander) Controls how left and right channels are attenuated/amplified as a stereo input is panned. All of the mono options are available, plus the following @@ -806,6 +849,48 @@ Any number of Send modules can be used with a single mix module. [Return to Table Of Contents](#venom) +## MULTI MERGE +![Multi Merge module image](doc/MultiMerge.png) +Merge one or more sets of mono and/or poly inputs into polyphonic outputs. + +This merge utility is extremely flexible, with many configurations possible. It can merge a set of monophonic inputs into one polyphonic output. It can also merge a set of polyphonic inputs into one polyphonic ouput. Or it can merge a mixture of mono and poly inputs into one polyphonic output. Finally, there can be multiple sets of merges, each with its own poly output. + +The thick red lines indicate which input ports are merged and sent to which output port. The groupings are defined by which output ports are patched. Unpatched output ports are ignored. The module is automatically reconfigured each time you patch or unpatch an output port. Each patched output merges the inputs from that row and above until it reaches another row with a patched output. If there are no output ports, then the module assumes the last output port will be patched. + +The number of polyphonic output channels cannot exceed 16. If the sum of polyphony counts across the inputs exceeds 16, then excess channels are dropped, and the LEDs next to input ports with dropped channels glow red. + +### Standard Venom Context Menus +[Venom Themes](#themes), [Custom Names](#custom-names), and [Parameter Locks and Custom Defaults](#parameter-locks-and-custom-defaults) are available via standard Venom context menus. + +### Bypass + +All outputs are monophonic 0V if Multi Merge is bypassed. + +[Return to Table Of Contents](#venom) + +## MULTI SPLIT +![Multi Split module image](doc/MultiSplit.png) +Split one or more poly inputs into multiple mono or poly outputs. + +This split utility is extremely flexible, with many configurations possible. Each polyphonic input can be split into any combination of monophonic and polyphonic outputs. The module has default Automatic behavior for distributing the input channels to the outputs. But the default behavior can be overridden by defining a specific channel count for one or more output ports via output port context menus. The hover tooltip for each output port includes information on the current channel configuration. + +The thick red lines indicate which input port is split to which set of output ports. The groupings are defined by which input ports are patched. Unpatched input ports are ignored. The module is automatically reconfigured each time you patch or unpatch an input port. Each patched input distributes its channels to the same output row downward until it reaches another row with a patched input. If no input is patched, then the module treats it as if a monophonic 0V input is patched to the top input. + +Multi Split attempts to distribute the input channels evenly to the output ports. When the input channels cannot be divided evenly, higher (lower numbered) output ports take precedence over lower (higher numbered) ports. However, any output channel that is configured for a specific channel count will always output that number of channels. So if you subtract the sum of the fixed output counts from the input count, then the remainder are divided amongst the output ports that are configured for Auto assignment. If the distributor runs out of input channels, then constant 0V is used for the remainder of the output channels. + +If the input channels cannot fit within the output channels, then the LED next to the input port glows red. This can only happen if all output ports in the group are configured for a specific channel count, and the sum of the channel counts is less than the input channel count. + +This all probably sounds confusing. But once you start patching, it will probably start making sense quickly. Even if you cannot figure out the automatic distrubution algorithm, you can always take control by assigning a specific channel count to each output port. + +### Standard Venom Context Menus +[Venom Themes](#themes), [Custom Names](#custom-names), and [Parameter Locks and Custom Defaults](#parameter-locks-and-custom-defaults) are available via standard Venom context menus. + +### Bypass + +All outputs are monophonic 0V if Multi Split is bypassed. + +[Return to Table Of Contents](#venom) + ## NON-OCTAVE REPEATING SCALE INTERVALLIC QUANTIZER ![Non-Octave Repeating Scale Intervallic Quantizer image](doc/NORS_IQ.png) Quantizer for any scale with up to 13 intervals between notes. The scale is defined by a root note for the scale, followed by a series of intervals. The first interval is added to the root to get the 2nd note in the scale. The second interval is added to the 2nd note to define the 3rd note, etc. The final interval defines the step from the Nth note of the scale to the root of the next pseudo-octave in the series. The total interval from one root to the next need not be an octave. @@ -1014,6 +1099,8 @@ All outputs are constant monophonic 0V if NORSIQ Chord To Scale is bypassed. ![Poly Clone module image](doc/PolyClone.png) Poly Clone replicates each channel from a polyphonic input and merges the result into a single polyphonic output. It is especially useful with the Recurse modules when using polyphonic inputs. Poly Clone provides a convenient way to replicate channels in polyphonnic CV inputs to match the recursion count. +Up to four auxilliary poly inputs may also be cloned via the [Auxilliary Clone Expander](#auxilliary-clone-expander). + ### CLONE knob Selects the number of times to clone or replicate each input channel. Possible values range from 1 to 16. @@ -1032,7 +1119,43 @@ All of the replicated channels are merged into the single polyphonic output. The ### Bypass -If Clone Merge is bypassed then the input is passed unchanged to the output. +If Clone Merge is bypassed then the output is constant monophonic 0 volts. + +[Return to Table Of Contents](#venom) + +## POLY OFFSET +![Poly Offset module image](doc/PolyOffset.png) +Provides an offset control for each channel of a polyphonic signal. For each polyphonic output channel, the channel's knob voltage is added to the input voltage to get the final output voltage. + +### Offset knobs +The default range for all offset knobs is bipolar +/- 10V. + +An "Offset range" option in the module context menu lets you specify a different range that is used for all the knobs +- 0-1 V +- 0-2 V +- 0-5 V +- 0-10 V +- +/- 1 V +- +/- 2 V +- +/- 5 V +- +/- 10 V (default) + +The default (initialize) value for all knobs always starts out at 0 volts, regardless of range. Of course the default can be overriden by the standard Venom parameter context menu option. + +### Output polyphonic channel count +By default the number of output channels matches the number of input channels. Knobs for channels above the output count are ignored. + +There is a "Polyphony channels" option in the module context menu that lets you override the default and select a specific output channel count. Input channels and knobs above the specified channel count are ignored. If the selected count is greater than the input channel count, then missing channel inputs are assumed to be constant 0 volts, meaning the knob alone specifies the output voltage. + +### Channel count display +The number of polyphonic channels at the output is displayed in the LED panel. The display will be yellow if the number of output channels is greater than or equal to the input channel count. The display will be red if the selected channel count is less than the input channel count. + +### Standard Venom Context Menus +[Venom Themes](#themes), [Custom Names](#custom-names), and [Parameter Locks and Custom Defaults](#parameter-locks-and-custom-defaults) are available via standard Venom context menus. + +### Bypass + +If Poly Offset is bypassed then the input is passed unchanged to the output. [Return to Table Of Contents](#venom) @@ -1042,7 +1165,7 @@ Ten row polyphonic sample and hold combined with a shift register, with oversamp Each row has its own polyphonic Trigger and Data inputs, and a polyphonic Hold output. In total that is 10 independent polyphonic sample and hold circuits. However, the inputs are normaled in a way that enables consecutive rows to function as a shift register. -If no input is provided, then random values are sampled from an internal random number generator. +If no input is provided, then values are sampled from an internal random number generator. ### TRIG (Trigger) button Manually triggers the first row only @@ -1066,7 +1189,7 @@ This color coded button controls the output range of the internal random number - **+/- 10 V (purple)** ### CLR (Clear) button -Resets all all polyphonic channels of all 10 Hold outputs to 0 V. +Resets all polyphonic channels of all 10 Hold outputs to 0 V. ### Sample & Hold row @@ -1119,11 +1242,45 @@ If Poly S&H ASR is bypassed then all outputs are monophonic constant 0 V. [Return to Table Of Contents](#venom) +## POLY SCALE +![Poly Scale module image](doc/PolyScale.png) +Provides a level control for each channel of a polyphonic signal. For each polyphonic output channel, the channel's input voltage is scaled (attenuated and/or iniverted and/or amplified) based on the Level knob for that channel, and then sent to the output. + +### Level knobs +The Level knobs set the scale factor for each polyphonic input channel. The default range for all Level knobs is unipolar 0-1x. + +A "Level range" option in the module context menu lets you specify a different range that is used for all the knobs. +- 0-1x (default) +- 0-2x +- 0-5x +- 0-10x +- +/- 1x +- +/- 2x +- +/- 5x +- +/- 10x + +The default (initialize) level for each knob always starts out at 1x, regardless of the range. Of course the default can be overriden by the standard Venom parameter context menu option. + +### Output polyphonic channel count +The output channel count always matches the input channel count, and is displayed in the LED display panel. + +If there is no input, then the output is effectively constant monophonic 0 volts. + +### Standard Venom Context Menus +[Venom Themes](#themes), [Custom Names](#custom-names), and [Parameter Locks and Custom Defaults](#parameter-locks-and-custom-defaults) are available via standard Venom context menus. + +### Bypass + +If Poly Scale is bypassed then the input is passed unchanged to the output. + +[Return to Table Of Contents](#venom) ## POLY UNISON ![Poly Unison module image](doc/PolyUnison.PNG) Replicate each channel of a polyphonic input with a variable detune spread, and merge the results into a single polyphonic output. +Up to four auxilliary poly inputs may also be cloned via the [Auxilliary Clone Expander](#auxilliary-clone-expander). Note that auxilliary outputs on the expander are not detuned. + ### COUNT (Unison Count) knob Sets the number of unison channels for each input channel, from 1 to 16. @@ -1739,7 +1896,7 @@ The Left and Right inputs are passed unchanged to the Left and Right outputs whe ## VCA MIX 4 ![VCA Mix 4 module image](doc/VCAMix4.png) -A compact polyphonic VCA, mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. +A compact polyphonic VCA, mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. The module includes options for bipolar VCA (ring mod), hard or soft clipping, and DC offset removal. Module functionality can be extended by a set of [Mix Expanders](#mix-expanders). ### General Operation There are four numbered inputs, each of which can be attenuated, inverted, and/or amplified by a level knob and CV input. Each modulated input can then be output to a dedicated numbered channel outupt and/or the modulated inputs can be summed to create a mix. There is also a 5th chain input, without modulation, that can be added to the mix. The mix can also be attenuated, inverted, and/or amplified by a mix level knob and CV input. Finally there are options to hard or soft clip the mix and/or remove DC offset, before sending the final mix to the Mix output. Oversampling is available for soft clipping to control any aliasing that might be introduced. The VCAs can be configured to have a linear or exponential response, and they can be unipolar or bipolar. Audio rate CV is supported so the VCA MIX 4 can do amplitude or ring modulation. @@ -1829,9 +1986,9 @@ The numbered channel inputs are passed unchanged to their corresponding outputs ## VCA MIX 4 STEREO ![VCA Mix 4 module image](doc/VCAMix4Stereo.png) -A stereo compact polyphonic VCA, mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. The module includes options for bipolar VCA (ring mod), hard or soft clipping, and DC offset removal. +A stereo compact polyphonic VCA, mixer, attenuator, inverter, amplifier, and/or offset suitable for both audio and CV. The module includes options for bipolar VCA (ring mod), hard or soft clipping, and DC offset removal. Module functionality can be extended by a set of [Mix Expanders](#mix-expanders). -VCA Mix 4 Stereo is a stereo version of the VCA MIX 4, sharing the same features, but with the following differences: +VCA Mix 4 Stereo is a stereo version of the [VCA MIX 4](#vca-mix-4), sharing the same features, but with the following differences: - Each of the channel inputs and outputs, as well as the Chain input and Mix output are doubled to support left and right channels. Each stereo pair is controlled by its own single Level knob and CV input. - Each right input is normaled to the corresponding left input. When using the bipolar Level mode, each input level knob produces constant CV only if both the left and right inputs are unpatched. - The output channel count for each numbered channel is the maximum polyphony found across the corresponding left, right, and CV inputs. @@ -1871,7 +2028,7 @@ When enabled, the context menu for every foreign input port, output port, and pa If a parameter or port has been given a custom name, then an additional menu option is added to restore the factory name. -If a module dynamically updates the parameter or port name, then that overrides any customm name from Widget Menu Extender. +If a module dynamically updates the parameter or port name, then that overrides any custom name from Widget Menu Extender. Do not include "input" or "output" in your custom port name - VCV will automatically append input or output to the name you provide. diff --git a/changelog.md b/changelog.md index 7c99bab..d60fc92 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,19 @@ # Venom Modules Changelog +## 2.7.0 (2024-03-11) +### New Modules +- Auxilliary Clone Expander +- Multi Merge +- Multi Split +- Poly Offset +- Poly Scale + +### Enhancements +- Modified Clone Merge, Poly Clone, and Poly Unison to work with the Auxilliary Clone Expander + +### Bug Fixes +- Fixed a bug with Venom expander modules. Prior to version 2.7, Venom expander modules could misbehave and possibly crash VCV Rack if an expander or base module was deleted. + ## 2.6.1 (2024-02-28) ### Enhancements - Harmonic Quantizer: Add detune parameters diff --git a/doc/AuxClone.png b/doc/AuxClone.png new file mode 100644 index 0000000..d4cce94 Binary files /dev/null and b/doc/AuxClone.png differ diff --git a/doc/MultiMerge.png b/doc/MultiMerge.png new file mode 100644 index 0000000..f9ed3a0 Binary files /dev/null and b/doc/MultiMerge.png differ diff --git a/doc/MultiSplit.png b/doc/MultiSplit.png new file mode 100644 index 0000000..116ec36 Binary files /dev/null and b/doc/MultiSplit.png differ diff --git a/doc/PolyOffset.png b/doc/PolyOffset.png new file mode 100644 index 0000000..86dfb31 Binary files /dev/null and b/doc/PolyOffset.png differ diff --git a/doc/PolyScale.png b/doc/PolyScale.png new file mode 100644 index 0000000..2ca376d Binary files /dev/null and b/doc/PolyScale.png differ diff --git a/plugin.json b/plugin.json index c4f60c6..aa8a6c0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "slug": "Venom", "name": "Venom", - "version": "2.6.1", + "version": "2.7.0", "license": "GPL-3.0-or-later", "brand": "", "author": "David Benham", @@ -13,6 +13,17 @@ "donateUrl": "", "changelogUrl": "https://github.com/DaveBenham/VenomModules/blob/main/changelog.md", "modules": [ + { + "slug": "AuxClone", + "name": "Auxilliary Clone Expander", + "description": "Adds additional cloned poly input/output pairs to Clone Merge, Poly Merge, or Poly Unison", + "manualUrl": "https://github.com/DaveBenham/VenomModules/blob/main/README.md#auxilliary-clone-expander", + "tags": [ + "Expander", + "Polyphonic", + "Utility" + ] + }, { "slug": "BenjolinOsc", "name": "Benjolin Oscillator", @@ -228,6 +239,26 @@ "Utility" ] }, + { + "slug": "MultiMerge", + "name": "Multi Merge", + "description": "Merge one or more sets of mono and/or poly inputs into polyphonic outputs", + "manualUrl": "https://github.com/DaveBenham/VenomModules/blob/main/README.md#multi-merge", + "tags": [ + "Polyphonic", + "Utility" + ] + }, + { + "slug": "MultiSplit", + "name": "Multi Split", + "description": "Split one or more poly inputs into multiple mono or poly outputs", + "manualUrl": "https://github.com/DaveBenham/VenomModules/blob/main/README.md#multi-merge", + "tags": [ + "Polyphonic", + "Utility" + ] + }, { "slug": "NORS_IQ", "name": "Non-Octave-Repeating Scale Intervallic Quantizer", @@ -258,6 +289,16 @@ "Utility" ] }, + { + "slug": "PolyOffset", + "name": "Poly Offset", + "description": "Provides an offset control for each channel of a polyphonic signal", + "manualUrl": "https://github.com/DaveBenham/VenomModules/blob/main/README.md#poly-offset", + "tags": [ + "Polyphonic", + "Utility" + ] + }, { "slug": "PolySHASR", "name": "Poly Sample & Hold Analog Shift Register", @@ -269,6 +310,17 @@ "Sample and hold" ] }, + { + "slug": "PolyScale", + "name": "Poly Scale", + "description": "Provides a level control for each channel of a polyphonic signal", + "manualUrl": "https://github.com/DaveBenham/VenomModules/blob/main/README.md#poly-offset", + "tags": [ + "Attenuator", + "Polyphonic", + "Utility" + ] + }, { "slug": "PolyUnison", "name": "Poly Unison", diff --git a/res/Coal/AuxClone_Coal.svg b/res/Coal/AuxClone_Coal.svg new file mode 100644 index 0000000..c1c43d1 --- /dev/null +++ b/res/Coal/AuxClone_Coal.svg @@ -0,0 +1,380 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Coal/MultiMerge_Coal.svg b/res/Coal/MultiMerge_Coal.svg new file mode 100644 index 0000000..b17cf45 --- /dev/null +++ b/res/Coal/MultiMerge_Coal.svg @@ -0,0 +1,488 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Coal/MultiSplit_Coal.svg b/res/Coal/MultiSplit_Coal.svg new file mode 100644 index 0000000..83ec551 --- /dev/null +++ b/res/Coal/MultiSplit_Coal.svg @@ -0,0 +1,485 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Coal/PolyOffset_Coal.svg b/res/Coal/PolyOffset_Coal.svg new file mode 100644 index 0000000..e254800 --- /dev/null +++ b/res/Coal/PolyOffset_Coal.svg @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Coal/PolyScale_Coal.svg b/res/Coal/PolyScale_Coal.svg new file mode 100644 index 0000000..dbd6391 --- /dev/null +++ b/res/Coal/PolyScale_Coal.svg @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Danger/AuxClone_Danger.svg b/res/Danger/AuxClone_Danger.svg new file mode 100644 index 0000000..5c2348e --- /dev/null +++ b/res/Danger/AuxClone_Danger.svg @@ -0,0 +1,380 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Danger/MultiMerge_Danger.svg b/res/Danger/MultiMerge_Danger.svg new file mode 100644 index 0000000..08636ba --- /dev/null +++ b/res/Danger/MultiMerge_Danger.svg @@ -0,0 +1,488 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Danger/MultiSplit_Danger.svg b/res/Danger/MultiSplit_Danger.svg new file mode 100644 index 0000000..f00a27e --- /dev/null +++ b/res/Danger/MultiSplit_Danger.svg @@ -0,0 +1,485 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Danger/PolyOffset_Danger.svg b/res/Danger/PolyOffset_Danger.svg new file mode 100644 index 0000000..7a6b54d --- /dev/null +++ b/res/Danger/PolyOffset_Danger.svg @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Danger/PolyScale_Danger.svg b/res/Danger/PolyScale_Danger.svg new file mode 100644 index 0000000..48eee5c --- /dev/null +++ b/res/Danger/PolyScale_Danger.svg @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Earth/AuxClone_Earth.svg b/res/Earth/AuxClone_Earth.svg new file mode 100644 index 0000000..292b26f --- /dev/null +++ b/res/Earth/AuxClone_Earth.svg @@ -0,0 +1,380 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Earth/MultiMerge_Earth.svg b/res/Earth/MultiMerge_Earth.svg new file mode 100644 index 0000000..627ed58 --- /dev/null +++ b/res/Earth/MultiMerge_Earth.svg @@ -0,0 +1,488 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Earth/MultiSplit_Earth.svg b/res/Earth/MultiSplit_Earth.svg new file mode 100644 index 0000000..d3c348d --- /dev/null +++ b/res/Earth/MultiSplit_Earth.svg @@ -0,0 +1,485 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Earth/PolyOffset_Earth.svg b/res/Earth/PolyOffset_Earth.svg new file mode 100644 index 0000000..567c458 --- /dev/null +++ b/res/Earth/PolyOffset_Earth.svg @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Earth/PolyScale_Earth.svg b/res/Earth/PolyScale_Earth.svg new file mode 100644 index 0000000..d617607 --- /dev/null +++ b/res/Earth/PolyScale_Earth.svg @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Ivory/AuxClone_Ivory.svg b/res/Ivory/AuxClone_Ivory.svg new file mode 100644 index 0000000..daf72bc --- /dev/null +++ b/res/Ivory/AuxClone_Ivory.svg @@ -0,0 +1,367 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Ivory/MultiMerge_Ivory.svg b/res/Ivory/MultiMerge_Ivory.svg new file mode 100644 index 0000000..27575a8 --- /dev/null +++ b/res/Ivory/MultiMerge_Ivory.svg @@ -0,0 +1,467 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Ivory/MultiSplit2_Ivory.svg b/res/Ivory/MultiSplit2_Ivory.svg new file mode 100644 index 0000000..8f4ac90 --- /dev/null +++ b/res/Ivory/MultiSplit2_Ivory.svg @@ -0,0 +1,446 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INPUT + + + OUTPUT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Ivory/MultiSplit_Ivory.svg b/res/Ivory/MultiSplit_Ivory.svg new file mode 100644 index 0000000..8de89f4 --- /dev/null +++ b/res/Ivory/MultiSplit_Ivory.svg @@ -0,0 +1,464 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Ivory/PolyOffset_Ivory.svg b/res/Ivory/PolyOffset_Ivory.svg new file mode 100644 index 0000000..4dbcf5c --- /dev/null +++ b/res/Ivory/PolyOffset_Ivory.svg @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Ivory/PolyScale_Ivory.svg b/res/Ivory/PolyScale_Ivory.svg new file mode 100644 index 0000000..6bf5746 --- /dev/null +++ b/res/Ivory/PolyScale_Ivory.svg @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AuxClone.cpp b/src/AuxClone.cpp new file mode 100644 index 0000000..d278f3f --- /dev/null +++ b/src/AuxClone.cpp @@ -0,0 +1,63 @@ +// Venom Modules (c) 2023, 2024 Dave Benham +// Licensed under GNU GPLv3 + +#include "plugin.hpp" +#include "CloneModule.hpp" + +#define LIGHT_OFF 0.02f +#define LIGHT_ON 1.f + +struct AuxClone : CloneModule { + + AuxClone() { + venomConfig(EXP_PARAMS_LEN, EXP_INPUTS_LEN, EXP_OUTPUTS_LEN, EXP_LIGHTS_LEN); + for (int i=0; idescription = "yellow: OK, orange: Missing channels, red: Excess channels dropped"; + outputExtensions[EXP_POLY_OUTPUT+i].portNameLink = EXP_POLY_INPUT+i; + inputExtensions[EXP_POLY_INPUT+i].portNameLink = EXP_POLY_OUTPUT+i; + } + configLight(EXP_CONNECT_LIGHT, "Left connection indicator"); + } + + void process(const ProcessArgs& args) override { + VenomModule::process(args); + } + + void onExpanderChange(const ExpanderChangeEvent& e) override { + Module* mod = getLeftExpander().module; + bool connected = mod && (mod->model==modelCloneMerge || mod->model==modelPolyUnison || mod->model==modelPolyClone); + lights[EXP_CONNECT_LIGHT].setBrightness( connected ); + if (!connected) { + for (int i=0; i(Vec(22.5f,61.5f+delta), module, AuxClone::EXP_POLY_INPUT+i)); + addOutput(createOutputCentered(Vec(22.5f,226.5f+delta), module, AuxClone::EXP_POLY_OUTPUT+i)); + addChild(createLightCentered>>(Vec(36.f, 214.f+delta), module, AuxClone::EXP_POLY_LIGHT+i*2)); + delta+=35.f; + } + addChild(createLightCentered>(Vec(6.f, 10.f), module, AuxClone::EXP_CONNECT_LIGHT)); + } + +}; + +Model* modelAuxClone = createModel("AuxClone"); diff --git a/src/BernoulliSwitch.cpp b/src/BernoulliSwitch.cpp index 8838cd3..0055e67 100644 --- a/src/BernoulliSwitch.cpp +++ b/src/BernoulliSwitch.cpp @@ -108,7 +108,7 @@ struct BernoulliSwitch : VenomModule { lights[NO_SWAP_LIGHT].setBrightness(true); lights[SWAP_LIGHT].setBrightness(false); } - + void process(const ProcessArgs& args) override { VenomModule::process(args); using float_4 = simd::float_4; diff --git a/src/CloneMerge.cpp b/src/CloneMerge.cpp index 761fc46..9b803b0 100644 --- a/src/CloneMerge.cpp +++ b/src/CloneMerge.cpp @@ -2,8 +2,10 @@ // Licensed under GNU GPLv3 #include "plugin.hpp" +#include "CloneModule.hpp" + +struct CloneMerge : CloneModuleBase { -struct CloneMerge : VenomModule { enum ParamId { CLONE_PARAM, PARAMS_LEN @@ -57,12 +59,14 @@ struct CloneMerge : VenomModule { outputs[POLY_OUTPUT].setVoltage(val, channel++); } outputs[POLY_OUTPUT].setChannels(channel); + processExpander(clones, goodIns); if (lightDivider.process()) { for (int i=0; i<8; i++) { lights[MONO_LIGHTS+i*2].setBrightness(i=goodIns && iisBypassed() + && expander->model == modelAuxClone + ? expander + : NULL; + if (!expander) return; + for (int i=0; ioutputs[EXP_POLY_OUTPUT+i].isConnected()) + expanderChannels[i] = std::max(expander->inputs[EXP_POLY_INPUT+i].getChannels(),1); + } + for (int c=0; cinputs[EXP_POLY_INPUT+e].getPolyVoltage(c); + for (int j=0, ec=c*clones; joutputs[EXP_POLY_OUTPUT+e].setVoltage(val, ec); + } + } + int outCnt = clones * goodCh; + for (int e=0; eoutputs[EXP_POLY_OUTPUT+e].setChannels( expanderChannels[e] ? outCnt : 0 ); + } + } + + void setExpanderLights(int goodCh) { + Module* expander = getRightExpander().module; + expander = expander + && !expander->isBypassed() + && expander->model == modelAuxClone + ? expander + : NULL; + if (!expander) return; + for (int e=0; eoutputs[EXP_POLY_OUTPUT+e].isConnected() ? std::max(expander->inputs[EXP_POLY_INPUT+e].getChannels(),1) : 0; + expander->lights[EXP_POLY_LIGHT+e*2].setBrightness( + channels>goodCh ? 0.f : (channels==goodCh || channels==1 ? 1.f : (channels ? 0.2f : 0.f)) + ); + expander->lights[EXP_POLY_LIGHT+e*2+1].setBrightness( + channels>goodCh ? 1.f : (channels==goodCh || channels==1 ? 0.f : (channels ? 1.0f : 0.f)) + ); + } + } + + void onBypass(const BypassEvent& e) override { + Module* expander = getRightExpander().module; + expander = expander + && expander->model == modelAuxClone + ? expander + : NULL; + if (expander){ + for (int i=0; ioutputs[EXP_POLY_OUTPUT+i].setVoltage(0.f); + expander->outputs[EXP_POLY_OUTPUT+i].setChannels(0); + expander->lights[EXP_POLY_LIGHT+i*2].setBrightness(0.f); + expander->lights[EXP_POLY_LIGHT+i*2+1].setBrightness(0.f); + } + } + } + +}; diff --git a/src/LinearBeats.cpp b/src/LinearBeats.cpp index 2ab21c5..57c398f 100644 --- a/src/LinearBeats.cpp +++ b/src/LinearBeats.cpp @@ -62,7 +62,7 @@ struct LinearBeats : VenomModule { void process(const ProcessArgs& args) override { VenomModule::process(args); - + bool preState = false; bool trig = (!inputs[CLOCK_INPUT].isConnected()) || clockTrigger.process(inputs[CLOCK_INPUT].getVoltage(), 0.1f, 1.f); Module* finalInMute = inMute && !inMute->isBypassed() ? inMute : NULL; diff --git a/src/LinearBeatsExpander.cpp b/src/LinearBeatsExpander.cpp index e7f9a1c..08a8755 100644 --- a/src/LinearBeatsExpander.cpp +++ b/src/LinearBeatsExpander.cpp @@ -32,9 +32,9 @@ struct LinearBeatsExpander : VenomModule { paramQuantities[MUTE_PARAM+i]->name = label[i]+str; inputInfos[MUTE_INPUT+i]->name = label[i]+str+" CV"; } - } + } - void onExpanderChange(const ExpanderChangeEvent& e) override { + void setConnectionLight(){ Module* mod = getRightExpander().module; if (mod && mod->model == modelLinearBeats) { lights[RIGHT_LIGHT].setBrightness(1.f); @@ -56,6 +56,10 @@ struct LinearBeatsExpander : VenomModule { left = false; } } + } + + void onExpanderChange(const ExpanderChangeEvent& e) override { + setConnectionLight(); } }; diff --git a/src/MixModule.hpp b/src/MixModule.hpp index 09db0a5..9072dca 100644 --- a/src/MixModule.hpp +++ b/src/MixModule.hpp @@ -393,11 +393,12 @@ struct MixExpanderWidget : VenomWidget { if(this->module) { mixMod = dynamic_cast(this->module); mixMod->lights[MixModule::EXP_LIGHT].setBrightness(connected); - if (!connected) + if (!connected){ for (int i=0; igetNumOutputs(); i++) { mixMod->outputs[i].setVoltage(0.f); mixMod->outputs[i].setChannels(1); } + } } VenomWidget::step(); diff --git a/src/MultiMerge.cpp b/src/MultiMerge.cpp new file mode 100644 index 0000000..647e3d6 --- /dev/null +++ b/src/MultiMerge.cpp @@ -0,0 +1,122 @@ +// Venom Modules (c) 2023, 2024 Dave Benham +// Licensed under GNU GPLv3 + +#include "plugin.hpp" +#define xInEven 14.5f +#define xInOdd 30.5f +#define xOutEven 59.5f +#define xOutOdd 75.5f +#define yOrigin 43.5f +#define yDelta 20.f + +struct MultiMerge : VenomModule { + enum ParamId { + PARAMS_LEN + }; + enum InputId { + ENUMS(POLY_INPUT,16), + INPUTS_LEN + }; + enum OutputId { + ENUMS(POLY_OUTPUT,16), + OUTPUTS_LEN + }; + enum LightId { + ENUMS(DROP_LIGHT,16), + LIGHTS_LEN + }; + + MultiMerge() { + venomConfig(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + for (int i=0; i<16; i++) { + std::string name = "Poly " + std::to_string(i+1); + configInput(POLY_INPUT+i, name); + configLight(DROP_LIGHT+i, name+" dropped channel(s) indicator"); + configOutput(POLY_OUTPUT+i, name); + } + } + + void process(const ProcessArgs& args) override { + VenomModule::process(args); + int out[16]{}; + int outNdx = -1; + int current = out[0]; + for (int i=15, o=15; i>=0; i--){ + out[i] = outputs[POLY_OUTPUT+i].isConnected() ? (o=i) : o; + } + for (int i=0; i<16; i++) { + if (current != out[i]){ + outputs[current].setChannels(std::min(outNdx+1,16)); + current = out[i]; + outNdx = -1; + } + int inCnt=std::max(inputs[POLY_INPUT+i].getChannels(),1); + for (int c=0; c=16); + } + outputs[current].setChannels(std::min(outNdx+1,16)); + } + +}; + +struct MultiMergeWidget : VenomWidget { + + struct Linework : widget::Widget { + MultiMerge* mod=NULL; + + void drawLine(const DrawArgs& args, float x0, float y0, float x1, float y1) { + nvgBeginPath(args.vg); + nvgMoveTo(args.vg, x0, y0); + nvgLineTo(args.vg, x1, y1); + nvgStroke(args.vg); + } + + void draw(const DrawArgs& args) override { + int theme = mod ? mod->currentTheme : 0; + if (!theme) theme = settings::preferDarkPanels ? getDefaultDarkTheme()+1 : getDefaultTheme()+1; + nvgStrokeColor(args.vg, theme==4 ? nvgRGB(0xf2, 0xf2, 0xf2) : nvgRGB(0xff, 0x00, 0x00)); + nvgFillColor(args.vg, nvgRGB(0x25, 0x25, 0x25)); + bool chain=false; + for (int i=0; i<16; i++){ + if (chain){ + nvgStrokeWidth(args.vg, 20.f); + drawLine(args, i%2 ? xInEven : xInOdd, yOrigin+yDelta*(i-1), i%2 ? xInOdd : xInEven, yOrigin+yDelta*i); + } + if ((mod && mod->outputs[MultiMerge::POLY_OUTPUT+i].isConnected()) || (i==15)){ + nvgStrokeWidth(args.vg, 7.f); + drawLine(args, i%2 ? xInOdd : xInEven, yOrigin+yDelta*i, i%2 ? xOutOdd : xOutEven, yOrigin+yDelta*i); + chain = false; + } else { + chain = true; + } + nvgBeginPath(args.vg); + nvgCircle(args.vg, i%2 ? xInOdd-18.5f : xInEven+18.5f, yOrigin+yDelta*i, 3.5f); + nvgFill(args.vg); + } + } + }; + + MultiMergeWidget(MultiMerge* module) { + setModule(module); + setVenomPanel("MultiMerge"); + + Linework* linework = createWidget(Vec(0.f,0.f)); + linework->mod = module; + linework->box.size = box.size; + addChild(linework); + + for (int i=0; i<16; i++){ + addInput(createInputCentered(Vec(i%2 ? xInOdd : xInEven, yOrigin+yDelta*i), module, MultiMerge::POLY_INPUT+i)); + addChild(createLightCentered>(Vec(i%2 ? xInOdd-18.5f : xInEven+18.5f, yOrigin+yDelta*i), module, MultiMerge::DROP_LIGHT+i)); + addOutput(createOutputCentered(Vec(i%2 ? xOutOdd : xOutEven, yOrigin+yDelta*i), module, MultiMerge::POLY_OUTPUT+i)); + } + } + +}; + +Model* modelMultiMerge = createModel("MultiMerge"); diff --git a/src/MultiSplit.cpp b/src/MultiSplit.cpp new file mode 100644 index 0000000..005276e --- /dev/null +++ b/src/MultiSplit.cpp @@ -0,0 +1,224 @@ +// Venom Modules (c) 2023, 2024 Dave Benham +// Licensed under GNU GPLv3 + +#include "plugin.hpp" +#define xInEven 14.5f +#define xInOdd 30.5f +#define xOutEven 59.5f +#define xOutOdd 75.5f +#define yOrigin 43.5f +#define yDelta 20.f + +struct MultiSplit : VenomModule { + enum ParamId { + PARAMS_LEN + }; + enum InputId { + ENUMS(POLY_INPUT,16), + INPUTS_LEN + }; + enum OutputId { + ENUMS(POLY_OUTPUT,16), + OUTPUTS_LEN + }; + enum LightId { + ENUMS(DROP_LIGHT,16), + LIGHTS_LEN + }; + + int outChannels[16]{}; // Assumes 1st DROP_LIGHT is 0 + std::string outLabels[17]{"Auto","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"}; + + void setOutputDescription(int id){ + outputInfos[id]->description = "Channels: "+outLabels[outChannels[id]]; + } + + MultiSplit() { + venomConfig(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + for (int i=0; i<16; i++) { + std::string name = "Poly " + std::to_string(i+1); + configInput(POLY_INPUT+i, name); + configLight(DROP_LIGHT+i, name+" dropped channel(s) indicator"); + configOutput(POLY_OUTPUT+i, name); + setOutputDescription(i); + } + } + + void process(const ProcessArgs& args) override { + VenomModule::process(args); + int currentIn = 0; + int nextIn = 0; + bool overflow[16]{}; + while (nextIn < 16){ + currentIn = nextIn; + nextIn = 16; + int inCnt = std::max(inputs[POLY_INPUT+currentIn].getChannels(), 1); + int fixedCnt = 0; + int freeCnt = 0; + for (int i=currentIn; i<16; i++) { + if (outChannels[i]) + fixedCnt += outChannels[i]; + else + freeCnt++; + if (i==15 || inputs[POLY_INPUT+i+1].isConnected()){ + nextIn = i+1; + break; + } + } + int baseCnt = 0; + int remainCnt = 0; + if (freeCnt && inCnt>=fixedCnt){ + baseCnt = (inCnt-fixedCnt) / freeCnt; + remainCnt = (inCnt-fixedCnt) % freeCnt; + } + int cIn=0; + for (int i=currentIn; icurrentTheme : 0; + if (!theme) theme = settings::preferDarkPanels ? getDefaultDarkTheme()+1 : getDefaultTheme()+1; + nvgStrokeColor(args.vg, theme==4 ? nvgRGBA(0xf2, 0xf2, 0xf2, 0xff) : nvgRGBA(0xff, 0x00, 0x00, 0xff)); + nvgFillColor(args.vg, nvgRGB(0x25, 0x25, 0x25)); + for (int i=0; i<16; i++){ + if ((mod && mod->inputs[MultiSplit::POLY_OUTPUT+i].isConnected()) || (i==0)){ + nvgStrokeWidth(args.vg, 7.f); + drawLine(args, i%2 ? xInOdd : xInEven, yOrigin+yDelta*i, i%2 ? xOutOdd : xOutEven, yOrigin+yDelta*i); + } else { + nvgStrokeWidth(args.vg, 20.f); + drawLine(args, i%2 ? xOutEven : xOutOdd, yOrigin+yDelta*(i-1), i%2 ? xOutOdd : xOutEven, yOrigin+yDelta*i); + } + nvgBeginPath(args.vg); + nvgCircle(args.vg, i%2 ? xInOdd-18.5f : xInEven+18.5f, yOrigin+yDelta*i, 3.5f); + nvgFill(args.vg); + } + } + }; + + struct OutPort : PolyPort { + int portId; + void appendContextMenu(Menu* menu) override { + MultiSplit* module = static_cast(this->module); + menu->addChild(new MenuSeparator); + menu->addChild(createIndexSubmenuItem( + "Channels", + {"Auto","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"}, + [=]() { + return module->outChannels[portId]; + }, + [=](int cnt) { + module->outChannels[portId] = cnt; + module->setOutputDescription(portId); + } + )); + PolyPort::appendContextMenu(menu); + } + }; + + template + TWidget* createConfigChannelsOutputCentered(math::Vec pos, engine::Module* module, int outputId) { + TWidget* o = createOutputCentered(pos, module, outputId); + o->portId = outputId; + return o; + } + + MultiSplitWidget(MultiSplit* module) { + setModule(module); + setVenomPanel("MultiSplit"); + + Linework* linework = createWidget(Vec(0.f,0.f)); + linework->mod = module; + linework->box.size = box.size; + addChild(linework); + + for (int i=0; i<16; i++){ + addInput(createInputCentered(Vec(i%2 ? xInOdd : xInEven, yOrigin+yDelta*i), module, MultiSplit::POLY_INPUT+i)); + addChild(createLightCentered>(Vec(i%2 ? xInOdd-18.5f : xInEven+18.5f, yOrigin+yDelta*i), module, MultiSplit::DROP_LIGHT+i)); + addOutput(createConfigChannelsOutputCentered(Vec(i%2 ? xOutOdd : xOutEven, yOrigin+yDelta*i), module, MultiSplit::POLY_OUTPUT+i)); + } + } + + void appendContextMenu(Menu* menu) override { + MultiSplit* module = static_cast(this->module); + menu->addChild(new MenuSeparator); + menu->addChild(createMenuLabel("Configure all output ports:")); + menu->addChild(createIndexSubmenuItem( + "Channels", + {"Auto","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"}, + [=]() { + int current = module->outChannels[0]; + for (int i=1; i<16; i++){ + if (module->outChannels[i] != current) + current = 17; + } + return current; + }, + [=](int cnt) { + if (cnt<16){ + for (int i=0; i<16; i++){ + module->outChannels[i] = cnt; + module->setOutputDescription(i); + } + } + } + )); + VenomWidget::appendContextMenu(menu); + } + +}; + +Model* modelMultiSplit = createModel("MultiSplit"); diff --git a/src/PolyClone.cpp b/src/PolyClone.cpp index 503ba4a..e7afedf 100644 --- a/src/PolyClone.cpp +++ b/src/PolyClone.cpp @@ -2,8 +2,10 @@ // Licensed under GNU GPLv3 #include "plugin.hpp" +#include "CloneModule.hpp" + +struct PolyClone : CloneModuleBase { -struct PolyClone : VenomModule { enum ParamId { CLONE_PARAM, PARAMS_LEN @@ -55,16 +57,19 @@ struct PolyClone : VenomModule { for (int j=0; j=goodCh && idefaultValue = r->dflt; + q->displayMultiplier = r->scale; + q->displayOffset = r->offset; + } + } + + PolyOffset() { + venomConfig(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + for (int i=0; i<16; i++) { + std::string nm = "Offset " + std::to_string(i+1); + configParam(OFFSET_PARAM+i, 0.f, 1.f, 0.5f, nm, " V", 0.f, 20.f, -10.f); + } + configInput(POLY_INPUT,"Poly"); + configOutput(POLY_OUTPUT,"Poly"); + configBypass(POLY_INPUT, POLY_OUTPUT); + } + + void process(const ProcessArgs& args) override { + VenomModule::process(args); + using float_4 = simd::float_4; + float_4 offset{}; + int cnt = channels ? channels : inputs[POLY_INPUT].getChannels(); + for (int i=0; i(i) + (offset * ranges[rangeId].scale) + ranges[rangeId].offset, i); + } + outputs[POLY_OUTPUT].setChannels(cnt); + } + + json_t* dataToJson() override { + json_t* rootJ = VenomModule::dataToJson(); + json_object_set_new(rootJ, "rangeId", json_integer(rangeId)); + json_object_set_new(rootJ, "channels", json_integer(channels)); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + VenomModule::dataFromJson(rootJ); + json_t* val; + if ((val = json_object_get(rootJ, "rangeId"))) + setRange(json_integer_value(val)); + if ((val = json_object_get(rootJ, "channels"))) + channels = json_integer_value(val); + } + +}; + +struct PolyOffsetWidget : VenomWidget { + + struct PCCountDisplay : DigitalDisplay18 { + void step() override { + if (module) { + PolyOffset* mod = static_cast(module); + if (mod->channels){ + text = string::f("%d", mod->channels); + fgColor = mod->inputs[PolyOffset::POLY_INPUT].getChannels() > mod->channels ? SCHEME_RED : SCHEME_YELLOW; + } else { + text = string::f("%d", mod->inputs[PolyOffset::POLY_INPUT].getChannels()); + fgColor = SCHEME_YELLOW; + } + } else { + text = "16"; + fgColor = SCHEME_YELLOW; + } + } + }; + + PolyOffsetWidget(PolyOffset* module) { + setModule(module); + setVenomPanel("PolyOffset"); + float y=64.5f; + for (int i=0; i<8; i++, y+=24.f){ + addParam(createLockableParamCentered(Vec(12.f, y), module, PolyOffset::OFFSET_PARAM+i)); + addParam(createLockableParamCentered(Vec(33.f, y), module, PolyOffset::OFFSET_PARAM+i+8)); + } + + PCCountDisplay* countDisplay = createWidget(Vec(10.316, 252.431)); + countDisplay->module = module; + addChild(countDisplay); + + addInput(createInputCentered(Vec(22.5,300.5), module, PolyOffset::POLY_INPUT)); + addOutput(createOutputCentered(Vec(22.5,339.5), module, PolyOffset::POLY_OUTPUT)); + }; + + void appendContextMenu(Menu* menu) override { + PolyOffset* module = dynamic_cast(this->module); + menu->addChild(new MenuSeparator); + menu->addChild(createIndexSubmenuItem( + "Offset range", + {"0-1 V","0-2 V","0-5 V","0-10 V","+/- 1 V","+/- 2 V","+/- 5 V","+/- 10 V"}, + [=]() { + return module->rangeId; + }, + [=](int rangeId) { + module->setRange(rangeId); + } + )); + menu->addChild(createIndexPtrSubmenuItem( + "Polyphony channels", + {"Auto","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"}, + &module->channels + )); + VenomWidget::appendContextMenu(menu); + } + +}; + +Model* modelPolyOffset = createModel("PolyOffset"); diff --git a/src/PolyScale.cpp b/src/PolyScale.cpp new file mode 100644 index 0000000..a6d85e4 --- /dev/null +++ b/src/PolyScale.cpp @@ -0,0 +1,125 @@ +// Venom Modules (c) 2023, 2024 Dave Benham +// Licensed under GNU GPLv3 + +#include "plugin.hpp" + +struct PolyScale : VenomModule { + enum ParamId { + ENUMS(LEVEL_PARAM,16), + PARAMS_LEN + }; + enum InputId { + POLY_INPUT, + INPUTS_LEN + }; + enum OutputId { + POLY_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + LIGHTS_LEN + }; + + struct Range { + float scale; + float offset; + float dflt; + }; + Range ranges[8]{ + // 0-1 0-2 0-5 0-10 + {1.f,0.f,1.f}, {2.f,0.f,0.5f}, {5.f,0.f,0.2f}, {10.f,0.f,0.1f}, + // +/- 1 +/- 2 +/- 5 +/- 10 + {2.f,-1.f,1.f}, {4.f,-2.f,0.75f}, {10.f,-5.f,0.6f}, {20.f,-10.f,0.55f} + }; + int rangeId = 0; + + void setRange(int val) { + rangeId = val; + for (int i=0; i<16; i++){ + ParamQuantity *q = paramQuantities[LEVEL_PARAM+i]; + Range* r = &ranges[rangeId]; + q->defaultValue = r->dflt; + q->displayMultiplier = r->scale; + q->displayOffset = r->offset; + } + } + + PolyScale() { + venomConfig(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + for (int i=0; i<16; i++) { + std::string nm = "Level " + std::to_string(i+1); + configParam(LEVEL_PARAM+i, 0.f, 1.f, 1.f, nm, "x"); + } + configInput(POLY_INPUT,"Poly"); + configOutput(POLY_OUTPUT,"Poly"); + configBypass(POLY_INPUT, POLY_OUTPUT); + } + + void process(const ProcessArgs& args) override { + VenomModule::process(args); + int cnt = inputs[POLY_INPUT].getChannels(); + for (int i=0; iinputs[PolyScale::POLY_INPUT].getChannels()) : "16"; + } + }; + + PolyScaleWidget(PolyScale* module) { + setModule(module); + setVenomPanel("PolyScale"); + float y=64.5f; + for (int i=0; i<8; i++, y+=24.f){ + addParam(createLockableParamCentered(Vec(12.f, y), module, PolyScale::LEVEL_PARAM+i)); + addParam(createLockableParamCentered(Vec(33.f, y), module, PolyScale::LEVEL_PARAM+i+8)); + } + + PCCountDisplay* countDisplay = createWidget(Vec(10.316, 252.431)); + countDisplay->module = module; + countDisplay->fgColor = SCHEME_YELLOW; + addChild(countDisplay); + + addInput(createInputCentered(Vec(22.5,300.5), module, PolyScale::POLY_INPUT)); + addOutput(createOutputCentered(Vec(22.5,339.5), module, PolyScale::POLY_OUTPUT)); + } + + void appendContextMenu(Menu* menu) override { + PolyScale* module = dynamic_cast(this->module); + menu->addChild(new MenuSeparator); + menu->addChild(createIndexSubmenuItem( + "Level range", + {"0-1x","0-2x","0-5x","0-10x","+/- 1x","+/- 2x","+/- 5x","+/- 10x"}, + [=]() { + return module->rangeId; + }, + [=](int rangeId) { + module->setRange(rangeId); + } + )); + VenomWidget::appendContextMenu(menu); + } +}; + +Model* modelPolyScale = createModel("PolyScale"); diff --git a/src/PolyUnison.cpp b/src/PolyUnison.cpp index 15f367c..a575e82 100644 --- a/src/PolyUnison.cpp +++ b/src/PolyUnison.cpp @@ -2,8 +2,10 @@ // Licensed under GNU GPLv3 #include "plugin.hpp" +#include "CloneModule.hpp" + +struct PolyUnison : CloneModuleBase { -struct PolyUnison : VenomModule { enum ParamId { CLONE_PARAM, DETUNE_PARAM, @@ -108,12 +110,14 @@ struct PolyUnison : VenomModule { outputs[POLY_OUTPUT].setVoltage(val, c++); } outputs[POLY_OUTPUT].setChannels(goodCh * clones); + processExpander(clones, goodCh); if (lightDivider.process()) { for (int i=1; i<16; i++) { lights[CHANNEL_LIGHTS+i*2].setBrightness(i=goodCh && iaddModel(modelAuxClone); p->addModel(modelBenjolinOsc); p->addModel(modelBernoulliSwitch); p->addModel(modelBernoulliSwitchExpander); @@ -92,10 +93,14 @@ void init(Plugin* p) { p->addModel(modelMixPan); p->addModel(modelMixSend); p->addModel(modelMixSolo); + p->addModel(modelMultiMerge); + p->addModel(modelMultiSplit); p->addModel(modelNORS_IQ); p->addModel(modelNORSIQChord2Scale); p->addModel(modelPolyClone); + p->addModel(modelPolyOffset); p->addModel(modelPolySHASR); + p->addModel(modelPolyScale); p->addModel(modelPolyUnison); p->addModel(modelPush5); p->addModel(modelRecurse); diff --git a/src/plugin.hpp b/src/plugin.hpp index b4969d7..8a3866c 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -10,6 +10,7 @@ using namespace rack; extern Plugin* pluginInstance; // Declare each Model, defined in each module source file +extern Model* modelAuxClone; extern Model* modelBenjolinOsc; extern Model* modelBernoulliSwitch; extern Model* modelBernoulliSwitchExpander; @@ -28,10 +29,14 @@ extern Model* modelMixOffset; extern Model* modelMixPan; extern Model* modelMixSend; extern Model* modelMixSolo; +extern Model* modelMultiMerge; +extern Model* modelMultiSplit; extern Model* modelNORS_IQ; extern Model* modelNORSIQChord2Scale; extern Model* modelPolyClone; +extern Model* modelPolyOffset; extern Model* modelPolySHASR; +extern Model* modelPolyScale; extern Model* modelPolyUnison; extern Model* modelPush5; extern Model* modelRecurse; @@ -187,10 +192,16 @@ struct VenomModule : Module { PortExtension* e = (type==engine::Port::INPUT ? &inputExtensions[portId] : &outputExtensions[portId]); ParamQuantity* q = NULL; ParamExtension* qe = NULL; + PortInfo* piLink = NULL; + PortExtension* eLink = NULL; if (e->nameLink >= 0){ q = paramQuantities[e->nameLink]; qe = ¶mExtensions[e->nameLink]; } + if (e->portNameLink >= 0){ + piLink = (type==engine::Port::INPUT ? outputInfos[e->portNameLink] : inputInfos[e->portNameLink]); + eLink = (type==engine::Port::INPUT ? &outputExtensions[e->portNameLink] : &inputExtensions[e->portNameLink]); + } menu->addChild(new MenuSeparator); menu->addChild(createSubmenuItem("Port name", "", [=](Menu *menu){ @@ -200,6 +211,11 @@ struct VenomModule : Module { editField->changeHandler = [=](std::string text) { pi->name = text; if (q) q->name = text; + if (piLink) { + if (!eLink->factoryName.size()) + eLink->factoryName = piLink->name; + piLink->name = text; + } }; menu->addChild(editField); } @@ -213,6 +229,7 @@ struct VenomModule : Module { [=]() { pi->name = e->factoryName; if (q) q->name = e->factoryName; + if (piLink) piLink->name = e->factoryName; } )); } @@ -240,10 +257,12 @@ struct VenomModule : Module { struct PortExtension { int nameLink; + int portNameLink; std::string factoryName; PortExtension(){ factoryName = ""; nameLink = -1; + portNameLink = -1; } }; @@ -286,6 +305,26 @@ struct VenomModule : Module { for (int i=0; igetLeftExpander().module = NULL; + expander.module->getLeftExpander().moduleId = -1; + event.side = 0; + expander.module->onExpanderChange(event); + } + expander = getLeftExpander(); + if (expander.module){ + expander.module->getRightExpander().module = NULL; + expander.module->getRightExpander().moduleId = -1; + event.side = 1; + expander.module->onExpanderChange(event); + } + } void process(const ProcessArgs& args) override { if (drawn && extProcNeeded){