Browse Source

Merge branch 'v2' of https://github.com/hemmer/Befaco into v2

pull/55/head
hemmer 1 year ago
parent
commit
bd1d36fb04
26 changed files with 12501 additions and 203 deletions
  1. +2
    -1
      .gitignore
  2. +25
    -0
      CHANGELOG.md
  3. +3
    -1
      README.md
  4. +28
    -0
      docs/MIDIThingV2.md
  5. BIN
      docs/img/MidiThingV2.png
  6. BIN
      docs/img/UpdateRate.png
  7. BIN
      docs/img/UpdateRatesScope.png
  8. BIN
      docs/img/VoltageRange.png
  9. +51
    -2
      plugin.json
  10. +76
    -0
      res/components/Davies1900hWhiteEndless.svg
  11. +24
    -0
      res/components/Davies1900hWhiteEndless_bg.svg
  12. +56
    -0
      res/fonts/MISO-info.txt
  13. BIN
      res/fonts/miso.otf
  14. +1131
    -0
      res/panels/Burst.svg
  15. +5595
    -0
      res/panels/MidiThing.svg
  16. +2383
    -0
      res/panels/Octaves.svg
  17. +1338
    -0
      res/panels/Voltio.svg
  18. +4
    -4
      src/ADSR.cpp
  19. +349
    -0
      src/Burst.cpp
  20. +29
    -26
      src/ChowDSP.hpp
  21. +804
    -0
      src/MidiThing.cpp
  22. +336
    -0
      src/Octaves.cpp
  23. +154
    -164
      src/PonyVCO.cpp
  24. +85
    -0
      src/Voltio.cpp
  25. +4
    -0
      src/plugin.cpp
  26. +24
    -5
      src/plugin.hpp

+ 2
- 1
.gitignore View File

@@ -3,4 +3,5 @@
/plugin.dylib
/plugin.dll
/plugin.so
.DS_Store
.DS_Store
/.vscode

+ 25
- 0
CHANGELOG.md View File

@@ -1,5 +1,30 @@
# Change Log


## v2.7.0
* Midi Thing 2
* Initial release
* Octaves
* Better default oversampling setting (x4)


## v2.6.0
* Octaves
* Initial release
* Misc
* Better default values for ADSR and Burst


## v2.5.0
* Burst
* Initial release
* Voltio
* Initial release
* PonyVCO
* Now polyphonic
* Misc
* Fix trigger inputs to follow Rack voltage standards (Kickall, Muxlicer, Rampage)

## v2.4.1
* Rampage
* Fix SIMD bug


+ 3
- 1
README.md View File

@@ -28,4 +28,6 @@ We have tried to make the VCV implementations as authentic as possible, however
* to limit the pulsewidth from 5% to 95% (hardware is full range)
* to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles)

* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid acidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop.
* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid acidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop.

* Burst hardware version version can also set the tempo by tapping the encoder, this is not possible in the VCV version.

+ 28
- 0
docs/MIDIThingV2.md View File

@@ -0,0 +1,28 @@
# MIDI Thing v2

The original MIDI Thing v2 hardware unit is described as follows:

> Midi Thing v2 is a flexible MIDI to CV converter. Allowing polyphonic notes handling, envelope and LFO generation as well as all available MIDI messages to be converted into CV. This is a huge upgrade from our previous beloved MIDI Thing, which adds a screen for easy configuration,12 assignable ports, TRS, USB Host and Device, MIDI merge OUT, a web configuration tool, and a VCV rack Bridge counterpart.

The VCV counterpart is designed to allow users to quickly get up and running with their hardware, i.e. sending CV from VCV to the hardware unit.

## Setup

To use, first ensure the MIDI Thing v2 is plugged into your computer, and visible as a MIDI device. Then select it, either from the top of the module, or the right click context menu. Then click "SYNC" - this puts the MIDI Thing into a preset designed to work with VCV Rack, and syncronises settings/voltage ranges etc. Note that for now, sync is one-way (VCV to hardware).

![MIDI Thing Config](img/MidiThingV2.png "MIDI Thing v2 Setup")

## Usage

To use, simply wire CV which you wish to send to the hardware to the matching input on the VCV module. Note that you will need to select the range, which can be done by right-clicking on the matching box (see below). Options are 0/10v, -5/5v, -10/0v, 0/8v, 0/5v. Note that the module is **not** designed to work with audio rate signals, just CV.

![MIDI Thing Voltage Range](img/VoltageRange.png "MIDI Thing v2 Voltage Range")

## Update Rate

Midi Thing v2 VCV allows the user to configure the update rate at which data is sent over MIDI. This must be shared between the channels, so if we set the hardware to update at 1 kHz, 1 active channel will update at 1 kHz, 2 active channels will update at 500 Hz, 4 active channels at 250 Hz and so on. The total update rate (to be shared between channels) is set from the context menu, noting that higher update rates will use more CPU. The effect of the update rate on a 90 Hz saw (blue trace) can be seen in the bottom image, specifically that the temporal resolution of the reconstructed signal (red traces) improves as the update rate is increased from 500 Hz to 1000 Hz to 2000 Hz.

![MIDI Thing Update Rates](img/UpdateRate.png "MIDI Thing v2 Update Ranges Menu")
![MIDI Thing Update Rates](img/UpdateRatesScope.png "MIDI Thing v2 Update Ranges Menu")



BIN
docs/img/MidiThingV2.png View File

Before After
Width: 281  |  Height: 1015  |  Size: 102KB

BIN
docs/img/UpdateRate.png View File

Before After
Width: 762  |  Height: 799  |  Size: 102KB

BIN
docs/img/UpdateRatesScope.png View File

Before After
Width: 2968  |  Height: 963  |  Size: 754KB

BIN
docs/img/VoltageRange.png View File

Before After
Width: 376  |  Height: 394  |  Size: 30KB

+ 51
- 2
plugin.json View File

@@ -1,6 +1,6 @@
{
"slug": "Befaco",
"version": "2.4.1",
"version": "2.7.0",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
@@ -267,6 +267,7 @@
"Hardware clone",
"Low-frequency oscillator",
"Oscillator",
"Polyphonic",
"Waveshaper"
]
},
@@ -282,6 +283,54 @@
"Mixer",
"Visual"
]
},
{
"slug": "Burst",
"name": "Burst",
"description": "Trigger processor and generator, designed to add an organic chain of events",
"manualUrl": "https://www.befaco.org/burst-2/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-burst-",
"tags": [
"Clock generator",
"Clock modulator",
"Hardware clone"
]
},
{
"slug": "MidiThingV2",
"name": "MIDI Thing V2",
"description": "Hardware MIDI Thing v2 is a flexible MIDI to CV converter, this module acts as a bridge from VCV",
"manualUrl": "https://github.com/VCVRack/Befaco/blob/v2/docs/MIDIThingV2.md",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-midi-thing-v2",
"tags": [
"External",
"MIDI",
"Hardware clone"
]
},
{
"slug": "Voltio",
"name": "Voltio",
"description": "An accurate voltage source and precision adder.",
"manualUrl": "https://www.befaco.org/voltio/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-voltio",
"tags": [
"Hardware clone",
"Polyphonic",
"Utility"
]
},
{
"slug": "Octaves",
"name": "Octaves",
"description": "A harsh and funky take of an additive Oscillator.",
"manualUrl": "https://www.befaco.org/octaves-vco/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-octaves-vco",
"tags": [
"Hardware clone",
"Oscillator",
"Polyphonic"
]
}
]
}
}

+ 76
- 0
res/components/Davies1900hWhiteEndless.svg View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->

<svg
version="1.0"
id="svg16908"
x="0px"
y="0px"
width="36px"
height="36.0016px"
viewBox="0 0 36 36.0016"
enable-background="new 0 0 36 36.0016"
xml:space="preserve"
sodipodi:docname="Davies1900hWhite.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs146" />
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1.0"
fit-margin-bottom="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-top="0"
id="base"
inkscape:current-layer="svg16908"
inkscape:cx="-99.373756"
inkscape:cy="8.9651038"
inkscape:document-units="mm"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-height="1301"
inkscape:window-maximized="0"
inkscape:window-width="2560"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:zoom="3.959798"
pagecolor="#ffffff"
showgrid="false"
units="px"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
</sodipodi:namedview>
<path
opacity="0.49"
fill="#C4C4C4"
enable-background="new "
d="M31.26635,28.45262 c0.93884-1.19196,2.5792-1.87889,3.18775-3.35014c0.6026-1.45668,0.14837-3.59276,0.31776-5.08902 c0.04287-0.14134,0.07395-0.28706,0.08797-0.43569c0.10079-1.0664,0.09812-2.13086-0.00212-3.17977 c-0.01245-0.13042-0.04262-0.25719-0.07741-0.38226c0.00037,0.00146,0.0014,0.00267,0.00176,0.00389 c-0.17899-1.50646,0.38048-3.4437-0.22946-4.91423c-0.60302-1.45401-2.31693-2.35022-3.2545-3.52615 c-0.06909-0.1292-0.1493-0.2533-0.24353-0.36769c-0.67072-0.8131-1.42128-1.56767-2.24664-2.25107 c-0.11427-0.09447-0.23855-0.1751-0.36799-0.24456c-1.17994-0.9338-1.64092-2.46532-3.09736-3.06786 c-1.44557-0.59792-3.78361-0.23893-5.27065-0.40505c-0.15015-0.0476-0.30576-0.08014-0.46429-0.0952 c-1.06664-0.10079-2.13098-0.09812-3.18008,0.00219c-0.13618,0.01287-0.26897,0.04347-0.39926,0.08087 c-1.50288,0.17753-3.3998-0.39479-4.86571,0.21309c-1.45595,0.60399-2.38756,2.335-3.5652,3.27342 c-0.12477,0.06776-0.24529,0.1445-0.35585,0.23582c-0.8131,0.67054-1.56791,1.42122-2.2512,2.24646 c-0.09289,0.1122-0.17152,0.2346-0.24019,0.36162c-0.9409,1.1871-2.73669,2.27351-3.3436,3.74063 c-0.60849,1.47101,0.01993,3.22704-0.15754,4.73399c-0.03315,0.12192-0.05938,0.23728-0.0714,0.36405 c-0.10079,1.06664-0.09812,2.13086,0.00213,3.18002c0.01263,0.13236,0.04098,0.25573,0.07668,0.38275 c0.1779,1.51375-0.36958,3.46796,0.24328,4.94577c0.60994,1.47052,2.33699,2.36579,3.27693,3.55678 c0.06521,0.11706,0.12022,0.19672,0.20631,0.3009c0.67072,0.8131,1.42122,1.56791,2.24658,2.25108 c0.10868,0.0901,0.20534,0.15495,0.32786,0.22222c1.19621,0.94448,1.86602,2.57203,3.34419,3.18356 c1.47113,0.60861,3.6505,0.16209,5.15727,0.33962c0.13029,0.03716,0.23503,0.05465,0.37109,0.06752 c1.06652,0.10078,2.13086,0.09811,3.1799-0.00219c0.13546-0.01287,0.24608-0.03595,0.37577-0.07286 c1.51381-0.17802,3.40228,0.39571,4.8799-0.21727c1.47052-0.60983,2.43178-2.36312,3.62277-3.303 c0.11839-0.06557,0.20243-0.12313,0.30783-0.21008c0.81304-0.67078,1.56779-1.42122,2.25101-2.2467 C31.13447,28.67265,31.20035,28.57113,31.26635,28.45262z M8.23466,4.50073c-0.00018,0-0.00036,0-0.00061,0 C8.23424,4.50073,8.23448,4.50073,8.23466,4.50073z"
id="path133" />
<path
opacity="0.32"
fill="#9C9C9C"
enable-background="new "
d="M35.90357,16.15523 c-0.07361-0.70705-0.49979-1.29864-1.06952-1.72377c-0.61895-0.46186-1.30921-1.50206-1.85906-2.79493 c-0.55021-1.29408-0.82048-2.5138-0.72318-3.27993c0.08954-0.70504-0.04032-1.42231-0.49814-1.9659 c-0.71619-0.85038-1.51525-1.63894-2.39182-2.35207c-0.55256-0.44953-1.27422-0.56869-1.97903-0.46552 c-0.76414,0.11186-1.98919-0.13576-3.29369-0.66165c-1.30531-0.52654-2.35981-1.19865-2.83229-1.80971 c-0.43578-0.56359-1.03802-0.97887-1.74792-1.03872c-1.12585-0.09492-2.24826-0.08171-3.35386,0.03338 c-0.7071,0.07361-1.29866,0.49991-1.72382,1.06969c-0.46185,0.61894-1.50183,1.30921-2.79484,1.85879 C10.34243,3.57512,9.12277,3.8454,8.35668,3.74825C7.65162,3.65884,6.93432,3.78858,6.39073,4.24642 c-0.85038,0.71624-1.639,1.51525-2.35212,2.39175c-0.44951,0.5525-0.56872,1.27411-0.46555,1.97886 c0.11186,0.76415-0.13561,1.9892-0.66171,3.29384c-0.52652,1.30541-1.19883,2.3602-1.80989,2.83256 c-0.56343,0.43554-0.97863,1.03749-1.03844,1.74712c-0.09491,1.12597-0.0817,2.24858,0.03345,3.35436 c0.07363,0.70707,0.49988,1.29863,1.06962,1.7238c0.61889,0.46184,1.30912,1.50186,1.85894,2.79489 c0.5502,1.29385,0.82051,2.5137,0.72324,3.27988c-0.08951,0.70504,0.0403,1.42231,0.49813,1.96589 c0.71613,0.85026,1.51513,1.63887,2.39162,2.35188c0.5526,0.44954,1.2743,0.56875,1.97914,0.46561 c0.76419-0.11182,1.98924,0.13566,3.29371,0.6618c1.3056,0.52645,2.36037,1.19888,2.83273,1.80995 c0.43551,0.56341,1.03745,0.97859,1.74704,1.03843c1.12602,0.09496,2.24863,0.08175,3.35437-0.03349 c0.70696-0.07368,1.29852-0.49973,1.72354-1.06945c0.46177-0.61897,1.50192-1.30924,2.79504-1.85913 c1.2939-0.55021,2.5135-0.82048,3.27964-0.72324c0.70521,0.08952,1.42269-0.04021,1.96637-0.4982 c0.85029-0.71627,1.63881-1.51539,2.35187-2.39191c0.44946-0.55249,0.56865-1.27406,0.46548-1.97877 c-0.11187-0.76414,0.1356-1.98916,0.66171-3.2936c0.52645-1.30538,1.19873-2.36014,1.8098-2.83266 c0.56349-0.43573,0.97872-1.03787,1.03854-1.74766C36.03189,18.38322,36.01868,17.26087,35.90357,16.15523z M35.14483,19.45553 c-0.03778,0.4441-0.26189,0.84652-0.59933,1.07659c-0.76952,0.52408-1.56887,1.71213-2.19307,3.25983 c-0.62362,1.54641-0.87236,2.95595-0.68247,3.86751c0.08327,0.39955-0.03467,0.84465-0.31572,1.19049 c-0.67815,0.83517-1.44105,1.61091-2.26751,2.30584c-0.26963,0.22685-0.59977,0.35347-0.92951,0.35656 c-0.08353,0.00078-0.16682-0.00671-0.2476-0.02198c-0.1923-0.03671-0.40678-0.05408-0.63754-0.05193 c-0.87313,0.00817-2.01639,0.29437-3.21898,0.80596c-1.5346,0.65219-2.70716,1.47313-3.21707,2.25219 c-0.22193,0.33908-0.61765,0.56918-1.05824,0.61569c-0.53955,0.05674-1.08767,0.08797-1.62899,0.09303 c-0.53344,0.00499-1.07326-0.01528-1.60433-0.06045c-0.44403-0.03797-0.84652-0.26189-1.07653-0.59959 c-0.52428-0.76965-1.71252-1.569-3.26015-2.19313c-1.21234-0.489-2.36075-0.75375-3.23362-0.74558 c-0.22967,0.00215-0.44288,0.02353-0.63362,0.0633c-0.08048,0.01678-0.16368,0.02557-0.24741,0.02635 c-0.33272,0.00311-0.66769-0.11832-0.94328-0.34214c-0.8353-0.67835-1.61104-1.44124-2.30585-2.2677 C4.55894,28.7474,4.434,28.30741,4.50968,27.90948c0.17409-0.9147-0.1008-2.32038-0.75404-3.85691 C3.103,22.51811,2.282,21.34549,1.5032,20.83563c-0.33915-0.22193-0.56925-0.61752-0.61549-1.05799 c-0.11271-1.07355-0.12367-2.16148-0.03246-3.23358c0.03778-0.4441,0.26183-0.84652,0.59927-1.07633 c0.76958-0.52408,1.56894-1.71239,2.19313-3.26009c0.62362-1.54641,0.87236-2.95595,0.68247-3.86725 c-0.08327-0.39955,0.0348-0.84465,0.31572-1.19075c0.67899-0.83595,1.44195-1.6117,2.26777-2.30559 C7.18324,4.61721,7.51324,4.49058,7.84279,4.4875c0.08366-0.00078,0.16695,0.00671,0.24773,0.02198 C8.28263,4.54619,8.49704,4.56356,8.72768,4.5614c0.87288-0.00817,2.01613-0.29436,3.21936-0.80597 c1.53479-0.65271,2.70734-1.47364,3.21701-2.25219c0.22206-0.33934,0.61758-0.56944,1.05818-0.61569 c0.53961-0.05674,1.08773-0.08797,1.62912-0.09304c0.53357-0.00499,1.07339,0.01528,1.60439,0.06045 c0.44416,0.03797,0.84659,0.26189,1.07653,0.59959c0.52408,0.76939,1.71232,1.56874,3.25989,2.19287 c1.2124,0.489,2.36088,0.75401,3.23388,0.74584c0.22967-0.00215,0.44281-0.02353,0.63356-0.0633 c0.08048-0.01678,0.16368-0.02557,0.2474-0.02635c0.33272-0.00311,0.66776,0.11832,0.94315,0.34188 c0.8353,0.6786,1.61118,1.4415,2.30585,2.2677c0.28513,0.33899,0.41014,0.77924,0.33445,1.17717 c-0.17403,0.9147,0.1008,2.32038,0.75397,3.85665c0.65245,1.53447,1.47345,2.70709,2.25245,3.21694 c0.33915,0.22193,0.56924,0.61752,0.61549,1.05824C35.22515,17.29576,35.23604,18.38344,35.14483,19.45553z"
id="path135" />
<path
opacity="0.46"
fill="#FFFFFF"
enable-background="new "
d="M34.06219,15.82835 c-0.92052-0.6026-1.8255-1.87058-2.54837-3.57056c-0.72367-1.70204-1.00892-3.23478-0.8033-4.3157 c0.0311-0.16363-0.03098-0.36205-0.16211-0.51795c-0.6622-0.78748-1.40201-1.51505-2.19884-2.16248 c-0.1321-0.10705-0.28667-0.16556-0.43514-0.16417c-0.03146,0.00029-0.0627,0.00343-0.09288,0.00966 c-0.24182,0.05033-0.50698,0.07737-0.78808,0.08c-0.97456,0.00912-2.23113-0.27633-3.53828-0.80347 c-1.71445-0.69131-2.99972-1.57298-3.61906-2.48206c-0.09538-0.14022-0.28238-0.23797-0.48807-0.25568 c-0.50527-0.04283-1.01994-0.0623-1.52929-0.05754c-0.51697,0.00484-1.03964,0.0348-1.55357,0.08871 c-0.20287,0.02128-0.38549,0.1212-0.47684,0.26084c-0.60235,0.92026-1.87045,1.82504-3.57063,2.54805 c-1.29693,0.5515-2.54794,0.86016-3.52263,0.86928C8.45162,5.35792,8.18457,5.33561,7.94153,5.2893 c-0.02874-0.00542-0.05997-0.00823-0.0913-0.00793c-0.14562,0.00136-0.2967,0.0617-0.42555,0.17016 C6.63706,6.11327,5.90956,6.85302,5.26206,7.65017C5.13214,7.81032,5.07279,8.01272,5.10729,8.17832 c0.22439,1.07689-0.03253,2.61315-0.72341,4.32637c-0.69151,1.71451-1.57317,2.99998-2.48252,3.61919 c-0.13989,0.09538-0.23765,0.28212-0.25511,0.48748c-0.08696,1.02217-0.07655,2.05946,0.03099,3.08344 c0.02128,0.20268,0.12113,0.38524,0.26064,0.47645c0.92026,0.6026,1.82524,1.87058,2.54831,3.57056 c0.7238,1.70256,1.00906,3.23529,0.80337,4.31621c-0.0311,0.16337,0.03104,0.36153,0.16204,0.51744 c0.66259,0.78799,1.40227,1.51556,2.19871,2.16248c0.13236,0.10731,0.287,0.16582,0.4354,0.16443 c0.0314-0.00029,0.06258-0.00343,0.09276-0.00965c0.24195-0.05033,0.50711-0.07737,0.78827-0.08 c0.97482-0.00912,2.23132,0.27607,3.53802,0.80321c1.71445,0.69131,2.99979,1.57298,3.61932,2.48232 c0.09532,0.14022,0.28238,0.23796,0.488,0.25568c0.50533,0.04282,1.02001,0.0623,1.52929,0.05754 c0.5169-0.00484,1.03958-0.0348,1.55337-0.08871c0.2028-0.02154,0.38569-0.12146,0.47697-0.26084 c0.6026-0.92078,1.87071-1.82556,3.57082-2.54805c1.29603-0.55149,2.54691-0.86015,3.52212-0.86928 c0.28349-0.00265,0.55054,0.01966,0.79378,0.06597c0.02887,0.00542,0.0601,0.00822,0.09137,0.00793 c0.14575-0.00136,0.29702-0.0617,0.42581-0.17016c0.78838-0.66304,1.51582-1.40279,2.16229-2.1989 c0.13011-0.1599,0.18941-0.3623,0.15484-0.52789c-0.22433-1.07715,0.0326-2.61341,0.7234-4.32662 c0.69144-1.71425,1.57304-2.99972,2.48245-3.61893c0.13983-0.09538,0.23771-0.28238,0.25517-0.48774 c0.08696-1.02217,0.07655-2.05946-0.03098-3.08318C34.30149,16.10213,34.20164,15.91957,34.06219,15.82835z"
id="path137" />
<g
opacity="0.6"
enable-background="new "
id="g141">
<path
fill="#FFFFFF"
d="M17.85875,1.58839c0.50938-0.00477,1.02404,0.01471,1.52934,0.05754 c0.20568,0.01772,0.39264,0.11547,0.48802,0.25568c0.61935,0.90909,1.90463,1.79075,3.61907,2.48207 c1.30716,0.52714,2.56374,0.81259,3.53827,0.80347c0.28111-0.00263,0.5463-0.02966,0.78808-0.08 c0.03021-0.00623,0.06145-0.00936,0.09288-0.00966c0.14846-0.00139,0.30307,0.05712,0.43514,0.16417 c0.79686,0.64743,1.53664,1.375,2.19888,2.16248c0.13108,0.15591,0.19319,0.35433,0.16209,0.51795 c-0.20565,1.08092,0.07962,2.61365,0.80329,4.3157c0.72289,1.69998,1.62788,2.96796,2.54836,3.57056 c0.13948,0.09122,0.23933,0.27378,0.26062,0.47671c0.10753,1.02372,0.11795,2.061,0.03097,3.08318 c-0.01748,0.20537-0.11537,0.39236-0.25518,0.48774c-0.9094,0.61921-1.79101,1.90468-2.48246,3.61893 c-0.69081,1.71321-0.94771,3.24947-0.72339,4.32662c0.03453,0.1656-0.02471,0.36799-0.15483,0.52789 c-0.64647,0.79611-1.37391,1.53585-2.1623,2.1989c-0.12882,0.10846-0.28006,0.1688-0.42584,0.17016 c-0.03127,0.00029-0.06247-0.00252-0.09134-0.00793c-0.24322-0.04631-0.51029-0.06862-0.79381-0.06597 c-0.97516,0.00913-2.22609,0.31778-3.52207,0.86928c-1.70012,0.72249-2.96826,1.62727-3.57084,2.54805 c-0.0913,0.13938-0.27418,0.2393-0.47697,0.26084c-0.51383,0.05391-1.03647,0.08387-1.55341,0.08871 c-0.50928,0.00476-1.02392-0.01471-1.52924-0.05754c-0.20568-0.01772-0.39268-0.11547-0.48805-0.25568 c-0.61948-0.90935-1.90487-1.79101-3.61932-2.48232c-1.30669-0.52714-2.56319-0.81233-3.53802-0.80321 c-0.28113,0.00263-0.54633,0.02966-0.78824,0.08c-0.03018,0.00623-0.06139,0.00936-0.09279,0.00965 c-0.1484,0.00139-0.30301-0.05712-0.43538-0.16443c-0.7964-0.64692-1.53613-1.37449-2.19871-2.16248 c-0.13102-0.15591-0.19311-0.35407-0.16204-0.51744c0.20572-1.08092-0.07959-2.61365-0.80337-4.31621 c-0.72307-1.69998-1.62804-2.96796-2.54829-3.57056c-0.13951-0.09122-0.23938-0.27378-0.26066-0.47645 c-0.1075-1.02397-0.11792-2.06126-0.03097-3.08344c0.01746-0.20537,0.11523-0.3921,0.25511-0.48748 c0.90933-0.61921,1.79098-1.90468,2.48253-3.61919c0.69082-1.71321,0.94775-3.24947,0.72339-4.32636 c-0.03453-0.1656,0.02482-0.36799,0.15476-0.52815c0.64751-0.79715,1.375-1.5369,2.16263-2.19864 C7.5535,5.34306,7.70463,5.28273,7.8502,5.28136C7.88153,5.28107,7.91277,5.28388,7.94152,5.2893 C8.1846,5.33561,8.4516,5.35792,8.73508,5.35527c0.97472-0.00912,2.22573-0.31778,3.52263-0.86928 c1.70023-0.72301,2.96828-1.62779,3.57063-2.54805c0.09134-0.13964,0.27402-0.23956,0.47687-0.26084 C16.81911,1.62319,17.34179,1.59323,17.85875,1.58839 M17.85133,0.79453c-0.54139,0.00507-1.08951,0.0363-1.62912,0.09304 c-0.4406,0.04625-0.83612,0.27635-1.05818,0.61569c-0.50966,0.77854-1.68222,1.59947-3.21701,2.25219 c-1.20323,0.5116-2.34649,0.7978-3.21936,0.80597C8.49702,4.56356,8.28261,4.54619,8.0905,4.50948 C8.00972,4.49421,7.92643,4.48672,7.84277,4.4875C7.51322,4.49058,7.18321,4.61721,6.91358,4.84406 C6.08777,5.53795,5.3248,6.31369,4.64582,7.14964C4.3649,7.49574,4.24682,7.94085,4.3301,8.3404 c0.18989,0.9113-0.05885,2.32084-0.68247,3.86725c-0.62419,1.5477-1.42355,2.736-2.19313,3.26009 c-0.33744,0.22981-0.56149,0.63223-0.59927,1.07633c-0.09121,1.07209-0.08026,2.16003,0.03246,3.23358 c0.04625,0.44047,0.27634,0.83606,0.61549,1.05799c0.7788,0.50985,1.5998,1.68247,2.25244,3.21694 c0.65325,1.53653,0.92814,2.9422,0.75404,3.85691c-0.07568,0.39793,0.04926,0.83792,0.33432,1.17692 c0.6948,0.82646,1.47055,1.58935,2.30585,2.2677c0.27559,0.22382,0.61057,0.34525,0.94328,0.34214 c0.08373-0.00078,0.16693-0.00957,0.24741-0.02635c0.19075-0.03978,0.40395-0.06115,0.63362-0.0633 c0.87288-0.00817,2.02128,0.25658,3.23362,0.74558c1.54764,0.62413,2.73588,1.42348,3.26015,2.19313 c0.23001,0.3377,0.63249,0.56162,1.07653,0.59959c0.53107,0.04517,1.07088,0.06544,1.60433,0.06045 c0.54132-0.00507,1.08945-0.0363,1.62899-0.09303c0.4406-0.04651,0.83631-0.27661,1.05824-0.61569 c0.50992-0.77906,1.68247-1.59999,3.21707-2.25219c1.20259-0.51159,2.34584-0.79779,3.21898-0.80596 c0.23077-0.00216,0.44524,0.01521,0.63754,0.05192c0.08078,0.01527,0.16406,0.02276,0.2476,0.02198 c0.32974-0.00308,0.65988-0.12971,0.92951-0.35656c0.82645-0.69493,1.58935-1.47067,2.26751-2.30584 c0.28105-0.34584,0.399-0.79095,0.31572-1.19049c-0.18989-0.91156,0.05885-2.3211,0.68247-3.86751 c0.62419-1.5477,1.42355-2.73575,2.19307-3.25983c0.33744-0.23007,0.56155-0.63249,0.59933-1.07659 c0.09121-1.07209,0.08032-2.15977-0.03245-3.23332c-0.04625-0.44073-0.27635-0.83632-0.6155-1.05824 c-0.779-0.50985-1.59999-1.68247-2.25245-3.21694c-0.65318-1.53627-0.928-2.94195-0.75397-3.85665 c0.07568-0.39793-0.04932-0.83818-0.33446-1.17717c-0.69467-0.8262-1.47054-1.5891-2.30585-2.2677 c-0.27539-0.22356-0.61044-0.34499-0.94315-0.34188c-0.08373,0.00078-0.16693,0.00957-0.24741,0.02635 c-0.19075,0.03978-0.40389,0.06115-0.63356,0.0633c-0.873,0.00817-2.02148-0.25684-3.23388-0.74584 c-1.54757-0.62413-2.73581-1.42348-3.25989-2.19287c-0.22994-0.3377-0.63236-0.56162-1.07653-0.59959 C18.92471,0.80981,18.3849,0.78953,17.85133,0.79453L17.85133,0.79453z"
id="path139" />
</g>

</svg>

+ 24
- 0
res/components/Davies1900hWhiteEndless_bg.svg View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0"
id="svg16908" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="36px" height="36.0016px"
viewBox="0 0 36 36.0016" enable-background="new 0 0 36 36.0016" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" fit-margin-bottom="0" fit-margin-left="0" fit-margin-right="0" fit-margin-top="0" id="base" inkscape:current-layer="layer1" inkscape:cx="-91.947964" inkscape:cy="8.8255993" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1422" inkscape:window-maximized="0" inkscape:window-width="2560" inkscape:window-x="0" inkscape:window-y="18" inkscape:zoom="3.959798" pagecolor="#ffffff" showgrid="false" units="px">
</sodipodi:namedview>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="18.00007" y1="9.094947e-12" x2="18.00007" y2="36">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#666666"/>
</linearGradient>
<circle fill="url(#SVGID_1_)" cx="18.00007" cy="18" r="18"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="112.00964" y1="213.36432" x2="112.00964" y2="233.88402" gradientTransform="matrix(-1 0 0 0.50871 129.93398 -104.22826)">
<stop offset="0" style="stop-color:#FDFDFF"/>
<stop offset="1" style="stop-color:#FCFEFF;stop-opacity:0"/>
</linearGradient>
<ellipse fill="url(#SVGID_2_)" cx="17.92434" cy="9.53234" rx="8.37098" ry="5.21932"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="112.00964" y1="213.36432" x2="112.00964" y2="233.88402" gradientTransform="matrix(-1 0 0 0.50871 129.93398 -104.22826)">
<stop offset="0" style="stop-color:#FDFDFF"/>
<stop offset="1" style="stop-color:#FCFEFF;stop-opacity:0"/>
</linearGradient>
<ellipse fill="url(#SVGID_3_)" cx="17.92434" cy="9.53234" rx="8.37098" ry="5.21932"/>
</svg>

+ 56
- 0
res/fonts/MISO-info.txt View File

@@ -0,0 +1,56 @@
M M I SSS OOO
MM MM I S S O O
M M M M I S O O
M M M I S O O
M M I S O O
M M I S S O O
M M I SSS OOO

---------------------------------------
MISO is an architectural lettering font
completed in 2006 by MĂĄrten Nettelbladt.
---------------------------------------
MISO is available in three weights
(Light, Regular, Bold)
in TrueType and OpenType format.
---------------------------------------

L I C E N S E I N F O R M A T I O N
---------------------------------------
MISO is a free typeface. However,
there is one important limitation:

MISO MUST ALWAYS REMAIN COMPLETELY FREE

You can use MISO for personal and commercial work.
You can share MISO with your friends
as long as you include this text file.

You must not sell MISO.
You must not charge someone else for using MISO.
You must not bundle MISO with a sold product.

Use it, share it, but keep it free.
---------------------------------------

MĂĄrten Nettelbladt
Omkrets arkitektur
www.omkrets.se

Stockholm, Sweden
July 9th 2009

---------------------------------------
If you have any comments about MISO
please let me know:
miso (a) omkrets.se
---------------------------------------

November 27th 2008
Converted to OpenType by Torin Hill.

June 24th 2007
Some small adjustments

October 23rd 2006
Released

BIN
res/fonts/miso.otf View File


+ 1131
- 0
res/panels/Burst.svg
File diff suppressed because it is too large
View File


+ 5595
- 0
res/panels/MidiThing.svg
File diff suppressed because it is too large
View File


+ 2383
- 0
res/panels/Octaves.svg
File diff suppressed because it is too large
View File


+ 1338
- 0
res/panels/Voltio.svg
File diff suppressed because it is too large
View File


+ 4
- 4
src/ADSR.cpp View File

@@ -231,10 +231,10 @@ struct ADSR : Module {
configButton(MANUAL_TRIGGER_PARAM, "Trigger envelope");
configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape");

configParam(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s", maxStageTime / minStageTime, minStageTime);
configParam(DECAY_PARAM, 0.f, 1.f, 0.f, "Decay time", "s", maxStageTime / minStageTime, minStageTime);
configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f);
configParam(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s", maxStageTime / minStageTime, minStageTime);
configParam(ATTACK_PARAM, 0.f, 1.f, 0.4f, "Attack time", "s", maxStageTime / minStageTime, minStageTime);
configParam(DECAY_PARAM, 0.f, 1.f, 0.4f, "Decay time", "s", maxStageTime / minStageTime, minStageTime);
configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.5f, "Sustain level", "%", 0.f, 100.f);
configParam(RELEASE_PARAM, 0.f, 1.f, 0.4f, "Release time", "s", maxStageTime / minStageTime, minStageTime);

configInput(TRIGGER_INPUT, "Trigger");
configInput(CV_ATTACK_INPUT, "Attack CV");


+ 349
- 0
src/Burst.cpp View File

@@ -0,0 +1,349 @@
#include "plugin.hpp"

#define MAX_REPETITIONS 32 /// max number of repetitions
#define TRIGGER_TIME 0.001

// a tempo/clock calculator that responds to pings - this sets the base tempo, multiplication/division of
// this tempo occurs in the BurstEngine
struct PingableClock {

dsp::Timer timer; // time the gap between pings
dsp::PulseGenerator clockTimer; // counts down from tempo length to zero
dsp::BooleanTrigger clockExpiry; // checks for when the clock timer runs out

float pingDuration = 0.5f; // used for calculating and updating tempo (default 2Hz / 120 bpm)
float tempo = 0.5f; // actual current tempo of clock

PingableClock() {
clockTimer.trigger(tempo);
}

void process(bool pingRecieved, float sampleTime) {
timer.process(sampleTime);

bool clockRestarted = false;

if (pingRecieved) {

bool tempoShouldBeUpdated = true;
float duration = timer.getTime();

// if the ping was unusually different to last time
bool outlier = duration > (pingDuration * 2) || duration < (pingDuration / 2);
// if there is a previous estimate of tempo, but it's an outlier
if ((pingDuration && outlier)) {
// don't calculate tempo from this; prime so future pings will update
tempoShouldBeUpdated = false;
pingDuration = 0;
}
else {
pingDuration = duration;
}
timer.reset();

if (tempoShouldBeUpdated) {
// if the tempo should be updated, do so
tempo = pingDuration;
clockRestarted = true;
}
}

// we restart the clock if a) a new valid ping arrived OR b) the current clock expired
clockRestarted = clockExpiry.process(!clockTimer.process(sampleTime)) || clockRestarted;
if (clockRestarted) {
clockTimer.reset();
clockTimer.trigger(tempo);
}
}

bool isTempoOutHigh() {
// give a 1ms pulse as tempo out
return clockTimer.remaining > tempo - TRIGGER_TIME;
}
};

// engine that generates a burst when triggered
struct BurstEngine {

dsp::PulseGenerator eocOutput; // for generating EOC trigger
dsp::PulseGenerator burstOutput; // for generating triggers for each occurance of the burst
dsp::Timer burstTimer; // for timing how far through the current burst we are

float timings[MAX_REPETITIONS + 1] = {}; // store timings (calculated once on burst trigger)

int triggersOccurred = 0; // how many triggers have been
int triggersRequested = 0; // how many bursts have been requested (fixed over course of burst)
bool active = true; // is there a burst active
bool wasInhibited = false; // was this burst inhibited (i.e. just the first trigger sent)

std::tuple<float, float, bool> process(float sampleTime) {

if (active) {
burstTimer.process(sampleTime);
}

bool eocTriggered = false;
if (burstTimer.time > timings[triggersOccurred]) {
if (triggersOccurred < triggersRequested) {
burstOutput.reset();
burstOutput.trigger(TRIGGER_TIME);
}
else if (triggersOccurred == triggersRequested) {
eocOutput.reset();
eocOutput.trigger(TRIGGER_TIME);
active = false;
eocTriggered = true;
}
triggersOccurred++;
}

const float burstOut = burstOutput.process(sampleTime);
// NOTE: we don't get EOC if the burst was inhibited
const float eocOut = eocOutput.process(sampleTime) * !wasInhibited;
return std::make_tuple(burstOut, eocOut, eocTriggered);
}

void trigger(int numBursts, int multDiv, float baseTimeWindow, float distribution, bool inhibitBurst, bool includeOriginalTrigger) {

active = true;
wasInhibited = inhibitBurst;

// the window in which the burst fits is a multiple (or division) of the base tempo
int divisions = multDiv + (multDiv > 0 ? 1 : multDiv < 0 ? -1 : 0); // skip 2/-2
float actualTimeWindow = baseTimeWindow;
if (divisions > 0) {
actualTimeWindow = baseTimeWindow * divisions;
}
else if (divisions < 0) {
actualTimeWindow = baseTimeWindow / (-divisions);
}

// calculate the times at which triggers should fire, will be skewed by distribution
const float power = 1 + std::abs(distribution) * 2;
for (int i = 0; i <= numBursts; ++i) {
if (distribution >= 0) {
timings[i] = actualTimeWindow * std::pow((float)i / numBursts, power);
}
else {
timings[i] = actualTimeWindow * std::pow((float)i / numBursts, 1 / power);
}
}

triggersOccurred = includeOriginalTrigger ? 0 : 1;
triggersRequested = inhibitBurst ? 1 : numBursts;
burstTimer.reset();
}
};

struct Burst : Module {
enum ParamIds {
CYCLE_PARAM,
QUANTITY_PARAM,
TRIGGER_PARAM,
QUANTITY_CV_PARAM,
DISTRIBUTION_PARAM,
TIME_PARAM,
PROBABILITY_PARAM,
NUM_PARAMS
};
enum InputIds {
QUANTITY_INPUT,
DISTRIBUTION_INPUT,
PING_INPUT,
TIME_INPUT,
PROBABILITY_INPUT,
TRIGGER_INPUT,
NUM_INPUTS
};
enum OutputIds {
TEMPO_OUTPUT,
EOC_OUTPUT,
OUT_OUTPUT,
NUM_OUTPUTS
};
enum LightIds {
ENUMS(QUANTITY_LIGHTS, 16),
TEMPO_LIGHT,
EOC_LIGHT,
OUT_LIGHT,
NUM_LIGHTS
};


dsp::SchmittTrigger pingTrigger; // for detecting Ping in
dsp::SchmittTrigger triggTrigger; // for detecting Trigg in
dsp::BooleanTrigger buttonTrigger; // for detecting when the trigger button is pressed
dsp::ClockDivider ledUpdate; // for only updating LEDs every N samples
const int ledUpdateRate = 16; // LEDs updated every N = 16 samples

PingableClock pingableClock;
BurstEngine burstEngine;
bool includeOriginalTrigger = true;

Burst() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configSwitch(Burst::CYCLE_PARAM, 0.0, 1.0, 0.0, "Mode", {"One-shot", "Cycle"});
auto quantityParam = configParam(Burst::QUANTITY_PARAM, 1, MAX_REPETITIONS, 4, "Number of bursts");
quantityParam->snapEnabled = true;
configButton(Burst::TRIGGER_PARAM, "Manual Trigger");
configParam(Burst::QUANTITY_CV_PARAM, 0.0, 1.0, 1.0, "Quantity CV");
configParam(Burst::DISTRIBUTION_PARAM, -1.0, 1.0, 0.0, "Distribution");
auto timeParam = configParam(Burst::TIME_PARAM, -4.0, 4.0, 0.0, "Time Division/Multiplication");
timeParam->snapEnabled = true;
configParam(Burst::PROBABILITY_PARAM, 0.0, 1.0, 0.0, "Probability", "%", 0.f, -100, 100.);

configInput(QUANTITY_INPUT, "Quantity CV");
configInput(DISTRIBUTION_INPUT, "Distribution");
configInput(PING_INPUT, "Ping");
configInput(TIME_INPUT, "Time Division/Multiplication");
configInput(PROBABILITY_INPUT, "Probability");
configInput(TRIGGER_INPUT, "Trigger");
ledUpdate.setDivision(ledUpdateRate);
}

void process(const ProcessArgs& args) override {

const bool pingReceived = pingTrigger.process(inputs[PING_INPUT].getVoltage());
pingableClock.process(pingReceived, args.sampleTime);

if (ledUpdate.process()) {
updateLEDRing(args);
}

const float quantityCV = params[QUANTITY_CV_PARAM].getValue() * clamp(inputs[QUANTITY_INPUT].getVoltage(), -5.0, +10.f) / 5.f;
const int quantity = clamp((int)(params[QUANTITY_PARAM].getValue() + std::round(16 * quantityCV)), 1, MAX_REPETITIONS);

const bool loop = params[CYCLE_PARAM].getValue();

const float divMultCV = 4.0 * inputs[TIME_INPUT].getVoltage() / 10.f;
const int divMult = -clamp((int)(divMultCV + params[TIME_PARAM].getValue()), -4, +4);

const float distributionCV = inputs[DISTRIBUTION_INPUT].getVoltage() / 10.f;
const float distribution = clamp(distributionCV + params[DISTRIBUTION_PARAM].getValue(), -1.f, +1.f);

const bool triggerInputTriggered = triggTrigger.process(inputs[TRIGGER_INPUT].getVoltage());
const bool triggerButtonTriggered = buttonTrigger.process(params[TRIGGER_PARAM].getValue());
const bool startBurst = triggerInputTriggered || triggerButtonTriggered;

if (startBurst) {
const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f);
const bool inhibitBurst = rack::random::uniform() < prob;

// remember to do at current tempo
burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger);
}

float burstOut, eocOut;
bool eoc;
std::tie(burstOut, eocOut, eoc) = burstEngine.process(args.sampleTime);

// if the burst has finished, we can also re-trigger
if (eoc && loop) {
const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f);
const bool inhibitBurst = rack::random::uniform() < prob;

// remember to do at current tempo
burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger);
}

const bool tempoOutHigh = pingableClock.isTempoOutHigh();
outputs[TEMPO_OUTPUT].setVoltage(10.f * tempoOutHigh);
lights[TEMPO_LIGHT].setBrightnessSmooth(tempoOutHigh, args.sampleTime);

outputs[OUT_OUTPUT].setVoltage(10.f * burstOut);
lights[OUT_LIGHT].setBrightnessSmooth(burstOut, args.sampleTime);

outputs[EOC_OUTPUT].setVoltage(10.f * eocOut);
lights[EOC_LIGHT].setBrightnessSmooth(eocOut, args.sampleTime);
}

void updateLEDRing(const ProcessArgs& args) {
int activeLed;
if (burstEngine.active) {
activeLed = (burstEngine.triggersOccurred - 1) % 16;
}
else {
activeLed = (((int) params[QUANTITY_PARAM].getValue() - 1) % 16);
}
for (int i = 0; i < 16; ++i) {
lights[QUANTITY_LIGHTS + i].setBrightnessSmooth(i == activeLed, args.sampleTime * ledUpdateRate);
}
}

json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "includeOriginalTrigger", json_boolean(includeOriginalTrigger));

return rootJ;
}

void dataFromJson(json_t* rootJ) override {
json_t* includeOriginalTriggerJ = json_object_get(rootJ, "includeOriginalTrigger");
if (includeOriginalTriggerJ) {
includeOriginalTrigger = json_boolean_value(includeOriginalTriggerJ);
}
}
};


struct BurstWidget : ModuleWidget {
BurstWidget(Burst* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/Burst.svg")));

addChild(createWidget<Knurlie>(Vec(15, 0)));
addChild(createWidget<Knurlie>(Vec(15, 365)));

addParam(createParam<BefacoSwitch>(mm2px(Vec(28.44228, 10.13642)), module, Burst::CYCLE_PARAM));
addParam(createParam<Davies1900hWhiteKnobEndless>(mm2px(Vec(9.0322, 16.21467)), module, Burst::QUANTITY_PARAM));
addParam(createParam<BefacoPush>(mm2px(Vec(28.43253, 29.6592)), module, Burst::TRIGGER_PARAM));
addParam(createParam<BefacoTinyKnobLightGrey>(mm2px(Vec(17.26197, 41.95461)), module, Burst::QUANTITY_CV_PARAM));
addParam(createParam<BefacoTinyKnobDarkGrey>(mm2px(Vec(22.85243, 58.45676)), module, Burst::DISTRIBUTION_PARAM));
addParam(createParam<BefacoTinyKnobBlack>(mm2px(Vec(28.47229, 74.91607)), module, Burst::TIME_PARAM));
addParam(createParam<BefacoTinyKnobDarkGrey>(mm2px(Vec(22.75115, 91.35201)), module, Burst::PROBABILITY_PARAM));

addInput(createInput<BananutBlack>(mm2px(Vec(2.02153, 42.27628)), module, Burst::QUANTITY_INPUT));
addInput(createInput<BananutBlack>(mm2px(Vec(7.90118, 58.74959)), module, Burst::DISTRIBUTION_INPUT));
addInput(createInput<BananutBlack>(mm2px(Vec(2.05023, 75.25163)), module, Burst::PING_INPUT));
addInput(createInput<BananutBlack>(mm2px(Vec(13.7751, 75.23049)), module, Burst::TIME_INPUT));
addInput(createInput<BananutBlack>(mm2px(Vec(7.89545, 91.66642)), module, Burst::PROBABILITY_INPUT));
addInput(createInput<BananutBlack>(mm2px(Vec(1.11155, 109.30346)), module, Burst::TRIGGER_INPUT));

addOutput(createOutput<BananutRed>(mm2px(Vec(11.07808, 109.30346)), module, Burst::TEMPO_OUTPUT));
addOutput(createOutput<BananutRed>(mm2px(Vec(21.08452, 109.32528)), module, Burst::EOC_OUTPUT));
addOutput(createOutput<BananutRed>(mm2px(Vec(31.01113, 109.30346)), module, Burst::OUT_OUTPUT));

addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(14.03676, 9.98712)), module, Burst::QUANTITY_LIGHTS + 0));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(18.35846, 10.85879)), module, Burst::QUANTITY_LIGHTS + 1));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(22.05722, 13.31827)), module, Burst::QUANTITY_LIGHTS + 2));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(24.48707, 16.96393)), module, Burst::QUANTITY_LIGHTS + 3));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(25.38476, 21.2523)), module, Burst::QUANTITY_LIGHTS + 4));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(24.48707, 25.5354)), module, Burst::QUANTITY_LIGHTS + 5));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(22.05722, 29.16905)), module, Burst::QUANTITY_LIGHTS + 6));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(18.35846, 31.62236)), module, Burst::QUANTITY_LIGHTS + 7));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(14.03676, 32.48786)), module, Burst::QUANTITY_LIGHTS + 8));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(9.74323, 31.62236)), module, Burst::QUANTITY_LIGHTS + 9));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(6.10149, 29.16905)), module, Burst::QUANTITY_LIGHTS + 10));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(3.68523, 25.5354)), module, Burst::QUANTITY_LIGHTS + 11));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(2.85312, 21.2523)), module, Burst::QUANTITY_LIGHTS + 12));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(3.68523, 16.96393)), module, Burst::QUANTITY_LIGHTS + 13));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(6.10149, 13.31827)), module, Burst::QUANTITY_LIGHTS + 14));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(9.74323, 10.85879)), module, Burst::QUANTITY_LIGHTS + 15));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(14.18119, 104.2831)), module, Burst::TEMPO_LIGHT));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(24.14772, 104.2831)), module, Burst::EOC_LIGHT));
addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(34.11425, 104.2831)), module, Burst::OUT_LIGHT));
}

void appendContextMenu(Menu* menu) override {
Burst* module = dynamic_cast<Burst*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());
menu->addChild(createBoolPtrMenuItem("Include original trigger in output", "", &module->includeOriginalTrigger));
}
};


Model* modelBurst = createModel<Burst, BurstWidget>("Burst");


+ 29
- 26
src/ChowDSP.hpp View File

@@ -225,7 +225,7 @@ typedef TBiquadFilter<> BiquadFilter;
Currently uses an 2*N-th order Butterworth filter.
source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp
*/
template<int N>
template<int N, typename T>
class AAFilter {
public:
AAFilter() = default;
@@ -255,10 +255,10 @@ public:
auto Qs = calculateButterQs(2 * N);
for (int i = 0; i < N; ++i)
filters[i].setParameters(BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
filters[i].setParameters(TBiquadFilter<T>::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
}
inline float process(float x) noexcept {
inline T process(T x) noexcept {
for (int i = 0; i < N; ++i)
x = filters[i].process(x);
@@ -266,14 +266,16 @@ public:
}
private:
BiquadFilter filters[N];
TBiquadFilter<T> filters[N];
};
/**
* Base class for oversampling of any order
* source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp
*/
template<typename T>
class BaseOversampling {
public:
BaseOversampling() = default;
@@ -283,13 +285,13 @@ public:
virtual void reset(float /*baseSampleRate*/) = 0;
/** Upsample a single input sample and update the oversampled buffer */
virtual void upsample(float) noexcept = 0;
virtual void upsample(T) noexcept = 0;
/** Output a downsampled output sample from the current oversampled buffer */
virtual float downsample() noexcept = 0;
virtual T downsample() noexcept = 0;
/** Returns a pointer to the oversampled buffer */
virtual float* getOSBuffer() noexcept = 0;
virtual T* getOSBuffer() noexcept = 0;
};
@@ -305,8 +307,8 @@ public:
float y = oversample.downsample();
@endcode
*/
template<int ratio, int filtN = 4>
class Oversampling : public BaseOversampling {
template<int ratio, int filtN = 4, typename T = float>
class Oversampling : public BaseOversampling<T> {
public:
Oversampling() = default;
virtual ~Oversampling() {}
@@ -317,7 +319,7 @@ public:
std::fill(osBuffer, &osBuffer[ratio], 0.0f);
}
inline void upsample(float x) noexcept override {
inline void upsample(T x) noexcept override {
osBuffer[0] = ratio * x;
std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f);
@@ -325,25 +327,26 @@ public:
osBuffer[k] = aiFilter.process(osBuffer[k]);
}
inline float downsample() noexcept override {
float y = 0.0f;
inline T downsample() noexcept override {
T y = 0.0f;
for (int k = 0; k < ratio; k++)
y = aaFilter.process(osBuffer[k]);
return y;
}
inline float* getOSBuffer() noexcept override {
inline T* getOSBuffer() noexcept override {
return osBuffer;
}
float osBuffer[ratio];
T osBuffer[ratio];
private:
AAFilter<filtN> aaFilter; // anti-aliasing filter
AAFilter<filtN> aiFilter; // anti-imaging filter
AAFilter<filtN, T> aaFilter; // anti-aliasing filter
AAFilter<filtN, T> aiFilter; // anti-imaging filter
};
typedef Oversampling<1, 4, simd::float_4> OversamplingSIMD;
/**
@@ -362,7 +365,7 @@ private:
source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp
*/
template<int filtN = 4>
template<int filtN = 4, typename T = float>
class VariableOversampling {
public:
VariableOversampling() = default;
@@ -384,17 +387,17 @@ public:
}
/** Upsample a single input sample and update the oversampled buffer */
inline void upsample(float x) noexcept {
inline void upsample(T x) noexcept {
oss[osIdx]->upsample(x);
}
/** Output a downsampled output sample from the current oversampled buffer */
inline float downsample() noexcept {
inline T downsample() noexcept {
return oss[osIdx]->downsample();
}
/** Returns a pointer to the oversampled buffer */
inline float* getOSBuffer() noexcept {
inline T* getOSBuffer() noexcept {
return oss[osIdx]->getOSBuffer();
}
@@ -411,12 +414,12 @@ private:
int osIdx = 0;
Oversampling < 1 << 0, filtN > os0; // 1x
Oversampling < 1 << 1, filtN > os1; // 2x
Oversampling < 1 << 2, filtN > os2; // 4x
Oversampling < 1 << 3, filtN > os3; // 8x
Oversampling < 1 << 4, filtN > os4; // 16x
BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
Oversampling < 1 << 0, filtN, T > os0; // 1x
Oversampling < 1 << 1, filtN, T > os1; // 2x
Oversampling < 1 << 2, filtN, T > os2; // 4x
Oversampling < 1 << 3, filtN, T > os3; // 8x
Oversampling < 1 << 4, filtN, T > os4; // 16x
BaseOversampling<T>* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
};
} // namespace chowdsp

+ 804
- 0
src/MidiThing.cpp View File

@@ -0,0 +1,804 @@
#include "plugin.hpp"


/*! \brief Decode System Exclusive messages.
SysEx messages are encoded to guarantee transmission of data bytes higher than
127 without breaking the MIDI protocol. Use this static method to reassemble
your received message.
\param inSysEx The SysEx data received from MIDI in.
\param outData The output buffer where to store the decrypted message.
\param inLength The length of the input buffer.
\param inFlipHeaderBits True for Korg and other who store MSB in reverse order
\return The length of the output buffer.
@see encodeSysEx @see getSysExArrayLength
Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com
*/
unsigned decodeSysEx(const uint8_t* inSysEx,
uint8_t* outData,
unsigned inLength,
bool inFlipHeaderBits) {
unsigned count = 0;
uint8_t msbStorage = 0;
uint8_t byteIndex = 0;

for (unsigned i = 0; i < inLength; ++i) {
if ((i % 8) == 0) {
msbStorage = inSysEx[i];
byteIndex = 6;
}
else {
const uint8_t body = inSysEx[i];
const uint8_t shift = inFlipHeaderBits ? 6 - byteIndex : byteIndex;
const uint8_t msb = uint8_t(((msbStorage >> shift) & 1) << 7);
byteIndex--;
outData[count++] = msb | body;
}
}
return count;
}

struct RoundRobinProcessor {
// if a channel (0 - 11) should be updated, return it's index, otherwise return -1
int process(float sampleTime, float period, int numActiveChannels) {

if (numActiveChannels == 0 || period <= 0) {
return -1;
}

time += sampleTime;

if (time > period) {
time -= period;

// special case: when there's only one channel, the below logic (which looks for when active channel changes)
// wont fire. as we've completed a period, return an "update channel 0" value
if (numActiveChannels == 1) {
return 0;
}
}

int currentActiveChannel = numActiveChannels * time / period;

if (currentActiveChannel != previousActiveChannel) {
previousActiveChannel = currentActiveChannel;
return currentActiveChannel;
}

// if we've got this far, no updates needed (-1)
return -1;
}
private:
float time = 0.f;
int previousActiveChannel = -1;
};


struct MidiThing : Module {
enum ParamId {
REFRESH_PARAM,
PARAMS_LEN
};
enum InputId {
A1_INPUT,
B1_INPUT,
C1_INPUT,
A2_INPUT,
B2_INPUT,
C2_INPUT,
A3_INPUT,
B3_INPUT,
C3_INPUT,
A4_INPUT,
B4_INPUT,
C4_INPUT,
INPUTS_LEN
};
enum OutputId {
OUTPUTS_LEN
};
enum LightId {
LIGHTS_LEN
};
/// Port mode
enum PORTMODE_t {
NOPORTMODE = 0,
MODE10V,
MODEPN5V,
MODENEG10V,
MODE8V,
MODE5V,

LASTPORTMODE
};

const char* cfgPortModeNames[7] = {
"No Mode",
"0/10v",
"-5/5v",
"-10/0v",
"0/8v",
"0/5v",
""
};

const std::vector<float> updateRates = {250., 500., 1000., 2000., 4000., 8000.};
const std::vector<std::string> updateRateNames = {"250 Hz (fewest active channels, slowest, lowest-cpu)", "500 Hz", "1 kHz", "2 kHz", "4 kHz",
"8 kHz (most active channels, fast, highest-cpu)"
};
int updateRateIdx = 2;

// use Pre-def 4 for bridge mode
const static int VCV_BRIDGE_PREDEF = 4;

midi::Output midiOut;
RoundRobinProcessor roundRobinProcessor;

MidiThing() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configButton(REFRESH_PARAM, "");

for (int i = 0; i < NUM_INPUTS; ++i) {
portModes[i] = MODE10V;
configInput(A1_INPUT + i, string::f("Port %d", i + 1));
}
}

void onReset() override {
midiOut.reset();

}

void requestAllChannelsParamsOverSysex() {
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 3; ++col) {
const int PORT_CONFIG = 2;
requestParamOverSysex(row, col, PORT_CONFIG);
}
}
}

// request that MidiThing loads a pre-defined template, 1-4
void setPredef(uint8_t predef) {
predef = clamp(predef, 1, 4);
midi::Message msg;
msg.bytes.resize(8);
// Midi spec is zeroo indexed
uint8_t predefToSend = predef - 1;
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x00, 0x02, 0x00, predefToSend, 0xF7};
midiOut.setChannel(0);
midiOut.sendMessage(msg);
// DEBUG("Predef %d msg request sent: %s", predef, msg.toString().c_str());
}

void setMidiMergeViaSysEx(bool mergeOn) {
midi::Message msg;
msg.bytes.resize(8);

msg.bytes = {0xF0, 0x7D, 0x19, 0x00, 0x05, 0x02, 0x00, (uint8_t) mergeOn, 0xF7};
midiOut.setChannel(0);
midiOut.sendMessage(msg);
// DEBUG("Predef %d msg request sent: %s", mergeOn, msg.toString().c_str());
}


void setVoltageModeOnHardware(uint8_t row, uint8_t col, PORTMODE_t outputMode_) {
uint8_t port = 3 * row + col;
portModes[port] = outputMode_;

midi::Message msg;
msg.bytes.resize(8);
// F0 7D 17 2n 02 02 00 0m F7
// Where n = 0 based port number
// and m is the volt output mode to select from:
msg.bytes = {0xF0, 0x7D, 0x17, static_cast<unsigned char>(32 + port), 0x02, 0x02, 0x00, (uint8_t) portModes[port], 0xF7};
midiOut.sendMessage(msg);
// DEBUG("Voltage mode msg sent: port %d (%d), mode %d", port, static_cast<unsigned char>(32 + port), portModes[port]);
}

void setVoltageModeOnHardware(uint8_t row, uint8_t col) {
setVoltageModeOnHardware(row, col, portModes[3 * row + col]);
}

void syncVcvStateToHardware() {
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 3; ++col) {
setVoltageModeOnHardware(row, col);
}
}
}


midi::InputQueue inputQueue;
void requestParamOverSysex(uint8_t row, uint8_t col, uint8_t mode) {

midi::Message msg;
msg.bytes.resize(8);
// F0 7D 17 00 01 03 00 nm pp F7
uint8_t port = 3 * row + col;
//Where n is:
// 0 = Full configuration request. The module will send only pre def, port functions and modified parameters
// 2 = Send Port configuration
// 4 = Send MIDI Channel configuration
// 6 = Send Voice Configuration

uint8_t n = mode * 16;
uint8_t m = port; // element number: 0-11 port number, 1-16 channel or voice number
uint8_t pp = 2;
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x01, 0x03, 0x00, static_cast<uint8_t>(n + m), pp, 0xF7};
midiOut.sendMessage(msg);
// DEBUG("API request mode msg sent: port %d, pp %s", port, msg.toString().c_str());
}

int getVoltageMode(uint8_t row, uint8_t col) {
// -1 because menu is zero indexed but enum is not
int channel = clamp(3 * row + col, 0, NUM_INPUTS - 1);
return portModes[channel] - 1;
}

const static int NUM_INPUTS = 12;
bool isClipping[NUM_INPUTS] = {};

bool checkIsVoltageWithinRange(uint8_t channel, float voltage) {
const float tol = 0.001;
switch (portModes[channel]) {
case MODE10V: return 0 - tol < voltage && voltage < 10 + tol;
case MODEPN5V: return -5 - tol < voltage && voltage < 5 + tol;
case MODENEG10V: return -10 - tol < voltage && voltage < 0 + tol;
case MODE8V: return 0 - tol < voltage && voltage < 8 + tol;
case MODE5V: return 0 - tol < voltage && voltage < 5 + tol;
default: return false;
}
}

uint16_t rescaleVoltageForChannel(uint8_t channel, float voltage) {
switch (portModes[channel]) {
case MODE10V: return rescale(clamp(voltage, 0.f, 10.f), 0.f, +10.f, 0, 16383);
case MODEPN5V: return rescale(clamp(voltage, -5.f, 5.f), -5.f, +5.f, 0, 16383);
case MODENEG10V: return rescale(clamp(voltage, -10.f, 0.f), -10.f, +0.f, 0, 16383);
case MODE8V: return rescale(clamp(voltage, 0.f, 8.f), 0.f, +8.f, 0, 16383);
case MODE5V: return rescale(clamp(voltage, 0.f, 5.f), 0.f, +5.f, 0, 16383);
default: return 0;
}
}

// one way sync (VCV -> hardware) for now
void doSync() {
// switch to VCV template (predef 4)
setPredef(4);

// disable MIDI merge (otherwise large sample rates will not work)
setMidiMergeViaSysEx(false);

// send full VCV config
syncVcvStateToHardware();

// disabled for now, but this would request what state the hardware is in
if (parseSysExMessagesFromHardware) {
requestAllChannelsParamsOverSysex();
}
}

// debug only
bool parseSysExMessagesFromHardware = false;
int numActiveChannels = 0;
dsp::BooleanTrigger buttonTrigger;
dsp::Timer rateLimiterTimer;
PORTMODE_t portModes[NUM_INPUTS] = {};
void process(const ProcessArgs& args) override {

if (buttonTrigger.process(params[REFRESH_PARAM].getValue())) {
doSync();
}

// disabled for now, but this is how VCV would read SysEx coming from the hardware (if requested above)
if (parseSysExMessagesFromHardware) {
midi::Message msg;
uint8_t outData[32] = {};
while (inputQueue.tryPop(&msg, args.frame)) {

uint8_t outLen = decodeSysEx(&msg.bytes[0], outData, msg.bytes.size(), false);
if (outLen > 3) {

int channel = (outData[2] & 0x0f) >> 0;

if (channel >= 0 && channel < NUM_INPUTS) {
if (outData[outLen - 1] < LASTPORTMODE) {
portModes[channel] = (PORTMODE_t) outData[outLen - 1];
}
}
}
}
}

std::vector<int> activeChannels;
for (int c = 0; c < NUM_INPUTS; ++c) {
if (inputs[A1_INPUT + c].isConnected()) {
activeChannels.push_back(c);
}
}
numActiveChannels = activeChannels.size();
// we're done if no channels are active
if (numActiveChannels == 0) {
return;
}

//DEBUG("updateRateIdx: %d", updateRateIdx);
const float updateRateHz = updateRates[updateRateIdx];
//DEBUG("updateRateHz: %f", updateRateHz);
const int maxCCMessagesPerSecondPerChannel = updateRateHz / numActiveChannels;

// MIDI baud rate is 31250 b/s, or 3125 B/s.
// CC messages are 3 bytes, so we can send a maximum of 1041 CC messages per second.
// The refresh rate period (i.e. how often we can send X channels of data is:
const float rateLimiterPeriod = 1.f / maxCCMessagesPerSecondPerChannel;

// this returns -1 if no channel should be updated, or the index of the channel that should be updated
// it distributes update times in a round robin fashion
int channelIdxToUpdate = roundRobinProcessor.process(args.sampleTime, rateLimiterPeriod, numActiveChannels);

if (channelIdxToUpdate >= 0 && channelIdxToUpdate < numActiveChannels) {
int c = activeChannels[channelIdxToUpdate];

const float channelVoltage = inputs[A1_INPUT + c].getVoltage();
uint16_t pw = rescaleVoltageForChannel(c, channelVoltage);
isClipping[c] = !checkIsVoltageWithinRange(c, channelVoltage);
midi::Message m;
m.setStatus(0xe);
m.setNote(pw & 0x7f);
m.setValue((pw >> 7) & 0x7f);
m.setFrame(args.frame);

midiOut.setChannel(c);
midiOut.sendMessage(m);
}
}


json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "midiOutput", midiOut.toJson());
json_object_set_new(rootJ, "inputQueue", inputQueue.toJson());
json_object_set_new(rootJ, "updateRateIdx", json_integer(updateRateIdx));

for (int c = 0; c < NUM_INPUTS; ++c) {
json_object_set_new(rootJ, string::f("portMode%d", c).c_str(), json_integer(portModes[c]));
}

return rootJ;
}

void dataFromJson(json_t* rootJ) override {
json_t* midiOutputJ = json_object_get(rootJ, "midiOutput");
if (midiOutputJ) {
midiOut.fromJson(midiOutputJ);
}

json_t* midiInputQueueJ = json_object_get(rootJ, "inputQueue");
if (midiInputQueueJ) {
inputQueue.fromJson(midiInputQueueJ);
}

json_t* updateRateIdxJ = json_object_get(rootJ, "updateRateIdx");
if (updateRateIdxJ) {
updateRateIdx = json_integer_value(updateRateIdxJ);
}

for (int c = 0; c < NUM_INPUTS; ++c) {
json_t* portModeJ = json_object_get(rootJ, string::f("portMode%d", c).c_str());
if (portModeJ) {
portModes[c] = (PORTMODE_t)json_integer_value(portModeJ);
}
}

// requestAllChannelsParamsOverSysex();
syncVcvStateToHardware();
}
};

struct MidiThingPort : BefacoInputPort {
int row = 0, col = 0;
MidiThing* module;

void appendContextMenu(Menu* menu) override {

menu->addChild(new MenuSeparator());
std::string label = string::f("Voltage Mode Port %d", 3 * row + col + 1);

menu->addChild(createIndexSubmenuItem(label,
{"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"},
[ = ]() {
return module->getVoltageMode(row, col);
},
[ = ](int modeIdx) {
MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(modeIdx + 1);
module->setVoltageModeOnHardware(row, col, mode);
}
));

/*
menu->addChild(createIndexSubmenuItem("Get Port Info",
{"Full", "Port", "MIDI", "Voice"},
[ = ]() {
return -1;
},
[ = ](int mode) {
module->requestParamOverSysex(row, col, 2 * mode);
}
));
*/
}
};

// dervied from https://github.com/countmodula/VCVRackPlugins/blob/v2.0.0/src/components/CountModulaLEDDisplay.hpp
struct LEDDisplay : LightWidget {
float fontSize = 9;
Vec textPos = Vec(1, 13);
int numChars = 7;
int row = 0, col = 0;
MidiThing* module;

LEDDisplay() {
box.size = mm2px(Vec(9.298, 5.116));
}

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(0x20, 0x20, 0x20);
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(0x20, 0x20, 0x20);
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
NVGcolor textColor = nvgRGB(0xff, 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);

if (module) {
const bool isClipping = module->isClipping[col + row * 3];
if (isClipping) {
borderColor = nvgRGB(0xff, 0x20, 0x20);
}
}

nvgStrokeColor(args.vg, borderColor);
nvgStroke(args.vg);

std::shared_ptr<Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));

if (font && font->handle >= 0) {

std::string text = "?-?v"; // fallback if module not yet defined
if (module) {
text = module->cfgPortModeNames[module->getVoltageMode(row, col) + 1];
}
char buffer[numChars + 1];
int l = text.size();
if (l > numChars)
l = numChars;

nvgGlobalTint(args.vg, color::WHITE);

text.copy(buffer, l);
buffer[l] = '\0';

nvgFontSize(args.vg, fontSize);
nvgFontFaceId(args.vg, font->handle);
nvgFillColor(args.vg, textColor);
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
NVGtextRow textRow;
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
nvgTextBox(args.vg, textPos.x, textPos.y, box.size.x, textRow.start, textRow.end);
}
}

void onButton(const ButtonEvent& e) override {
if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) {
ui::Menu* menu = createMenu();

menu->addChild(createMenuLabel(string::f("Voltage mode port %d:", col + 3 * row + 1)));

const std::string labels[5] = {"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"};

for (int i = 0; i < 5; ++i) {
menu->addChild(createCheckMenuItem(labels[i], "",
[ = ]() {
return module->getVoltageMode(row, col) == i;
},
[ = ]() {
MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(i + 1);
module->setVoltageModeOnHardware(row, col, mode);
}
));
}

e.consume(this);
return;
}

LightWidget::onButton(e);
}

};


struct MidiThingWidget : ModuleWidget {

struct LedDisplayCenterChoiceEx : LedDisplayChoice {
LedDisplayCenterChoiceEx() {
box.size = mm2px(math::Vec(0, 8.0));
color = nvgRGB(0xf0, 0xf0, 0xf0);
bgColor = nvgRGBAf(0, 0, 0, 0);
textOffset = math::Vec(0, 16);
}

void drawLayer(const DrawArgs& args, int layer) override {
nvgScissor(args.vg, RECT_ARGS(args.clipBox));
if (layer == 1) {
if (bgColor.a > 0.0) {
nvgBeginPath(args.vg);
nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
nvgFillColor(args.vg, bgColor);
nvgFill(args.vg);
}

std::shared_ptr<window::Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));

if (font && font->handle >= 0 && !text.empty()) {
nvgFillColor(args.vg, color);
nvgFontFaceId(args.vg, font->handle);
nvgTextLetterSpacing(args.vg, -0.6f);
nvgFontSize(args.vg, 10);
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
NVGtextRow textRow;
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
nvgTextBox(args.vg, textOffset.x, textOffset.y, box.size.x, textRow.start, textRow.end);
}
}
nvgResetScissor(args.vg);
}
};


struct MidiDriverItem : ui::MenuItem {
midi::Port* port;
int driverId;
void onAction(const event::Action& e) override {
port->setDriverId(driverId);
}
};

struct MidiDriverChoice : LedDisplayCenterChoiceEx {
midi::Port* port;
void onAction(const event::Action& e) override {
if (!port)
return;
createContextMenu();
}

virtual ui::Menu* createContextMenu() {
ui::Menu* menu = createMenu();
menu->addChild(createMenuLabel("MIDI driver"));
for (int driverId : midi::getDriverIds()) {
MidiDriverItem* item = new MidiDriverItem;
item->port = port;
item->driverId = driverId;
item->text = midi::getDriver(driverId)->getName();
item->rightText = CHECKMARK(item->driverId == port->driverId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = port ? port->getDriver()->getName() : "";
if (text.empty()) {
text = "(No driver)";
color.a = 0.5f;
}
else {
color.a = 1.f;
}
}
};

struct MidiDeviceItem : ui::MenuItem {
midi::Port* outPort, *inPort;
int deviceId;
void onAction(const event::Action& e) override {
outPort->setDeviceId(deviceId);
inPort->setDeviceId(deviceId);
}
};

struct MidiDeviceChoice : LedDisplayCenterChoiceEx {
midi::Port* outPort, *inPort;
void onAction(const event::Action& e) override {
if (!outPort || !inPort)
return;
createContextMenu();
}

virtual ui::Menu* createContextMenu() {
ui::Menu* menu = createMenu();
menu->addChild(createMenuLabel("MIDI device"));
{
MidiDeviceItem* item = new MidiDeviceItem;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = -1;
item->text = "(No device)";
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
for (int deviceId : outPort->getDeviceIds()) {
MidiDeviceItem* item = new MidiDeviceItem;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = deviceId;
item->text = outPort->getDeviceName(deviceId);
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = outPort ? outPort->getDeviceName(outPort->deviceId) : "";
if (text.empty()) {
text = "(No device)";
color.a = 0.5f;
}
else {
color.a = 1.f;
}
}
};

struct MidiWidget : LedDisplay {
MidiDriverChoice* driverChoice;
LedDisplaySeparator* driverSeparator;
MidiDeviceChoice* deviceChoice;
LedDisplaySeparator* deviceSeparator;

void setMidiPorts(midi::Port* outPort, midi::Port* inPort) {

clearChildren();
math::Vec pos;

MidiDriverChoice* driverChoice = createWidget<MidiDriverChoice>(pos);
driverChoice->box.size = Vec(box.size.x, 20.f);
//driverChoice->textOffset = Vec(6.f, 14.7f);
driverChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
driverChoice->port = outPort;

addChild(driverChoice);
pos = driverChoice->box.getBottomLeft();
this->driverChoice = driverChoice;

this->driverSeparator = createWidget<LedDisplaySeparator>(pos);
this->driverSeparator->box.size.x = box.size.x;
addChild(this->driverSeparator);

MidiDeviceChoice* deviceChoice = createWidget<MidiDeviceChoice>(pos);
deviceChoice->box.size = Vec(box.size.x, 21.f);
//deviceChoice->textOffset = Vec(6.f, 14.7f);
deviceChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
deviceChoice->outPort = outPort;
deviceChoice->inPort = inPort;
addChild(deviceChoice);
pos = deviceChoice->box.getBottomLeft();
this->deviceChoice = deviceChoice;
}
};


MidiThingWidget(MidiThing* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MidiThing.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

MidiWidget* midiInputWidget = createWidget<MidiWidget>(Vec(1.5f, 36.4f)); //mm2px(Vec(0.5f, 10.f)));
midiInputWidget->box.size = mm2px(Vec(5.08 * 6 - 1, 13.5f));
if (module) {
midiInputWidget->setMidiPorts(&module->midiOut, &module->inputQueue);
}
else {
midiInputWidget->setMidiPorts(nullptr, nullptr);
}
addChild(midiInputWidget);

addParam(createParamCentered<BefacoButton>(mm2px(Vec(21.12, 57.32)), module, MidiThing::REFRESH_PARAM));

const float xStartLed = 0.2 + 0.628;
const float yStartLed = 28.019;

for (int row = 0; row < 4; row++) {
for (int col = 0; col < 3; col++) {

LEDDisplay* display = createWidget<LEDDisplay>(mm2px(Vec(xStartLed + 9.751 * col, yStartLed + 5.796 * row)));
display->module = module;
display->row = row;
display->col = col;
addChild(display);

auto input = createInputCentered<MidiThingPort>(mm2px(Vec(5.08 + 10 * col, 69.77 + 14.225 * row)), module, MidiThing::A1_INPUT + 3 * row + col);
input->row = row;
input->col = col;
input->module = module;
addInput(input);


}
}
}

void appendContextMenu(Menu* menu) override {
MidiThing* module = dynamic_cast<MidiThing*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());

menu->addChild(createSubmenuItem("Select MIDI Device", "",
[ = ](Menu * menu) {

for (auto driverId : rack::midi::getDriverIds()) {
midi::Driver* driver = midi::getDriver(driverId);
const bool activeDriver = module->midiOut.getDriverId() == driverId;

menu->addChild(createSubmenuItem(driver->getName(), CHECKMARK(activeDriver),
[ = ](Menu * menu) {

for (auto deviceId : driver->getOutputDeviceIds()) {
const bool activeDevice = activeDriver && module->midiOut.getDeviceId() == deviceId;

menu->addChild(createMenuItem(driver->getOutputDeviceName(deviceId),
CHECKMARK(activeDevice),
[ = ]() {
module->midiOut.setDriverId(driverId);
module->midiOut.setDeviceId(deviceId);

module->inputQueue.setDriverId(driverId);
module->inputQueue.setDeviceId(deviceId);
module->inputQueue.setChannel(0); // TODO update

module->doSync();

// DEBUG("Updating Output MIDI settings - driver: %s, device: %s",
// driver->getName().c_str(), driver->getOutputDeviceName(deviceId).c_str());
}));
}
}));
}
}));

menu->addChild(createIndexPtrSubmenuItem("All channels MIDI update rate",
module->updateRateNames,
&module->updateRateIdx));

float updateRate = module->updateRates[module->updateRateIdx] / module->numActiveChannels;
menu->addChild(createMenuLabel(string::f("Per-channel MIDI update rate: %.3g Hz", updateRate)));
}
};


Model* modelMidiThing = createModel<MidiThing, MidiThingWidget>("MidiThingV2");

+ 336
- 0
src/Octaves.cpp View File

@@ -0,0 +1,336 @@
#include "plugin.hpp"
#include "ChowDSP.hpp"

using namespace simd;

struct Octaves : Module {
enum ParamId {
PWM_CV_PARAM,
OCTAVE_PARAM,
TUNE_PARAM,
PWM_PARAM,
RANGE_PARAM,
GAIN_01F_PARAM,
GAIN_02F_PARAM,
GAIN_04F_PARAM,
GAIN_08F_PARAM,
GAIN_16F_PARAM,
GAIN_32F_PARAM,
PARAMS_LEN
};
enum InputId {
VOCT1_INPUT,
VOCT2_INPUT,
SYNC_INPUT,
PWM_INPUT,
GAIN_01F_INPUT,
GAIN_02F_INPUT,
GAIN_04F_INPUT,
GAIN_08F_INPUT,
GAIN_16F_INPUT,
GAIN_32F_INPUT,
INPUTS_LEN
};
enum OutputId {
OUT_01F_OUTPUT,
OUT_02F_OUTPUT,
OUT_04F_OUTPUT,
OUT_08F_OUTPUT,
OUT_16F_OUTPUT,
OUT_32F_OUTPUT,
OUTPUTS_LEN
};
enum LightId {
LIGHTS_LEN
};

bool limitPW = true;
bool removePulseDC = false;
bool useTriangleCore = false;
static const int NUM_OUTPUTS = 6;
const float ranges[3] = {4.f, 1.f, 1.f / 12.f}; // full, octave, semitone

float_4 phase[4] = {}; // phase for core waveform, in [0, 1]
chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling

DCBlockerT<2, float_4> blockDCFilter[NUM_OUTPUTS][4]; // optionally block DC with RC filter @ ~22 Hz
dsp::TSchmittTrigger<float_4> syncTrigger[4]; // for hard sync

Octaves() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configParam(PWM_CV_PARAM, 0.f, 1.f, 1.f, "PWM CV attenuater");

auto octParam = configSwitch(OCTAVE_PARAM, 0.f, 6.f, 1.f, "Octave", {"C1", "C2", "C3", "C4", "C5", "C6", "C7"});
octParam->snapEnabled = true;

configParam(TUNE_PARAM, -1.f, 1.f, 0.f, "Tune");
configParam(PWM_PARAM, 0.5f, 0.f, 0.5f, "PWM");
auto rangeParam = configSwitch(RANGE_PARAM, 0.f, 2.f, 1.f, "Range", {"VCO: Full", "VCO: Octave", "VCO: Semitone"});
rangeParam->snapEnabled = true;

configParam(GAIN_01F_PARAM, 0.f, 1.f, 1.00f, "Gain Fundamental");
configParam(GAIN_02F_PARAM, 0.f, 1.f, 0.75f, "Gain x2 Fundamental");
configParam(GAIN_04F_PARAM, 0.f, 1.f, 0.50f, "Gain x4 Fundamental");
configParam(GAIN_08F_PARAM, 0.f, 1.f, 0.25f, "Gain x8 Fundamental");
configParam(GAIN_16F_PARAM, 0.f, 1.f, 0.f, "Gain x16 Fundamental");
configParam(GAIN_32F_PARAM, 0.f, 1.f, 0.f, "Gain x32 Fundamental");

configInput(VOCT1_INPUT, "V/Octave 1");
configInput(VOCT2_INPUT, "V/Octave 2");
configInput(SYNC_INPUT, "Sync");
configInput(PWM_INPUT, "PWM");
configInput(GAIN_01F_INPUT, "Gain x1F CV");
configInput(GAIN_02F_INPUT, "Gain x1F CV");
configInput(GAIN_04F_INPUT, "Gain x1F CV");
configInput(GAIN_08F_INPUT, "Gain x1F CV");
configInput(GAIN_16F_INPUT, "Gain x1F CV");
configInput(GAIN_32F_INPUT, "Gain x1F CV");

configOutput(OUT_01F_OUTPUT, "x1F");
configOutput(OUT_02F_OUTPUT, "x2F");
configOutput(OUT_04F_OUTPUT, "x4F");
configOutput(OUT_08F_OUTPUT, "x8F");
configOutput(OUT_16F_OUTPUT, "x16F");
configOutput(OUT_32F_OUTPUT, "x32F");

// calculate up/downsampling rates
onSampleRateChange();
}

void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
for (int c = 0; c < NUM_OUTPUTS; c++) {
for (int i = 0; i < 4; i++) {
oversampler[c][i].setOversamplingIndex(oversamplingIndex);
oversampler[c][i].reset(sampleRate);
blockDCFilter[c][i].setFrequency(22.05 / sampleRate);
}
}
}


void process(const ProcessArgs& args) override {

const int numActivePolyphonyEngines = getNumActivePolyphonyEngines();

// work out active outputs
const std::vector<int> connectedOutputs = getConnectedOutputs();
if (connectedOutputs.size() == 0) {
return;
}
// only process up to highest active channel
const int highestOutput = *std::max_element(connectedOutputs.begin(), connectedOutputs.end());

for (int c = 0; c < numActivePolyphonyEngines; c += 4) {

const int rangeIndex = params[RANGE_PARAM].getValue();
float_4 pitch = ranges[rangeIndex] * params[TUNE_PARAM].getValue() + inputs[VOCT1_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[VOCT2_INPUT].getPolyVoltageSimd<float_4>(c);
pitch += params[OCTAVE_PARAM].getValue() - 3;
const float_4 freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
// -1 to +1
const float_4 pwmCV = params[PWM_CV_PARAM].getValue() * clamp(inputs[PWM_INPUT].getPolyVoltageSimd<float_4>(c) / 10.f, -1.f, 1.f);
const float_4 pulseWidthLimit = limitPW ? 0.05f : 0.0f;

// pwm in [-0.25 : +0.25]
const float_4 pwm = 2 * clamp(0.5 - params[PWM_PARAM].getValue() + 0.5 * pwmCV, -0.5f + pulseWidthLimit, 0.5f - pulseWidthLimit);

const int oversamplingRatio = oversampler[0][0].getOversamplingRatio();

const float_4 deltaPhase = freq * args.sampleTime / oversamplingRatio;

// process sync
float_4 sync = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c));
phase[c / 4] = simd::ifelse(sync, 0.5f, phase[c / 4]);


for (int i = 0; i < oversamplingRatio; i++) {

phase[c / 4] += deltaPhase;
phase[c / 4] -= simd::floor(phase[c / 4]);

float_4 sum = {};
for (int oct = 0; oct <= highestOutput; oct++) {

const float_4 gainCV = simd::clamp(inputs[GAIN_01F_INPUT + oct].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f, 0.f, 1.0f);
const float_4 gain = params[GAIN_01F_PARAM + oct].getValue() * gainCV;

// don't bother processing if gain is zero and no output is connected
const bool isGainZero = simd::movemask(gain != 0.f) == 0;
if (isGainZero && !outputs[OUT_01F_OUTPUT + oct].isConnected()) {
continue;
}

// derive phases for higher octaves from base phase (this keeps things in sync!)
const float_4 n = (float)(1 << oct);
// this is on [0, 1]
const float_4 effectivePhase = n * simd::fmod(phase[c / 4], 1 / n);
const float_4 waveTri = 1.0 - 2.0 * simd::abs(2.f * effectivePhase - 1.0);
// build square from triangle + comparator
const float_4 waveSquare = simd::ifelse(waveTri > pwm, +1.f, -1.f);

sum += (useTriangleCore ? waveTri : waveSquare) * gain;
sum = clamp(sum, -1.f, 1.f);

if (outputs[OUT_01F_OUTPUT + oct].isConnected()) {
oversampler[oct][c/4].getOSBuffer()[i] = sum;
sum = 0.f;

// DEBUG("here %f %f %f %f %f", phase[c/4][0], waveTri[0], sum[0], gain[0], gainCV[0]);
}


}

} // end of oversampling loop

// only downsample required channels
for (int oct = 0; oct <= highestOutput; oct++) {
if (outputs[OUT_01F_OUTPUT + oct].isConnected()) {

// downsample (if required)
float_4 out = (oversamplingRatio > 1) ? oversampler[oct][c/4].downsample() : oversampler[oct][c/4].getOSBuffer()[0];
if (removePulseDC) {
out = blockDCFilter[oct][c/4].process(out);
}

outputs[OUT_01F_OUTPUT + oct].setVoltageSimd(5.f * out, c);
}
}
} // end of polyphony loop

for (int connectedOutput : connectedOutputs) {
outputs[OUT_01F_OUTPUT + connectedOutput].setChannels(numActivePolyphonyEngines);
}
}

// polyphony is defined by the largest number of active channels on voct, pwm or gain inputs
int getNumActivePolyphonyEngines() {
int activePolyphonyEngines = 1;
for (int c = 0; c < NUM_OUTPUTS; c++) {
if (inputs[GAIN_01F_INPUT + c].isConnected()) {
activePolyphonyEngines = std::max(activePolyphonyEngines, inputs[GAIN_01F_INPUT + c].getChannels());
}
}
activePolyphonyEngines = std::max({activePolyphonyEngines, inputs[VOCT1_INPUT].getChannels(), inputs[VOCT2_INPUT].getChannels()});
activePolyphonyEngines = std::max(activePolyphonyEngines, inputs[PWM_INPUT].getChannels());

return activePolyphonyEngines;
}

std::vector<int> getConnectedOutputs() {
std::vector<int> connectedOutputs;
for (int c = 0; c < NUM_OUTPUTS; c++) {
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
connectedOutputs.push_back(c);
}
}
return connectedOutputs;
}

json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0][0].getOversamplingIndex()));
json_object_set_new(rootJ, "useTriangleCore", json_boolean(useTriangleCore));

return rootJ;
}

void dataFromJson(json_t* rootJ) override {

json_t* removePulseDCJ = json_object_get(rootJ, "removePulseDC");
if (removePulseDCJ) {
removePulseDC = json_boolean_value(removePulseDCJ);
}

json_t* limitPWJ = json_object_get(rootJ, "limitPW");
if (limitPWJ) {
limitPW = json_boolean_value(limitPWJ);
}

json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
if (oversamplingIndexJ) {
oversamplingIndex = json_integer_value(oversamplingIndexJ);
onSampleRateChange();
}

json_t* useTriangleCoreJ = json_object_get(rootJ, "useTriangleCore");
if (useTriangleCoreJ) {
useTriangleCore = json_boolean_value(useTriangleCoreJ);
}
}
};

struct OctavesWidget : ModuleWidget {
OctavesWidget(Octaves* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Octaves.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(52.138, 15.037)), module, Octaves::PWM_CV_PARAM));
addParam(createParam<CKSSVert7>(mm2px(Vec(22.171, 30.214)), module, Octaves::OCTAVE_PARAM));
addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(10.264, 33.007)), module, Octaves::TUNE_PARAM));
addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM));
addParam(createParam<CKSSThreeHorizontal>(mm2px(Vec(6.023, 48.937)), module, Octaves::RANGE_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(2.9830, 60.342)), module, Octaves::GAIN_01F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(12.967, 60.342)), module, Octaves::GAIN_02F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(22.951, 60.342)), module, Octaves::GAIN_04F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(32.936, 60.342)), module, Octaves::GAIN_08F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(42.920, 60.342)), module, Octaves::GAIN_16F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(52.905, 60.342)), module, Octaves::GAIN_32F_PARAM));

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.247, 15.181)), module, Octaves::VOCT1_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.282, 15.181)), module, Octaves::VOCT2_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.316, 15.181)), module, Octaves::SYNC_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(37.092, 15.135)), module, Octaves::PWM_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.247, 100.492)), module, Octaves::GAIN_01F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.282, 100.492)), module, Octaves::GAIN_02F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.316, 100.492)), module, Octaves::GAIN_04F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.35, 100.492)), module, Octaves::GAIN_08F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(45.384, 100.492)), module, Octaves::GAIN_16F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(55.418, 100.492)), module, Octaves::GAIN_32F_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.247, 113.508)), module, Octaves::OUT_01F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.282, 113.508)), module, Octaves::OUT_02F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 113.508)), module, Octaves::OUT_04F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.35, 113.508)), module, Octaves::OUT_08F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(45.384, 113.508)), module, Octaves::OUT_16F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(55.418, 113.508)), module, Octaves::OUT_32F_OUTPUT));

}

void appendContextMenu(Menu* menu) override {
Octaves* module = dynamic_cast<Octaves*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());
menu->addChild(createSubmenuItem("Hardware compatibility", "",
[ = ](Menu * menu) {
menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
menu->addChild(createBoolPtrMenuItem("Remove pulse DC", "", &module->removePulseDC));
menu->addChild(createBoolPtrMenuItem("Use triangle core", "", &module->useTriangleCore));
}
));

menu->addChild(createIndexSubmenuItem("Oversampling",
{"Off", "x2", "x4", "x8"},
[ = ]() {
return module->oversamplingIndex;
},
[ = ](int mode) {
module->oversamplingIndex = mode;
module->onSampleRateChange();
}
));

}
};

Model* modelOctaves = createModel<Octaves, OctavesWidget>("Octaves");

+ 154
- 164
src/PonyVCO.cpp View File

@@ -1,6 +1,7 @@
#include "plugin.hpp"
#include "ChowDSP.hpp"

using simd::float_4;

// references:
// * "REDUCING THE ALIASING OF NONLINEAR WAVESHAPING USING CONTINUOUS-TIME CONVOLUTION" (https://www.dafx.de/paper-archive/2016/dafxpapers/20-DAFx-16_paper_41-PN.pdf)
@@ -8,46 +9,27 @@
// * https://ccrma.stanford.edu/~jatin/Notebooks/adaa.html
// * Pony waveshape https://www.desmos.com/calculator/1kvahyl4ti

template<typename T>
class FoldStage1 {
public:

float process(float x, float xt) {
float y;
T process(T x, T xt) {
T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5,
f(0.5 * (xPrev + x), xt),
(F(x, xt) - F(xPrev, xt)) / (x - xPrev));

if (fabs(x - xPrev) < 1e-5) {
y = f(0.5 * (xPrev + x), xt);
}
else {
y = (F(x, xt) - F(xPrev, xt)) / (x - xPrev);
}
xPrev = x;
return y;
}

// xt - threshold x
static float f(float x, float xt) {
if (x > xt) {
return +5 * xt - 4 * x;
}
else if (x < -xt) {
return -5 * xt - 4 * x;
}
else {
return x;
}
static T f(T x, T xt) {
return simd::ifelse(x > xt, +5 * xt - 4 * x, simd::ifelse(x < -xt, -5 * xt - 4 * x, x));
}

static float F(float x, float xt) {
if (x > xt) {
return 5 * xt * x - 2 * x * x - 2.5 * xt * xt;
}
else if (x < -xt) {
return -5 * xt * x - 2 * x * x - 2.5 * xt * xt;

}
else {
return x * x / 2.f;
}
static T F(T x, T xt) {
return simd::ifelse(x > xt, 5 * xt * x - 2 * x * x - 2.5 * xt * xt,
simd::ifelse(x < -xt, -5 * xt * x - 2 * x * x - 2.5 * xt * xt, x * x / 2.f));
}

void reset() {
@@ -55,55 +37,29 @@ public:
}

private:
float xPrev = 0.f;
T xPrev = 0.f;
};

template<typename T>
class FoldStage2 {
public:
float process(float x) {
float y;

if (fabs(x - xPrev) < 1e-5) {
y = f(0.5 * (xPrev + x));
}
else {
y = (F(x) - F(xPrev)) / (x - xPrev);
}
T process(T x) {
const T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5, f(0.5 * (xPrev + x)), (F(x) - F(xPrev)) / (x - xPrev));
xPrev = x;
return y;
}

static float f(float x) {
if (-(x + 2) > c) {
return c;
}
else if (x < -1) {
return -(x + 2);
}
else if (x < 1) {
return x;
}
else if (-x + 2 > -c) {
return -x + 2;
}
else {
return -c;
}
static T f(T x) {
return simd::ifelse(-(x + 2) > c, c, simd::ifelse(x < -1, -(x + 2), simd::ifelse(x < 1, x, simd::ifelse(-x + 2 > -c, -x + 2, -c))));
}

static float F(float x) {
if (x < 0) {
return F(-x);
}
else if (x < 1) {
return x * x * 0.5;
}
else if (x < 2 + c) {
return 2 * x * (1.f - x * 0.25f) - 1.f;
}
else {
return 2 * (2 + c) * (1 - (2 + c) * 0.25f) - 1.f - c * (x - 2 - c);
}
static T F(T x) {
return simd::ifelse(x > 0, F_signed(x), F_signed(-x));
}

static T F_signed(T x) {
return simd::ifelse(x < 1, x * x * 0.5, simd::ifelse(x < 2.f + c, 2.f * x * (1.f - x * 0.25f) - 1.f,
2.f * (2.f + c) * (1.f - (2.f + c) * 0.25f) - 1.f - c * (x - 2.f - c)));
}

void reset() {
@@ -111,8 +67,8 @@ public:
}

private:
float xPrev = 0.f;
static constexpr float c = 0.1;
T xPrev = 0.f;
static constexpr float c = 0.1f;
};


@@ -148,10 +104,10 @@ struct PonyVCO : Module {
};

float range[4] = {8.f, 1.f, 1.f / 12.f, 10.f};
chowdsp::VariableOversampling<6> oversampler; // uses a 2*6=12th order Butterworth filter
chowdsp::VariableOversampling<6, float_4> oversampler[4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling

dsp::RCFilter blockTZFMDCFilter;
dsp::TRCFilter<float_4> blockTZFMDCFilter[4];
bool blockTZFMDC = true;

// hardware doesn't limit PW but some user might want to (to 5%->95%)
@@ -160,10 +116,10 @@ struct PonyVCO : Module {
// hardware has DC for non-50% duty cycle, optionally add/remove it
bool removePulseDC = true;

dsp::SchmittTrigger syncTrigger;
dsp::TSchmittTrigger<float_4> syncTrigger[4];

FoldStage1 stage1;
FoldStage2 stage2;
FoldStage1<float_4> stage1[4];
FoldStage2<float_4> stage2[4];

PonyVCO() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
@@ -191,22 +147,21 @@ struct PonyVCO : Module {

void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
blockTZFMDCFilter.setCutoffFreq(5.0 / sampleRate);
oversampler.setOversamplingIndex(oversamplingIndex);
oversampler.reset(sampleRate);
for (int c = 0; c < 4; c++) {
blockTZFMDCFilter[c].setCutoffFreq(5.0 / sampleRate);
oversampler[c].setOversamplingIndex(oversamplingIndex);
oversampler[c].reset(sampleRate);

stage1.reset();
stage2.reset();
stage1[c].reset();
stage2[c].reset();
}
}

// implementation taken from "Alias-Suppressed Oscillators Based on Differentiated Polynomial Waveforms",
// also the notes from Surge Synthesier repo:
// https://github.com/surge-synthesizer/surge/blob/09f1ec8e103265bef6fc0d8a0fc188238197bf8c/src/common/dsp/oscillators/ModernOscillator.cpp#L19
// Calculation is performed at double precision, as the differencing equations appeared to work poorly with only float.

double phase = 0.0; // phase at current (sub)sample
double phases[3] = {}; // phase as extrapolated to the current and two previous samples
double sawBuffer[3] = {}, sawOffsetBuff[3] = {}, triBuffer[3] = {}; // buffers for storing the terms in the difference equation
float_4 phase[4] = {}; // phase at current (sub)sample

void process(const ProcessArgs& args) override {

@@ -216,130 +171,160 @@ struct PonyVCO : Module {
const Waveform waveform = (Waveform) params[WAVE_PARAM].getValue();
const float mult = lfoMode ? 1.0 : dsp::FREQ_C4;
const float baseFreq = std::pow(2, (int)(params[OCT_PARAM].getValue() - 3)) * mult;
const int oversamplingRatio = lfoMode ? 1 : oversampler.getOversamplingRatio();
const float timbre = clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getVoltage() / 10.f, 0.f, 1.f);

float tzfmVoltage = inputs[TZFM_INPUT].getVoltage();
if (blockTZFMDC) {
blockTZFMDCFilter.process(tzfmVoltage);
tzfmVoltage = blockTZFMDCFilter.highpass();
}
const int oversamplingRatio = lfoMode ? 1 : oversampler[0].getOversamplingRatio();

const double pitch = inputs[VOCT_INPUT].getVoltage() + params[FREQ_PARAM].getValue() * range[rangeIndex];
const double freq = baseFreq * simd::pow(2.f, pitch);
const double deltaBasePhase = clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f);
// denominator for the second-order FD
const double denominator = 0.25 / (deltaBasePhase * deltaBasePhase);
// not clamped, but _total_ phase treated later with floor/ceil
const double deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio;

float pw = timbre;
if (limitPW) {
pw = clamp(pw, 0.05, 0.95);
}
// pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
// for it to be added back in for hardware compatibility reasons
const float pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);

// hard sync
if (syncTrigger.process(inputs[SYNC_INPUT].getVoltage())) {
// hardware waveform is actually cos, so pi/2 phase offset is required
// - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25
phase = (waveform == WAVE_SIN) ? 0.25f : 0.f;
}
// number of active polyphony engines (must be at least 1)
const int channels = std::max({inputs[TZFM_INPUT].getChannels(), inputs[VOCT_INPUT].getChannels(), inputs[TIMBRE_INPUT].getChannels(), 1});

float* osBuffer = oversampler.getOSBuffer();
for (int i = 0; i < oversamplingRatio; ++i) {
for (int c = 0; c < channels; c += 4) {
const float_4 timbre = simd::clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getPolyVoltageSimd<float_4>(c) / 10.f, 0.f, 1.f);

phase += deltaBasePhase + deltaFMPhase;
if (phase > 1.f) {
phase -= floor(phase);
float_4 tzfmVoltage = inputs[TZFM_INPUT].getPolyVoltageSimd<float_4>(c);
if (blockTZFMDC) {
blockTZFMDCFilter[c / 4].process(tzfmVoltage);
tzfmVoltage = blockTZFMDCFilter[c / 4].highpass();
}
else if (phase < 0.f) {
phase += -ceil(phase) + 1;

const float_4 pitch = inputs[VOCT_INPUT].getPolyVoltageSimd<float_4>(c) + params[FREQ_PARAM].getValue() * range[rangeIndex];
const float_4 freq = baseFreq * simd::pow(2.f, pitch);
const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f);
// floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
// becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
// a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;

// 1 / denominator for the second-order FD
const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);
// not clamped, but _total_ phase treated later with floor/ceil
const float_4 deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio;

float_4 pw = timbre;
if (limitPW) {
pw = clamp(pw, 0.05, 0.95);
}
// pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
// for it to be added back in for hardware compatibility reasons
const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);

// sin is simple
// hard sync
const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c));
if (waveform == WAVE_SIN) {
osBuffer[i] = sin2pi_pade_05_5_4(phase);
// hardware waveform is actually cos, so pi/2 phase offset is required
// - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25
phase[c / 4] = simd::ifelse(syncMask, 0.25f, phase[c / 4]);
}
else {
phase[c / 4] = simd::ifelse(syncMask, 0.f, phase[c / 4]);
}

phases[0] = phase - 2 * deltaBasePhase + (phase < 2 * deltaBasePhase);
phases[1] = phase - deltaBasePhase + (phase < deltaBasePhase);
phases[2] = phase;
float_4* osBuffer = oversampler[c / 4].getOSBuffer();
for (int i = 0; i < oversamplingRatio; ++i) {

switch (waveform) {
case WAVE_TRI: {
osBuffer[i] = aliasSuppressedTri() * denominator;
break;
}
case WAVE_SAW: {
osBuffer[i] = aliasSuppressedSaw() * denominator;
break;
}
case WAVE_PULSE: {
double saw = aliasSuppressedSaw();
double sawOffset = aliasSuppressedOffsetSaw(pw);
phase[c / 4] += deltaBasePhase + deltaFMPhase;
// ensure within [0, 1]
phase[c / 4] -= simd::floor(phase[c / 4]);

osBuffer[i] = (sawOffset - saw) * denominator;
osBuffer[i] += pulseDCOffset;
break;
// sin is simple
if (waveform == WAVE_SIN) {
osBuffer[i] = sin2pi_pade_05_5_4(phase[c / 4]);
}
else {
float_4 phases[3]; // phase as extrapolated to the current and two previous samples

phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f);
phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f);
phases[2] = phase[c / 4];

switch (waveform) {
case WAVE_TRI: {
const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0);
const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv;

osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
break;
}
case WAVE_SAW: {
const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;

osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
break;
}
case WAVE_PULSE: {
float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < 1. - pw, +1.0, -1.0);
dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f;

float_4 saw = aliasSuppressedSaw(phases);
float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw);
float_4 dpwOrder3 = (sawOffset - saw) * denominatorInv + pulseDCOffset;

osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
break;
}
default: break;
}
default: break;
}
}

if (waveform != WAVE_PULSE) {
osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre));
}
}
if (waveform != WAVE_PULSE) {
osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre), c);
}

} // end of oversampling loop

// downsample (if required)
const float out = (oversamplingRatio > 1) ? oversampler.downsample() : osBuffer[0];
// downsample (if required)
const float_4 out = (oversamplingRatio > 1) ? oversampler[c / 4].downsample() : osBuffer[0];

// end of chain VCA
const float gain = std::max(0.f, inputs[VCA_INPUT].getNormalVoltage(10.f) / 10.f);
outputs[OUT_OUTPUT].setVoltage(5.f * out * gain);
// end of chain VCA
const float_4 gain = simd::clamp(inputs[VCA_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f, 0.f, 1.f);
outputs[OUT_OUTPUT].setVoltageSimd(5.f * out * gain, c);

} // end of channels loop

outputs[OUT_OUTPUT].setChannels(channels);
}

double aliasSuppressedTri() {
float_4 aliasSuppressedTri(float_4* phases) {
float_4 triBuffer[3];
for (int i = 0; i < 3; ++i) {
double p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
double s = 0.5 - std::abs(p); // eq 30
float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
float_4 s = 0.5 - simd::abs(p); // eq 30
triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29
}
return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]);
}

double aliasSuppressedSaw() {
float_4 aliasSuppressedSaw(float_4* phases) {
float_4 sawBuffer[3];
for (int i = 0; i < 3; ++i) {
double p = 2 * phases[i] - 1.0; // range -1 to +1
float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11
}

return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

double aliasSuppressedOffsetSaw(double pw) {
float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
float_4 sawOffsetBuff[3];

for (int i = 0; i < 3; ++i) {
double p = 2 * phases[i] - 1.0; // range -1 to +1
double pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
pwp += (pwp > 1) * -2; // modulo on [-1, +1]
float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1]
sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
}
return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
}

float wavefolder(float x, float xt) {
return stage2.process(stage1.process(x, xt));
float_4 wavefolder(float_4 x, float_4 xt, int c) {
return stage2[c / 4].process(stage1[c / 4].process(x, xt));
}

json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "blockTZFMDC", json_boolean(blockTZFMDC));
json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler.getOversamplingIndex()));
json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
return rootJ;
}

@@ -355,6 +340,11 @@ struct PonyVCO : Module {
removePulseDC = json_boolean_value(removePulseDCJ);
}

json_t* limitPWJ = json_object_get(rootJ, "limitPW");
if (limitPWJ) {
limitPW = json_boolean_value(limitPWJ);
}

json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
if (oversamplingIndexJ) {
oversamplingIndex = json_integer_value(oversamplingIndexJ);


+ 85
- 0
src/Voltio.cpp View File

@@ -0,0 +1,85 @@
#include "plugin.hpp"

using simd::float_4;

struct Voltio : Module {
enum ParamId {
OCT_PARAM,
RANGE_PARAM,
SEMITONES_PARAM,
PARAMS_LEN
};
enum InputId {
SUM_INPUT,
INPUTS_LEN
};
enum OutputId {
OUT_OUTPUT,
OUTPUTS_LEN
};
enum LightId {
PLUSMINUS5_LIGHT,
ZEROTOTEN_LIGHT,
LIGHTS_LEN
};

Voltio() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
auto octParam = configParam(OCT_PARAM, 0.f, 10.f, 0.f, "Octave");
octParam->snapEnabled = true;

configSwitch(RANGE_PARAM, 0.f, 1.f, 0.f, "Range", {"0 to 10", "-5 to +5"});
auto semitonesParam = configParam(SEMITONES_PARAM, 0.f, 11.f, 0.f, "Semitones");
semitonesParam->snapEnabled = true;

configInput(SUM_INPUT, "Sum");
configOutput(OUT_OUTPUT, "");
}

void process(const ProcessArgs& args) override {
const int channels = std::max(1, inputs[SUM_INPUT].getChannels());

for (int c = 0; c < channels; c += 4) {
float_4 in = inputs[SUM_INPUT].getPolyVoltageSimd<float_4>(c);

float offset = params[RANGE_PARAM].getValue() ? -5.f : 0.f;
in += params[SEMITONES_PARAM].getValue() / 12.f + params[OCT_PARAM].getValue() + offset;

outputs[OUT_OUTPUT].setVoltageSimd<float_4>(in, c);
}

outputs[OUT_OUTPUT].setChannels(channels);

lights[PLUSMINUS5_LIGHT].setBrightness(params[RANGE_PARAM].getValue() ? 1.f : 0.f);
lights[ZEROTOTEN_LIGHT].setBrightness(params[RANGE_PARAM].getValue() ? 0.f : 1.f);
}

};


struct VoltioWidget : ModuleWidget {
VoltioWidget(Voltio* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Voltio.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParamCentered<Davies1900hLargeLightGreyKnob>(mm2px(Vec(15.0, 20.828)), module, Voltio::OCT_PARAM));
addParam(createParamCentered<BefacoSwitch>(mm2px(Vec(22.083, 44.061)), module, Voltio::RANGE_PARAM));
auto p = createParamCentered<Davies1900hLargeLightGreyKnob>(mm2px(Vec(15.0, 67.275)), module, Voltio::SEMITONES_PARAM);
p->minAngle = -0.83 * M_PI;
p->maxAngle = M_PI;
addParam(p);

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(7.117, 111.003)), module, Voltio::SUM_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(22.661, 111.003)), module, Voltio::OUT_OUTPUT));

addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(5.695, 41.541)), module, Voltio::PLUSMINUS5_LIGHT));
addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(5.695, 46.633)), module, Voltio::ZEROTOTEN_LIGHT));
}
};


Model* modelVoltio = createModel<Voltio, VoltioWidget>("Voltio");

+ 4
- 0
src/plugin.cpp View File

@@ -27,4 +27,8 @@ void init(rack::Plugin *p) {
p->addModel(modelChannelStrip);
p->addModel(modelPonyVCO);
p->addModel(modelMotionMTR);
p->addModel(modelBurst);
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
p->addModel(modelOctaves);
}

+ 24
- 5
src/plugin.hpp View File

@@ -28,6 +28,10 @@ extern Model* modelNoisePlethora;
extern Model* modelChannelStrip;
extern Model* modelPonyVCO;
extern Model* modelMotionMTR;
extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
extern Model* modelOctaves;

struct Knurlie : SvgScrew {
Knurlie() {
@@ -221,6 +225,21 @@ struct BefacoSlidePotSmall : app::SvgSlider {
}
};

struct BefacoButton : app::SvgSwitch {
BefacoButton() {
momentary = true;
addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_0.svg")));
addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_1.svg")));
}
};

struct Davies1900hWhiteKnobEndless : Davies1900hKnob {
Davies1900hWhiteKnobEndless() {
setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless.svg")));
bg->setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless_bg.svg")));
}
};

inline int unsigned_modulo(int a, int b) {
return ((a % b) + b) % b;
}
@@ -295,7 +314,7 @@ private:
};

// Creates a Butterworth 2*Nth order highpass filter for blocking DC
template<int N>
template<int N, typename T>
struct DCBlockerT {

DCBlockerT() {
@@ -308,7 +327,7 @@ struct DCBlockerT {
recalculateCoefficients();
}

float process(float x) {
T process(T x) {
for (int idx = 0; idx < N; idx++) {
x = blockDCFilter[idx].process(x);
}
@@ -325,17 +344,17 @@ private:

for (int idx = 0; idx < N; idx++) {
float Q = 1.0f / (2.0f * std::cos(firstAngle + idx * poleInc));
blockDCFilter[idx].setParameters(dsp::BiquadFilter::HIGHPASS, fc_, Q, 1.0f);
blockDCFilter[idx].setParameters(dsp::TBiquadFilter<T>::HIGHPASS, fc_, Q, 1.0f);
}
}

float fc_;
static const int order = 2 * N;

dsp::BiquadFilter blockDCFilter[N];
dsp::TBiquadFilter<T> blockDCFilter[N];
};

typedef DCBlockerT<2> DCBlocker;
typedef DCBlockerT<2, float> DCBlocker;

/** When triggered, holds a high value for a specified time before going low again */
struct PulseGenerator_4 {


Loading…
Cancel
Save