├── .github
└── workflows
│ └── build-plugin.yml
├── CHANGELOG.md
├── LICENSE-dist.txt
├── LICENSE.txt
├── Makefile
├── README.md
├── docs
├── Oscelot-Ctx.gif
├── Oscelot-Meowmory.gif
├── Oscelot-exp.png
├── Oscelot-expander.gif
├── Oscelot-intro.gif
├── Oscelot-map-select.gif
├── Oscelot-map.gif
├── Oscelot-scan.gif
├── Oscelot-target.png
├── Oscelot.md
└── screenshot.png
├── plugin.json
├── presets
├── 8FEB_OpenStageControl.json
└── Oscelot_Patch.vcv
├── res
├── OscelotExpander_BlackSteel.svg
├── OscelotExpander_BlueSteel.svg
├── OscelotExpander_Brass.svg
├── OscelotExpander_GunMetal.svg
├── Oscelot_BlackSteel.svg
├── Oscelot_BlueSteel.svg
├── Oscelot_Brass.svg
├── Oscelot_GunMetal.svg
├── components
│ ├── Port.svg
│ ├── Screw.svg
│ ├── SliderHandle.svg
│ ├── SliderHorizontal.svg
│ ├── paw.svg
│ ├── paw0.svg
│ └── paw1.svg
└── fonts
│ ├── NovaMono-Regular.ttf
│ └── OFL.txt
└── src
├── MapModuleBase.hpp
├── OscController.cpp
├── Oscelot.cpp
├── Oscelot.hpp
├── OscelotExpander.cpp
├── OscelotExpander.hpp
├── components.hpp
├── components
├── LedTextField.hpp
├── MeowMory.hpp
├── OscelotParam.hpp
├── ParamHandleIndicator.hpp
└── PawButtons.hpp
├── helpers.hpp
├── osc
├── OscArgs.hpp
├── OscBundle.hpp
├── OscController.hpp
├── OscMessage.hpp
├── OscReceiver.hpp
├── OscSender.hpp
└── oscpack
│ ├── LICENSE
│ ├── README.md
│ ├── ip
│ ├── IpEndpointName.cpp
│ ├── IpEndpointName.h
│ ├── NetworkingUtils.h
│ ├── PacketListener.h
│ ├── TimerListener.h
│ ├── UdpSocket.h
│ ├── posix
│ │ ├── NetworkingUtils.cpp
│ │ └── UdpSocket.cpp
│ └── win32
│ │ ├── NetworkingUtils.cpp
│ │ └── UdpSocket.cpp
│ └── osc
│ ├── MessageMappingOscPacketListener.h
│ ├── OscException.h
│ ├── OscHostEndianness.h
│ ├── OscOutboundPacketStream.cpp
│ ├── OscOutboundPacketStream.h
│ ├── OscPacketListener.h
│ ├── OscPrintReceivedElements.cpp
│ ├── OscPrintReceivedElements.h
│ ├── OscReceivedElements.cpp
│ ├── OscReceivedElements.h
│ ├── OscTypes.cpp
│ └── OscTypes.h
├── plugin.cpp
├── plugin.hpp
└── ui
├── ParamWidgetContextExtender.hpp
└── ThemedModuleWidget.hpp
/.github/workflows/build-plugin.yml:
--------------------------------------------------------------------------------
1 | name: "Build VCV Rack Plugin"
2 |
3 | on:
4 | push:
5 | branches:
6 | - "master"
7 | - "*test"
8 | tags:
9 | - v*
10 | pull_request:
11 | branches:
12 | - "master"
13 |
14 | env:
15 | rack-sdk-version: 2.0.0
16 |
17 | defaults:
18 | run:
19 | shell: bash
20 |
21 | jobs:
22 | build:
23 | name: ${{ matrix.config.name }}
24 | runs-on: ${{ matrix.config.os }}
25 | strategy:
26 | matrix:
27 | config:
28 | - {
29 | name: Linux,
30 | arch: lin,
31 | os: ubuntu-latest,
32 | prepare-os: sudo apt-get update && sudo apt install -y libglu-dev
33 | }
34 | - {
35 | name: MacOS,
36 | arch: mac,
37 | os: macos-latest,
38 | prepare-os: "brew install mesa"
39 | }
40 | - {
41 | name: Windows,
42 | arch: win,
43 | os: windows-latest,
44 | prepare-os: export CC=gcc
45 | }
46 | steps:
47 | - uses: actions/checkout@v2
48 | with:
49 | submodules: recursive
50 | - name: Get Rack-SDK
51 | run: |
52 | pushd $HOME
53 | curl -o Rack-SDK.zip https://vcvrack.com/downloads/Rack-SDK-${{ env.rack-sdk-version }}-${{ matrix.config.arch }}.zip
54 | unzip Rack-SDK.zip
55 | - name: Patch plugin.mk, use 7zip on Windows
56 | if: runner.os == 'Windows'
57 | run: |
58 | sed -i 's/zip -q -9 -r/7z a -tzip -mx=9/' $HOME/Rack-SDK/plugin.mk
59 | - name: Modify plugin version
60 | # only modify plugin version if no tag was created
61 | if: "! startsWith(github.ref, 'refs/tags/v')"
62 | run: |
63 | gitrev=`git rev-parse --short HEAD`
64 | pluginversion=`jq -r '.version' plugin.json`
65 | echo "Set plugin version from $pluginversion to $pluginversion-test because no TAG was created (${{github.ref}})"
66 | cat <<< `jq --arg VERSION "$pluginversion-test" '.version=$VERSION' plugin.json` > plugin.json
67 | - name: Build plugin
68 | run: |
69 | ${{ matrix.config.prepare-os }}
70 | export RACK_DIR=$HOME/Rack-SDK
71 | make -j dep
72 | make -j dist
73 | - name: Upload artifact
74 | uses: actions/upload-artifact@v2
75 | with:
76 | path: dist/**/*.vcvplugin
77 | name: ${{ matrix.config.name }}
78 |
79 | pre-release:
80 | name: "Pre Release"
81 | # only create a dev release if no tag was created
82 | if: "! startsWith(github.ref, 'refs/tags/v')"
83 | runs-on: "ubuntu-latest"
84 | needs: build
85 |
86 | steps:
87 | - uses: actions/download-artifact@v2
88 | with:
89 | path: _artifacts
90 | - uses: "marvinpinto/action-automatic-releases@latest"
91 | with:
92 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
93 | automatic_release_tag: "latest"
94 | prerelease: true
95 | title: "Latest Development Build"
96 | files: |
97 | _artifacts/**/*.vcvplugin
98 |
99 | release:
100 | name: "Release"
101 | # only create a release if a tag was created e.g. v1.2.3
102 | if: "startsWith(github.ref, 'refs/tags/v')"
103 | runs-on: "ubuntu-latest"
104 | needs: build
105 |
106 | steps:
107 | - uses: actions/download-artifact@v2
108 | with:
109 | path: _artifacts
110 | - uses: "marvinpinto/action-automatic-releases@latest"
111 | with:
112 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
113 | prerelease: false
114 | files: |
115 | _artifacts/**/*.vcvplugin
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.0.0
2 | - VCV Library Release
3 |
4 | ## 2.0.0 Beta (11/21/2021)
5 | - Added fix for deadlock issue, updated to latest VCV SDK
6 | - Send feedback only if display value of param changes
7 | - Fix headers
8 |
9 | ## 2.0.0 Beta (11/09/2021)
10 | - Fix module browser crash on Mac
11 | - Removed some test code
12 | - Added new skin - Black Steel
13 |
14 | ## 2.0.0 Beta (11/07/2021)
15 | - 2.0 API changes, Code refactor/cleanup
16 | - New Features:
17 | - Reduce OSC feedback, only send full feedback when param changes. Also Added option to always send full feedback.
18 | - MeowMory Banks! Save upto 128 banks of mappings using the slider
19 |
20 |
21 | ## 1.0.2
22 |
23 | - Trigger/CV Expander for OSC'elot
24 | - Fix doc links
25 |
26 | ## 1.0.1
27 |
28 | - First Public release
29 |
30 | ## 1.0.0-alpha
31 |
32 | - Alpha release
--------------------------------------------------------------------------------
/LICENSE-dist.txt:
--------------------------------------------------------------------------------
1 |
2 | #### FUNDAMENTAL ####
3 |
4 | Copyright (c) 2016-2018 Andrew Belt
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 |
12 |
13 |
14 |
15 | #### AUDIBLE INSTRUMENTS
16 |
17 | Copyright 2016 Andrew Belt
18 |
19 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
20 |
21 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
22 |
23 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
24 |
25 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
26 |
27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
30 |
31 |
32 | #### CORE and VCV RACK ####
33 |
34 | All VCV Rack source code is copyright © 2019 Andrew Belt and licensed under the GNU General Public License v3.0 with the "VCV Rack Non-Commercial Plugin License Exception", allowed under section 7 of GPLv3, and a commercial licensing option.
35 |
36 |
37 | #### BACONMUSIC ####
38 |
39 | Copyright © 2017-2019 Paul Walker
40 | Licsened under the Gnu Public License, version 3. https://www.gnu.org/licenses/gpl-3.0.en.html
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | RACK_DIR ?= ../..
2 |
3 |
4 | include $(RACK_DIR)/arch.mk
5 |
6 | ifdef ARCH_WIN
7 | SOURCES += $(wildcard src/osc/oscpack/ip/win32/*.cpp)
8 | LDFLAGS += -lws2_32 -lwinmm
9 | else
10 | SOURCES += $(wildcard src/osc/oscpack/ip/posix/*.cpp)
11 | endif
12 |
13 | SOURCES += $(wildcard src/osc/oscpack/ip/*.cpp) $(wildcard src/osc/oscpack/osc/*.cpp)
14 | SOURCES += $(wildcard src/*.cpp)
15 |
16 | DISTRIBUTABLES += $(wildcard LICENSE*) res presets
17 |
18 | include $(RACK_DIR)/plugin.mk
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OSC'elot
2 |
3 |
4 | 
5 | 
6 | 
7 | 
8 |
9 | [OSC'elot Manual](./docs/Oscelot.md): Map VCV Rack parameters to OSC controllers with OSC feedback.
10 |
11 | 
12 |
13 | Feel free to contact me or create a GitHub issue if you have any problems or questions!
14 |
15 | If you like my work consider donating to https://paypal.me/themodularmind. Thank you!
16 |
17 | ## Special thanks
18 |
19 | - [Stoermelder](https://library.vcvrack.com/?brand=stoermelder) for all his awesome modules, especially MIDI-CAT on which OSC'elot is based.
20 | - [Omri Cohen](https://www.youtube.com/channel/UCuWKHSHTHMV_nVSeNH4gYAg) for his video tutorials that got me into into VCV Rack.
21 | - [VCV Rack](https://vcvrack.com/) Andrew Belt for creating VCV Rack
22 |
23 | ## License
24 |
25 | All **source code** is copyright © 2021 and is licensed under the [GNU General Public License, version v3.0](./LICENSE.txt).
26 |
27 | All **files** and **graphics** in the `res` and `res-src` directories are licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/). You may not distribute modified adaptations of these graphics.
--------------------------------------------------------------------------------
/docs/Oscelot-Ctx.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-Ctx.gif
--------------------------------------------------------------------------------
/docs/Oscelot-Meowmory.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-Meowmory.gif
--------------------------------------------------------------------------------
/docs/Oscelot-exp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-exp.png
--------------------------------------------------------------------------------
/docs/Oscelot-expander.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-expander.gif
--------------------------------------------------------------------------------
/docs/Oscelot-intro.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-intro.gif
--------------------------------------------------------------------------------
/docs/Oscelot-map-select.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-map-select.gif
--------------------------------------------------------------------------------
/docs/Oscelot-map.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-map.gif
--------------------------------------------------------------------------------
/docs/Oscelot-scan.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-scan.gif
--------------------------------------------------------------------------------
/docs/Oscelot-target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/Oscelot-target.png
--------------------------------------------------------------------------------
/docs/Oscelot.md:
--------------------------------------------------------------------------------
1 | # OSC'elot
2 |
3 | OSC'elot is a mapping module for OSC controllers based on stoermelder's MIDI-CAT.
4 |
5 | 
6 |
7 |
8 |
9 | * [Controller Types](#controller-types)
10 | + [Faders](#faders)
11 | + [Encoders](#encoders)
12 | + [Buttons](#buttons)
13 | * [OSC Feedback](#osc-feedback)
14 | * [Mapping parameters](#mapping-parameters)
15 | + [Map an entire module](#map-an-entire-module)
16 | + [Map parameters one at a time](#map-parameters-one-at-a-time)
17 | * [MeowMory](#meowmory)
18 | * [Context-Label](#context-label)
19 | * [Menu Options](#menu-options)
20 | + [Additional features](#additional-features)
21 | * [Controller Modes](#controller-modes)
22 | * [Expander](#expander)
23 |
24 |
25 |
26 | ---
27 | ## Controller Types
28 | It currently supports mapping of Faders, Encoders and Buttons.
29 |
30 | ### Faders
31 | - OSC Messages **must** have an address ending with `/fader`
32 | - It **must** have two arguments, Id (Integer) and Value(Float between 0-1)
33 | - > `/fader, args: (1, 0.5573)`
34 |
35 | ### Encoders
36 | - OSC Messages **must** have an address ending with `/encoder`
37 | - It **must** have two arguments, Id (Integer) and Delta Value(multiples of -1.0 or +1.0)
38 | - Encoders have default sensitivity set to *649*
39 | - > `/encoder, args: (1, -1.0)`
40 |
41 | ### Buttons
42 | - OSC Messages **must** have an address ending with `/button`
43 | - It **must** have two arguments, Id (Integer) and Value(0.0 or 1.0)
44 | - > `/button, args: (1, 1.0)`
45 |
46 |
47 |
48 | ---
49 | ## OSC Feedback
50 | If the Sender is activated, any parameter change will generate two OSC messages as feedback. On activation of the Sender it also sends these two messages for all currently mapped controls. This is useful for initialization of the controls on the OSC controller.
51 |
52 | For a fader mapped to a MixMaster Volume fader:
53 | > Sent from controller:
54 | `/fader/1/0.3499999940395355`
55 |
56 | > Sent from OSC'elot:
57 | `/fader, args: (1, 0.3499999940395355)`
58 | `/fader/info, args: (1, 'MixMaster', '-01-: level', '-21.335', ' dB')`
59 |
60 | For an encoder mapped to a MixMaster Pan knob:
61 | > Sent from controller:
62 | `/encoder/1/1.0`
63 |
64 | > Sent from OSC'elot:
65 | `/encoder, args: (1, 0.5231125950813293)`
66 | `/encoder/info, args: (1, 'MixMaster', '-01-: pan', '4.6225', '%')`
67 |
68 | The first message contains the id and the current value of the mapped param. (0.0-1.0)
69 | The second message ending with `/info` has the integer Id arg followed by info about the mapped param:
70 | | Name | Type | Value | Notes |
71 | | ------------- |:---------:|:-------------:|-------------------------------------------|
72 | | Id | Integer | `1` | Id of mapped OSC controller |
73 | | ModuleName | String | `'MixMaster'` | Not affected by OSC'elot labels |
74 | | Label | String | `'-01-: pan'` | Not affected by OSC'elot labels |
75 | | DisplayValue | String | `'4.6225'` | Value shown when for param in VCV |
76 | | Unit | String | `'%'` | Blank string if param does not have units |
77 |
78 |
79 |
80 | ---
81 | ## Mapping parameters
82 | A typical workflow for mapping your controller will look like this:
83 |
84 | - Connect your OSC controller, whether physical/virtual by setting the receive port and starting the Receiver.
85 | - If your controller can receive OSC messages you can set the send port and start the Sender.
86 |
87 |
88 |
89 | ### Map an entire module
90 | This option changes your cursor into a crosshair which needs to be pointed onto any module within your patch by clicking on the panel.
91 | - *`Clear first`* clears OSC mappings before mapping new module. **SHORTCUT** `Ctrl/Cmd+Shift+D`
92 | - *`Keep OSC assignments`* keeps the OSC mappings and re-maps them onto the new module. **SHORTCUT** `Shift+D`
93 |
94 | 
95 |
96 |
97 |
98 | ### Map parameters one at a time
99 | - Activate the first mapping slot by clicking on it.
100 | - Click on a parameter of any module in your patch. The slot will bind this parameter.
101 | - Touch a control or key on your OSC device. The slot will bind the OSC message.
102 | - Repeat this process until all the desired parameters have been mapped.
103 |
104 | A blinking mapping indicator will indicate the bound parameter the mapping-slot which is currently selected.
105 |
106 | 
107 |
108 |
109 |
110 | ---
111 | ## MeowMory
112 | OSC'elot allows you store an unlimited number of module-specific mappings which can be recalled for the same type of module without doing any mapping manually.
113 | A typical workflow will look like this:
114 |
115 | - Create a mapping using your OSC device of any module in OSC'elot.
116 | - Under the `MeowMory` section you have options to *`Store mapping`*.
117 | - The saved module mappings are listed under *`Available mappings`*.
118 | - You can store only one mapping of any specific module-type. If you store a mapping for a module which has one already it will be replaced.
119 | - You can remove any mapping using the the context menu *`Delete`*. Intializing the module does not remove them.
120 |
121 | Stored module-mappings can be recalled by using the middle `Apply` button or hotkey `Shift+V` while hovering OSC'elot.
122 | The cursor changes to a crosshair and the saved OSC-mapping is loaded into OSC'elot after you click on a module in your patch.
123 |
124 | 
125 |
126 |
127 |
128 | The `Prev` and `Next` buttons scan your patch from top-left to bottom-right and apply a stored mapping to the next/previous module of the current mapped module.
129 | Modules without a mapping will be skipped. This can also be triggered via OSC:
130 | > `/oscelot/next`
131 | > `/oscelot/prev`
132 |
133 | 
134 |
135 |
136 |
137 | ---
138 | ## Context-Label
139 | After a parameter has been mapped the parameter's context menu is extended with some addtional menu items allowing quick OSC learning and centering it's OSC'elot module on the center of the screen.
140 |
141 | 
142 |
143 | There are even further options when you set a `Context label`. This allows you to name each instance of OSC'elot in your patch. This name can be addressed in any parameter's context menu for activating OSC mapping or re-mapping parameters to an existing OSC control.
144 |
145 | 
146 |
147 |
148 |
149 | ---
150 | ## Menu Options
151 | *`Re-send OSC feedback`* allows you to manually send feedback for all mapped parameters back to your OSC device.
152 | - The *`Now`* option can be useful if you switch/restart your OSC device or the device needs to be initalized again.
153 | - The *`Periodically`* option when enabled sends OSC feedback **once a second** for all mapped controls regardless of whether the parameter has changed.
154 |
155 | *`Locate and indicate`*:
156 | Received OSC messages have no effect on the mapped parameters, instead the module is centered on the screen and the parameter mapping indicator flashes for a short period of time. When finished verifying all OSC controls switch back to *`Operating`* mode for normal operation of OSC'elot.
157 |
158 | *`Lock mapping slots`*:
159 | Accidental changes of the mapping slots can be prevented by using this option, which locks access to the mapping slots.
160 | - Scrolling Rack's current view by mouse is interrupted by OSC'elot's list widget while hovered. As this behavior can be annoying all scrolling events are ignored if _Lock mapping slots_ is enabled.
161 |
162 | *`Ignore OSC devices`*:
163 | Skips any OSC device settings when loading a preset into OSC'elot, only mapping slots will be loaded.
164 |
165 | ### Additional features
166 |
167 | - The text shown in every mapping slot can be replaced by a custom text label in the context menu. It's prefilled by default with the automatic label.
168 | - If you find the blue mapping indicators distracting you can disable them in OSC'elot's context menu.
169 | - An active mapping process can be aborted by hitting the `ESC`-key while hovering the mouse over OSC'elot.
170 | - An active mapping slot can be skipped by hitting the `SPACE`-key while hovering the mouse over OSC'elot.
171 | - Settings of a mapping slot are copied from the previous slot: If you set up the first mapping slot and map further mapping slots afterwards, these settings are copied over. Useful for settings like Encoder Sensitivity and Controller mode.
172 |
173 |
174 |
175 | ---
176 | ## Controller Modes
177 |
178 | This pretty much works the same as `MIDI-CAT`. By default the mode is set to `DIRECT`.
179 | Encoders are always `DIRECT`. Buttons and faders can be changed to other modes below.
180 | You will likely only need to change this if using a hardware OSC controller.
181 |
182 | - **`Direct`**: Every received OSC message is directly applied to the mapped parameter (*default*).
183 |
184 | - **`Pickup (snap)`**: OSC messages are ignored until the control reaches the current value of the parameter. After that the OSC control is "snaped" unto the parameter and will only unsnap if the parameter is changed from within Rack, e.g. manually by mouse or preset-loading.
185 |
186 | - **`Pickup (jump)`**: Same as snap-mode, but the control will loose the parameter when jumping to another value. This mode can be used if your OSC controller supports switching templates and you don't want your parameters to change when loading a different template.
187 |
188 | - **`Toggle`**: Every OSC message toggles the parameter between its minimum and maximum value (usually for buttons)
189 |
190 | - **`Toggle + Value`**: Every OSC message toggles the parameter between its minimum and the control's value.
191 |
192 |
193 |
194 | ---
195 | ## Expander
196 |
197 | The expander adds a Poly `trigger` and `CV` output along with 8 individual `trigger` and `CV` outputs for any configured controllers. The default CV range is `-5V to 5V`, but this can be changed in the right-click menu.
198 |
199 |
200 | 
201 | 
202 |
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/docs/screenshot.png
--------------------------------------------------------------------------------
/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "slug": "OSCelot",
3 | "version": "2.0.0",
4 | "license": "GPL-3.0-only",
5 | "author": "TheModularMind",
6 | "name": "OSCelot",
7 | "brand": "TheModularMind",
8 | "authorEmail": "",
9 | "pluginUrl": "https://github.com/The-Modular-Mind/oscelot",
10 | "authorUrl": "https://github.com/The-Modular-Mind",
11 | "sourceUrl": "https://github.com/The-Modular-Mind/oscelot.git",
12 | "manualUrl": "https://github.com/The-Modular-Mind/oscelot/blob/master/docs/Oscelot.md",
13 | "changelogUrl": "https://github.com/The-Modular-Mind/oscelot/blob/master/CHANGELOG.md",
14 | "donateUrl": "https://paypal.me/themodularmind",
15 | "modules": [
16 | {
17 | "slug": "OSCelot",
18 | "name": "OSCelot",
19 | "description": "OSC mapping module for parameters of any module",
20 | "tags": ["Utility"],
21 | "manualUrl": "https://github.com/The-Modular-Mind/oscelot/blob/master/docs/Oscelot.md"
22 | },
23 | {
24 | "slug": "OSCelotExpander",
25 | "name": "OSCelotExpander",
26 | "description": "Expander for OSC'elot outputting a trigger and CV for each controller",
27 | "tags": ["Expander"],
28 | "manualUrl": "https://github.com/The-Modular-Mind/oscelot/blob/master/docs/Oscelot.md#expander"
29 | }
30 | ]
31 | }
--------------------------------------------------------------------------------
/presets/8FEB_OpenStageControl.json:
--------------------------------------------------------------------------------
1 | {
2 | "createdWith": "Open Stage Control",
3 | "version": "1.9.12",
4 | "type": "session",
5 | "content": {
6 | "type": "root",
7 | "id": "root",
8 | "visible": true,
9 | "interaction": true,
10 | "comments": "",
11 | "width": "auto",
12 | "height": "auto",
13 | "colorText": "auto",
14 | "colorWidget": "auto",
15 | "alphaFillOn": "auto",
16 | "padding": "auto",
17 | "html": "",
18 | "css": "",
19 | "colorBg": "auto",
20 | "layout": "default",
21 | "justify": "start",
22 | "gridTemplate": "",
23 | "contain": true,
24 | "scroll": true,
25 | "innerPadding": true,
26 | "verticalTabs": false,
27 | "variables": "@{parent.variables}",
28 | "traversing": false,
29 | "value": "",
30 | "default": "",
31 | "linkId": "",
32 | "address": "auto",
33 | "preArgs": "",
34 | "typeTags": "",
35 | "decimals": 2,
36 | "target": "",
37 | "ignoreDefaults": false,
38 | "bypass": false,
39 | "script": "",
40 | "widgets": [
41 | {
42 | "type": "panel",
43 | "top": "auto",
44 | "left": "auto",
45 | "id": "panel_2",
46 | "visible": true,
47 | "interaction": true,
48 | "comments": "",
49 | "width": "100%",
50 | "height": "100%",
51 | "expand": "false",
52 | "colorText": "auto",
53 | "colorWidget": "auto",
54 | "colorStroke": "auto",
55 | "colorFill": "auto",
56 | "alphaStroke": "auto",
57 | "alphaFillOff": "auto",
58 | "alphaFillOn": "auto",
59 | "lineWidth": "auto",
60 | "padding": "auto",
61 | "html": "",
62 | "css": "",
63 | "colorBg": "auto",
64 | "layout": "default",
65 | "justify": "start",
66 | "gridTemplate": "",
67 | "contain": true,
68 | "scroll": true,
69 | "innerPadding": true,
70 | "verticalTabs": false,
71 | "variables": "@{parent.variables}",
72 | "traversing": false,
73 | "value": "",
74 | "default": "",
75 | "linkId": "",
76 | "address": "auto",
77 | "preArgs": "",
78 | "typeTags": "",
79 | "decimals": 2,
80 | "target": "",
81 | "ignoreDefaults": false,
82 | "bypass": false,
83 | "script": "",
84 | "widgets": [
85 | {
86 | "type": "matrix",
87 | "top": "30%",
88 | "left": "0%",
89 | "id": "EncoderLabelScript",
90 | "visible": false,
91 | "interaction": true,
92 | "comments": "",
93 | "width": "auto",
94 | "height": "5%",
95 | "expand": "false",
96 | "colorText": "auto",
97 | "colorWidget": "rgba(23,110,197,1)",
98 | "colorStroke": "auto",
99 | "colorFill": "auto",
100 | "alphaStroke": "auto",
101 | "alphaFillOff": "auto",
102 | "alphaFillOn": "auto",
103 | "lineWidth": "auto",
104 | "padding": 0,
105 | "html": "",
106 | "css": "",
107 | "colorBg": "auto",
108 | "layout": "horizontal",
109 | "justify": "center",
110 | "gridTemplate": "",
111 | "contain": true,
112 | "scroll": false,
113 | "innerPadding": false,
114 | "variables": "@{parent.variables}",
115 | "traversing": false,
116 | "widgetType": "text",
117 | "quantity": 8,
118 | "props": "JS{{\nvar props = {}\nprops.wrap= \"soft\"\nprops.address = \"/encoder/info\"\nprops.preArgs = [$]\nprops.script = \"var label=value[0] + '\\\\n'+ value[1] + '\\\\n'+ value[2] + ' '+ value[3]; set('encoderLabels/#{$}', label);\"\nreturn props\n}}",
119 | "value": "",
120 | "default": "",
121 | "linkId": "",
122 | "address": "auto",
123 | "preArgs": "",
124 | "typeTags": "",
125 | "decimals": 2,
126 | "target": "",
127 | "ignoreDefaults": false,
128 | "bypass": false,
129 | "script": "",
130 | "widgets": [],
131 | "tabs": []
132 | },
133 | {
134 | "type": "matrix",
135 | "top": "30%",
136 | "left": "0%",
137 | "id": "ButtonLabelScript",
138 | "visible": false,
139 | "interaction": true,
140 | "comments": "",
141 | "width": "auto",
142 | "height": "5%",
143 | "expand": "false",
144 | "colorText": "auto",
145 | "colorWidget": "rgba(23,110,197,1)",
146 | "colorStroke": "auto",
147 | "colorFill": "auto",
148 | "alphaStroke": "auto",
149 | "alphaFillOff": "auto",
150 | "alphaFillOn": "auto",
151 | "lineWidth": "auto",
152 | "padding": 0,
153 | "html": "",
154 | "css": "",
155 | "colorBg": "auto",
156 | "layout": "grid",
157 | "justify": "center",
158 | "gridTemplate": 8,
159 | "contain": true,
160 | "scroll": false,
161 | "innerPadding": false,
162 | "variables": "@{parent.variables}",
163 | "traversing": false,
164 | "widgetType": "text",
165 | "quantity": 16,
166 | "props": "JS{{\nvar props = {}\nprops.wrap= \"soft\"\nprops.address = \"/button/info\"\nprops.preArgs = [$]\nprops.script = \"var label=value[0] + '\\\\n'+ value[1]; set('buttonLabels/#{$}', label);\"\nreturn props\n}}",
167 | "value": "",
168 | "default": "",
169 | "linkId": "",
170 | "address": "auto",
171 | "preArgs": "",
172 | "typeTags": "",
173 | "decimals": 2,
174 | "target": "",
175 | "ignoreDefaults": false,
176 | "bypass": false,
177 | "script": "",
178 | "widgets": [],
179 | "tabs": []
180 | },
181 | {
182 | "type": "matrix",
183 | "top": "30%",
184 | "left": "0%",
185 | "id": "FaderLabelScript",
186 | "visible": false,
187 | "interaction": true,
188 | "comments": "",
189 | "width": "auto",
190 | "height": "5%",
191 | "expand": "false",
192 | "colorText": "auto",
193 | "colorWidget": "rgba(23,110,197,1)",
194 | "colorStroke": "auto",
195 | "colorFill": "auto",
196 | "alphaStroke": "auto",
197 | "alphaFillOff": "auto",
198 | "alphaFillOn": "auto",
199 | "lineWidth": "auto",
200 | "padding": 0,
201 | "html": "",
202 | "css": "",
203 | "colorBg": "auto",
204 | "layout": "horizontal",
205 | "justify": "center",
206 | "gridTemplate": "",
207 | "contain": true,
208 | "scroll": false,
209 | "innerPadding": false,
210 | "variables": "@{parent.variables}",
211 | "traversing": false,
212 | "widgetType": "text",
213 | "quantity": 8,
214 | "props": "JS{{\nvar props = {}\nprops.wrap= \"soft\"\nprops.address = \"/fader/info\"\nprops.preArgs = [$]\nprops.script = \"var label=value[0] + '\\\\n'+ value[1] + '\\\\n'+ value[2] + ' '+ value[3]; set('faderLabels/#{$}', label);\"\nreturn props\n}}",
215 | "value": "",
216 | "default": "",
217 | "linkId": "",
218 | "address": "auto",
219 | "preArgs": "",
220 | "typeTags": "",
221 | "decimals": 2,
222 | "target": "",
223 | "ignoreDefaults": false,
224 | "bypass": false,
225 | "script": "",
226 | "widgets": [],
227 | "tabs": []
228 | },
229 | {
230 | "type": "matrix",
231 | "top": "60%",
232 | "left": "0%",
233 | "id": "buttons",
234 | "visible": true,
235 | "interaction": true,
236 | "comments": "",
237 | "width": "100%",
238 | "height": "20%",
239 | "expand": "false",
240 | "colorText": "auto",
241 | "colorWidget": "auto",
242 | "colorStroke": "auto",
243 | "colorFill": "auto",
244 | "alphaStroke": "auto",
245 | "alphaFillOff": "auto",
246 | "alphaFillOn": "auto",
247 | "lineWidth": "auto",
248 | "padding": "auto",
249 | "html": "",
250 | "css": "",
251 | "colorBg": "auto",
252 | "layout": "horizontal",
253 | "justify": "start",
254 | "gridTemplate": "",
255 | "contain": true,
256 | "scroll": false,
257 | "innerPadding": false,
258 | "variables": "@{parent.variables}",
259 | "traversing": false,
260 | "widgetType": "button",
261 | "quantity": 8,
262 | "props": "JS{{\nvar props = {}\nprops.on= 1\nprops.off= 0\nprops.mode = \"toggle\"\nprops.address = \"/button\"\nprops.preArgs = [$]\nprops.typeTags = \"if\"\nprops.label=\"\"\nreturn props\n}}",
263 | "value": "",
264 | "default": "",
265 | "linkId": "",
266 | "address": "/button",
267 | "preArgs": "",
268 | "typeTags": "",
269 | "decimals": 2,
270 | "target": "",
271 | "ignoreDefaults": false,
272 | "bypass": false,
273 | "script": "",
274 | "widgets": [],
275 | "tabs": []
276 | },
277 | {
278 | "type": "matrix",
279 | "top": "0%",
280 | "left": "0%",
281 | "id": "faders",
282 | "visible": true,
283 | "interaction": true,
284 | "comments": "",
285 | "width": "100%",
286 | "height": "60%",
287 | "expand": "false",
288 | "colorText": "auto",
289 | "colorWidget": "rgba(23,110,197,1)",
290 | "colorStroke": "auto",
291 | "colorFill": "auto",
292 | "alphaStroke": "auto",
293 | "alphaFillOff": "auto",
294 | "alphaFillOn": "auto",
295 | "lineWidth": "auto",
296 | "padding": "auto",
297 | "html": "",
298 | "css": "",
299 | "colorBg": "auto",
300 | "layout": "horizontal",
301 | "justify": "start",
302 | "gridTemplate": "",
303 | "contain": true,
304 | "scroll": false,
305 | "innerPadding": false,
306 | "variables": "@{parent.variables}",
307 | "traversing": false,
308 | "widgetType": "fader",
309 | "quantity": 8,
310 | "props": "JS{{\nvar props = {}\nprops.design= \"compact\"\nprops.gradient={\n \"0\": \"blue\",\n \"0.70\": \"purple\",\n \"0.90\": \"red\"\n}\nprops.knobSize= 25\nprops.address = \"/fader\"\nprops.preArgs = [$]\nprops.typeTags = \"if\"\nreturn props\n}}",
311 | "value": "",
312 | "default": "",
313 | "linkId": "",
314 | "address": "/button",
315 | "preArgs": "",
316 | "typeTags": "if",
317 | "decimals": 2,
318 | "target": "",
319 | "ignoreDefaults": false,
320 | "bypass": false,
321 | "script": "",
322 | "widgets": [],
323 | "tabs": []
324 | },
325 | {
326 | "type": "matrix",
327 | "top": "45%",
328 | "left": "0%",
329 | "id": "faderLabels",
330 | "visible": true,
331 | "interaction": false,
332 | "comments": "",
333 | "width": "100%",
334 | "height": "10%",
335 | "expand": "false",
336 | "colorText": "auto",
337 | "colorWidget": "rgba(23,110,197,0)",
338 | "colorStroke": "rgba(23,110,197,0)",
339 | "colorFill": "auto",
340 | "alphaStroke": "auto",
341 | "alphaFillOff": "auto",
342 | "alphaFillOn": "auto",
343 | "lineWidth": "auto",
344 | "padding": 0,
345 | "html": "",
346 | "css": "",
347 | "colorBg": "rgba(255,255,255,0)",
348 | "layout": "horizontal",
349 | "justify": "center",
350 | "gridTemplate": "",
351 | "contain": true,
352 | "scroll": false,
353 | "innerPadding": false,
354 | "variables": "@{parent.variables}",
355 | "traversing": false,
356 | "widgetType": "text",
357 | "quantity": 8,
358 | "props": "JS{{\nvar props = {}\nprops.wrap= \"soft\"\nreturn props\n}}",
359 | "value": "",
360 | "default": "",
361 | "linkId": "",
362 | "address": "auto",
363 | "preArgs": "",
364 | "typeTags": "",
365 | "decimals": 2,
366 | "target": "",
367 | "ignoreDefaults": false,
368 | "bypass": false,
369 | "script": "",
370 | "widgets": [],
371 | "tabs": []
372 | },
373 | {
374 | "type": "matrix",
375 | "top": "60%",
376 | "left": "0%",
377 | "id": "buttonLabels",
378 | "visible": true,
379 | "interaction": false,
380 | "comments": "",
381 | "width": "100%",
382 | "height": "20%",
383 | "expand": "false",
384 | "colorText": "auto",
385 | "colorWidget": "rgba(23,110,197,0)",
386 | "colorStroke": "rgba(23,110,197,0)",
387 | "colorFill": "auto",
388 | "alphaStroke": "auto",
389 | "alphaFillOff": "auto",
390 | "alphaFillOn": "auto",
391 | "lineWidth": "auto",
392 | "padding": 0,
393 | "html": "",
394 | "css": "",
395 | "colorBg": "rgba(255,255,255,0)",
396 | "layout": "horizontal",
397 | "justify": "center",
398 | "gridTemplate": "",
399 | "contain": true,
400 | "scroll": false,
401 | "innerPadding": false,
402 | "variables": "@{parent.variables}",
403 | "traversing": false,
404 | "widgetType": "text",
405 | "quantity": 8,
406 | "props": "JS{{\nvar props = {}\nprops.wrap= \"soft\"\nreturn props\n}}",
407 | "value": "",
408 | "default": "",
409 | "linkId": "",
410 | "address": "auto",
411 | "preArgs": "",
412 | "typeTags": "",
413 | "decimals": 2,
414 | "target": "",
415 | "ignoreDefaults": false,
416 | "bypass": false,
417 | "script": "",
418 | "widgets": [],
419 | "tabs": []
420 | },
421 | {
422 | "type": "matrix",
423 | "top": "80%",
424 | "left": "0%",
425 | "id": "encoders",
426 | "visible": true,
427 | "interaction": true,
428 | "comments": "",
429 | "width": "100%",
430 | "height": "20%",
431 | "expand": "false",
432 | "colorText": "auto",
433 | "colorWidget": "auto",
434 | "colorStroke": "auto",
435 | "colorFill": "auto",
436 | "alphaStroke": "auto",
437 | "alphaFillOff": "auto",
438 | "alphaFillOn": "auto",
439 | "lineWidth": "auto",
440 | "padding": "auto",
441 | "html": "",
442 | "css": "",
443 | "colorBg": "auto",
444 | "layout": "horizontal",
445 | "justify": "center",
446 | "gridTemplate": "",
447 | "contain": true,
448 | "scroll": false,
449 | "innerPadding": false,
450 | "variables": "@{parent.variables}",
451 | "traversing": false,
452 | "widgetType": "encoder",
453 | "quantity": 8,
454 | "props": "JS{{\nvar props = {}\nprops.address = \"/encoder\"\nprops.preArgs = [$]\nprops.typeTags = \"if\"\nreturn props\n}}",
455 | "value": "",
456 | "default": "",
457 | "linkId": "",
458 | "address": "/encoder",
459 | "preArgs": "",
460 | "typeTags": "if",
461 | "decimals": 2,
462 | "target": "",
463 | "ignoreDefaults": false,
464 | "bypass": false,
465 | "script": "",
466 | "widgets": [],
467 | "tabs": []
468 | },
469 | {
470 | "type": "matrix",
471 | "top": "80%",
472 | "left": "0%",
473 | "id": "encoderLabels",
474 | "visible": true,
475 | "interaction": false,
476 | "comments": "",
477 | "width": "100%",
478 | "height": "20%",
479 | "expand": "false",
480 | "colorText": "auto",
481 | "colorWidget": "rgba(23,110,197,0)",
482 | "colorStroke": "rgba(23,110,197,0)",
483 | "colorFill": "auto",
484 | "alphaStroke": "auto",
485 | "alphaFillOff": "auto",
486 | "alphaFillOn": "auto",
487 | "lineWidth": "auto",
488 | "padding": "auto",
489 | "html": "",
490 | "css": "",
491 | "colorBg": "rgba(255,255,255,0)",
492 | "layout": "horizontal",
493 | "justify": "center",
494 | "gridTemplate": "",
495 | "contain": true,
496 | "scroll": false,
497 | "innerPadding": false,
498 | "variables": "@{parent.variables}",
499 | "traversing": false,
500 | "widgetType": "text",
501 | "quantity": 8,
502 | "props": "JS{{\nvar props = {}\nprops.wrap= \"soft\"\nreturn props\n}}",
503 | "value": "",
504 | "default": "",
505 | "linkId": "",
506 | "address": "auto",
507 | "preArgs": "",
508 | "typeTags": "",
509 | "decimals": 2,
510 | "target": "",
511 | "ignoreDefaults": false,
512 | "bypass": false,
513 | "script": "",
514 | "widgets": [],
515 | "tabs": []
516 | }
517 | ],
518 | "tabs": []
519 | }
520 | ],
521 | "tabs": []
522 | }
523 | }
--------------------------------------------------------------------------------
/res/components/Port.svg:
--------------------------------------------------------------------------------
1 |
2 |
49 |
--------------------------------------------------------------------------------
/res/components/Screw.svg:
--------------------------------------------------------------------------------
1 |
2 |
31 |
--------------------------------------------------------------------------------
/res/components/SliderHandle.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/res/components/SliderHorizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
30 |
--------------------------------------------------------------------------------
/res/components/paw.svg:
--------------------------------------------------------------------------------
1 |
2 |
35 |
--------------------------------------------------------------------------------
/res/components/paw0.svg:
--------------------------------------------------------------------------------
1 |
2 |
30 |
--------------------------------------------------------------------------------
/res/components/paw1.svg:
--------------------------------------------------------------------------------
1 |
2 |
30 |
--------------------------------------------------------------------------------
/res/fonts/NovaMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Modular-Mind/oscelot/81c5b530bb39a9405714a974ac5d07241e303d18/res/fonts/NovaMono-Regular.ttf
--------------------------------------------------------------------------------
/res/fonts/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011, wmk69 (wmk69@o2.pl),
2 | with Reserved Font Name NovaMono.
3 |
4 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
5 | This license is copied below, and is also available with a FAQ at:
6 | http://scripts.sil.org/OFL
7 |
8 |
9 | -----------------------------------------------------------
10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11 | -----------------------------------------------------------
12 |
13 | PREAMBLE
14 | The goals of the Open Font License (OFL) are to stimulate worldwide
15 | development of collaborative font projects, to support the font creation
16 | efforts of academic and linguistic communities, and to provide a free and
17 | open framework in which fonts may be shared and improved in partnership
18 | with others.
19 |
20 | The OFL allows the licensed fonts to be used, studied, modified and
21 | redistributed freely as long as they are not sold by themselves. The
22 | fonts, including any derivative works, can be bundled, embedded,
23 | redistributed and/or sold with any software provided that any reserved
24 | names are not used by derivative works. The fonts and derivatives,
25 | however, cannot be released under any other type of license. The
26 | requirement for fonts to remain under this license does not apply
27 | to any document created using the fonts or their derivatives.
28 |
29 | DEFINITIONS
30 | "Font Software" refers to the set of files released by the Copyright
31 | Holder(s) under this license and clearly marked as such. This may
32 | include source files, build scripts and documentation.
33 |
34 | "Reserved Font Name" refers to any names specified as such after the
35 | copyright statement(s).
36 |
37 | "Original Version" refers to the collection of Font Software components as
38 | distributed by the Copyright Holder(s).
39 |
40 | "Modified Version" refers to any derivative made by adding to, deleting,
41 | or substituting -- in part or in whole -- any of the components of the
42 | Original Version, by changing formats or by porting the Font Software to a
43 | new environment.
44 |
45 | "Author" refers to any designer, engineer, programmer, technical
46 | writer or other person who contributed to the Font Software.
47 |
48 | PERMISSION & CONDITIONS
49 | Permission is hereby granted, free of charge, to any person obtaining
50 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
51 | redistribute, and sell modified and unmodified copies of the Font
52 | Software, subject to the following conditions:
53 |
54 | 1) Neither the Font Software nor any of its individual components,
55 | in Original or Modified Versions, may be sold by itself.
56 |
57 | 2) Original or Modified Versions of the Font Software may be bundled,
58 | redistributed and/or sold with any software, provided that each copy
59 | contains the above copyright notice and this license. These can be
60 | included either as stand-alone text files, human-readable headers or
61 | in the appropriate machine-readable metadata fields within text or
62 | binary files as long as those fields can be easily viewed by the user.
63 |
64 | 3) No Modified Version of the Font Software may use the Reserved Font
65 | Name(s) unless explicit written permission is granted by the corresponding
66 | Copyright Holder. This restriction only applies to the primary font name as
67 | presented to the users.
68 |
69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70 | Software shall not be used to promote, endorse or advertise any
71 | Modified Version, except to acknowledge the contribution(s) of the
72 | Copyright Holder(s) and the Author(s) or with their explicit written
73 | permission.
74 |
75 | 5) The Font Software, modified or unmodified, in part or in whole,
76 | must be distributed entirely under this license, and must not be
77 | distributed under any other license. The requirement for fonts to
78 | remain under this license does not apply to any document created
79 | using the Font Software.
80 |
81 | TERMINATION
82 | This license becomes null and void if any of the above conditions are
83 | not met.
84 |
85 | DISCLAIMER
86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94 | OTHER DEALINGS IN THE FONT SOFTWARE.
95 |
--------------------------------------------------------------------------------
/src/MapModuleBase.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "plugin.hpp"
3 | #include "settings.hpp"
4 | #include "components/ParamHandleIndicator.hpp"
5 | #include "components/OscelotParam.hpp"
6 |
7 | namespace TheModularMind {
8 |
9 | // Widgets
10 |
11 | template< int MAX_PARAMS, typename MODULE >
12 | struct MapModuleChoice : LedDisplayChoice {
13 | MODULE* module = NULL;
14 | bool processEvents = true;
15 | int id;
16 |
17 | std::chrono::time_point hscrollUpdate = std::chrono::system_clock::now();
18 | int hscrollCharOffset = 0;
19 |
20 | MapModuleChoice() {
21 | box.size = mm2px(Vec(0, 7.5));
22 | textOffset = Vec(6, 14.7);
23 | fontPath = asset::plugin(pluginInstance, "res/fonts/NovaMono-Regular.ttf");
24 | color = nvgRGB(0xf0, 0xf0, 0xf0);
25 | }
26 |
27 | ~MapModuleChoice() {
28 | if (module && module->learningId == id) {
29 | glfwSetCursor(APP->window->win, NULL);
30 | }
31 | }
32 |
33 | void setModule(MODULE* module) {
34 | this->module = module;
35 | }
36 |
37 | void onButton(const event::Button& e) override {
38 | e.stopPropagating();
39 | if (!module) return;
40 | if (module->locked) return;
41 |
42 | if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) {
43 | e.consume(this);
44 | }
45 |
46 | if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) {
47 | e.consume(this);
48 |
49 | if (module->paramHandles[id].moduleId >= 0) {
50 | createContextMenu();
51 | }
52 | else {
53 | module->clearMap(id);
54 | }
55 | }
56 | }
57 |
58 | void createContextMenu() {
59 |
60 | struct LabelMenuItem : MenuItem {
61 | MODULE* module;
62 | int id;
63 | std::string tempLabel;
64 |
65 | LabelMenuItem() {
66 | rightText = RIGHT_ARROW;
67 | }
68 |
69 | struct LabelField : ui::TextField {
70 | MODULE* module;
71 | int id;
72 | void onSelectKey(const event::SelectKey& e) override {
73 | if (e.action == GLFW_PRESS && e.key == GLFW_KEY_ENTER) {
74 | module->textLabels[id] = text;
75 |
76 | ui::MenuOverlay* overlay = getAncestorOfType();
77 | overlay->requestDelete();
78 | e.consume(this);
79 | }
80 |
81 | if (!e.getTarget()) {
82 | ui::TextField::onSelectKey(e);
83 | }
84 | }
85 | };
86 |
87 | Menu* createChildMenu() override {
88 | Menu* menu = new Menu;
89 |
90 | LabelField* labelField = new LabelField;
91 | labelField->placeholder = "Label";
92 | labelField->box.size.x = 220;
93 | labelField->module = module;
94 | labelField->id = id;
95 | labelField->text = module->textLabels[id];
96 | if(labelField->text==""){
97 | labelField->text=tempLabel;
98 | }
99 | menu->addChild(labelField);
100 | menu->addChild(createMenuItem("Reset", "", [=]() { module->textLabels[id] = ""; }));
101 |
102 | return menu;
103 | }
104 | }; // struct LabelMenuItem
105 |
106 | ui::Menu* menu = createMenu();
107 | menu->addChild(createMenuLabel("Parameter \"" + getParamName() + "\""));
108 |
109 | menu->addChild(construct(
110 | &MenuItem::text, "Custom label", &LabelMenuItem::module, module, &LabelMenuItem::id, id,
111 | &LabelMenuItem::tempLabel, getSlotPrefix() == ".... " ? getParamName() : getSlotPrefix() + getParamName()));
112 |
113 | menu->addChild(createMenuItem("Locate and indicate", "", [=]() {
114 | ParamHandle* paramHandle = &module->paramHandles[id];
115 | ModuleWidget* mw = APP->scene->rack->getModule(paramHandle->moduleId);
116 | module->paramHandleIndicator[id].indicate(mw);
117 | }));
118 |
119 | menu->addChild(new MenuSeparator());
120 | menu->addChild(createMenuItem("Unmap", "", [=]() { module->clearMap(id); }));
121 | appendContextMenu(menu);
122 | }
123 |
124 | virtual void appendContextMenu(Menu* menu) { }
125 |
126 | void onSelect(const event::Select& e) override {
127 | if (!module) return;
128 | if (module->locked) return;
129 |
130 | ScrollWidget *scroll = getAncestorOfType();
131 | scroll->scrollTo(box);
132 |
133 | // Reset touchedParam, unstable API
134 | APP->scene->rack->touchedParam = NULL;
135 | module->enableLearn(id);
136 |
137 | GLFWcursor* cursor = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR);
138 | glfwSetCursor(APP->window->win, cursor);
139 | }
140 |
141 | void onDeselect(const event::Deselect& e) override {
142 | if (!module) return;
143 | if (!processEvents) return;
144 |
145 | // Check if a ParamWidget was touched, unstable API
146 | ParamWidget *touchedParam = APP->scene->rack->touchedParam;
147 | if (touchedParam && touchedParam->getParamQuantity()->module != module) {
148 | APP->scene->rack->touchedParam = NULL;
149 | int64_t moduleId = touchedParam->getParamQuantity()->module->id;
150 | int paramId = touchedParam->getParamQuantity()->paramId;
151 | module->learnParam(id, moduleId, paramId);
152 | hscrollCharOffset = 0;
153 | }
154 | else {
155 | module->disableLearn(id);
156 | }
157 | glfwSetCursor(APP->window->win, NULL);
158 | }
159 |
160 | void step() override {
161 | if (!module)
162 | return;
163 |
164 | if (module->learningId == id) {
165 | bgColor = color;
166 | bgColor.a = 0.15;
167 | if (APP->event->getSelectedWidget() != this)
168 | APP->event->setSelectedWidget(this);
169 | }
170 | else {
171 | bgColor = nvgRGBA(0, 0, 0, 0);
172 | if (APP->event->getSelectedWidget() == this)
173 | APP->event->setSelectedWidget(NULL);
174 | }
175 |
176 | // Set text
177 | if (module->paramHandles[id].moduleId >= 0 && module->learningId != id) {
178 | std::string prefix = "";
179 | std::string label = getSlotLabel();
180 | if (label == "") {
181 | prefix = getSlotPrefix();
182 | label = getParamName();
183 | if (label == "") {
184 | module->clearMap(id);
185 | return;
186 | }
187 | }
188 |
189 | size_t hscrollMaxLength = ceil(box.size.x / 6.2f);
190 | if (module->textScrolling && label.length() + prefix.length() > hscrollMaxLength) {
191 | // Scroll the parameter-name horizontically
192 | text = prefix + label.substr(hscrollCharOffset > (int)label.length() ? 0 : hscrollCharOffset);
193 | auto now = std::chrono::system_clock::now();
194 | if (now - hscrollUpdate > std::chrono::milliseconds{100}) {
195 | hscrollCharOffset = (hscrollCharOffset + 1) % (label.length() + hscrollMaxLength);
196 | hscrollUpdate = now;
197 | }
198 | }
199 | else {
200 | text = prefix + label;
201 | }
202 | }
203 | else {
204 | if (module->learningId == id) {
205 | text = getSlotPrefix() + "Mapping...";
206 | } else {
207 | text = getSlotPrefix() + "Unmapped";
208 | }
209 | }
210 |
211 | // Set text color
212 | if (module->paramHandles[id].moduleId >= 0 || module->learningId == id) {
213 | color.a = 0.9;
214 | }
215 | else {
216 | color.a = 0.5;
217 | }
218 | }
219 |
220 | virtual std::string getSlotLabel() {
221 | return "";
222 | }
223 |
224 | virtual std::string getSlotPrefix() {
225 | return MAX_PARAMS > 1 ? string::f("%02d ", id + 1) : "";
226 | }
227 |
228 | ParamQuantity* getParamQuantity() {
229 | if (!module)
230 | return NULL;
231 | if (id >= module->mapLen)
232 | return NULL;
233 | ParamHandle* paramHandle = &module->paramHandles[id];
234 | if (paramHandle->moduleId < 0)
235 | return NULL;
236 | ModuleWidget *mw = APP->scene->rack->getModule(paramHandle->moduleId);
237 | if (!mw)
238 | return NULL;
239 | // Get the Module from the ModuleWidget instead of the ParamHandle.
240 | // I think this is more elegant since this method is called in the app world instead of the engine world.
241 | Module* m = mw->module;
242 | if (!m)
243 | return NULL;
244 | int paramId = paramHandle->paramId;
245 | if (paramId >= (int) m->params.size())
246 | return NULL;
247 | ParamQuantity* paramQuantity = m->paramQuantities[paramId];
248 | return paramQuantity;
249 | }
250 |
251 | std::string getParamName() {
252 | if (!module)
253 | return "";
254 | if (id >= module->mapLen)
255 | return "";
256 | ParamHandle* paramHandle = &module->paramHandles[id];
257 | if (paramHandle->moduleId < 0)
258 | return "";
259 | ModuleWidget *mw = APP->scene->rack->getModule(paramHandle->moduleId);
260 | if (!mw)
261 | return "";
262 | // Get the Module from the ModuleWidget instead of the ParamHandle.
263 | // I think this is more elegant since this method is called in the app world instead of the engine world.
264 | Module* m = mw->module;
265 | if (!m)
266 | return "";
267 | int paramId = paramHandle->paramId;
268 | if (paramId >= (int) m->params.size())
269 | return "";
270 | ParamQuantity* paramQuantity = m->paramQuantities[paramId];
271 | std::string s;
272 | s += mw->model->name;
273 | s += " > ";
274 | s += paramQuantity->name;
275 | return s;
276 | }
277 |
278 | void drawLayer(const DrawArgs& args, int layer) override {
279 | if (layer == 1) {
280 | if (bgColor.a > 0.0) {
281 | nvgScissor(args.vg, RECT_ARGS(args.clipBox));
282 | nvgBeginPath(args.vg);
283 | nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
284 | nvgFillColor(args.vg, bgColor);
285 | nvgFill(args.vg);
286 | nvgResetScissor(args.vg);
287 | }
288 |
289 | std::shared_ptr font = APP->window->loadFont(fontPath);
290 |
291 | if (font && font->handle >= 0) {
292 | Rect r = Rect(textOffset.x, 0.f, box.size.x - textOffset.x * 2, box.size.y).intersect(args.clipBox);
293 | nvgScissor(args.vg, RECT_ARGS(r));
294 | nvgFillColor(args.vg, color);
295 | nvgFontFaceId(args.vg, font->handle);
296 | nvgTextLetterSpacing(args.vg, 0.0);
297 | nvgFontSize(args.vg, 14);
298 | nvgText(args.vg, textOffset.x, textOffset.y, text.c_str(), NULL);
299 | nvgResetScissor(args.vg);
300 | }
301 | }
302 | }
303 | };
304 |
305 | struct OscelotScrollWidget : ScrollWidget {
306 | void draw(const DrawArgs& args) override {
307 | NVGcolor color = color::BLACK;
308 | nvgScissor(args.vg, RECT_ARGS(args.clipBox));
309 | Widget::draw(args);
310 | nvgResetScissor(args.vg);
311 |
312 | if(verticalScrollbar->visible){
313 | color.a = 0.5;
314 | nvgBeginPath(args.vg);
315 | nvgRoundedRect(args.vg, verticalScrollbar->box.pos.x, verticalScrollbar->box.pos.y, verticalScrollbar->box.size.x, verticalScrollbar->box.size.y, 3.0);
316 | nvgFillColor(args.vg, color);
317 | nvgFill(args.vg);
318 |
319 | color.a = 0.4;
320 | nvgBeginPath(args.vg);
321 | nvgRoundedRect(args.vg, verticalScrollbar->box.pos.x+1, verticalScrollbar->box.pos.y+1, verticalScrollbar->box.size.x - 2, verticalScrollbar->box.size.y - 2, 3.0);
322 | nvgFillColor(args.vg, color);
323 | nvgFill(args.vg);
324 | }
325 | }
326 | };
327 |
328 | template< int MAX_PARAMS, typename MODULE, typename CHOICE = MapModuleChoice >
329 | struct MapModuleDisplay : LedDisplay {
330 | MODULE* module;
331 | ScrollWidget* scroll;
332 | CHOICE* choices[MAX_PARAMS];
333 |
334 | ~MapModuleDisplay() {
335 | for (int id = 0; id < MAX_PARAMS; id++) {
336 | choices[id]->processEvents = false;
337 | }
338 | }
339 |
340 | void setModule(MODULE* module) {
341 | this->module = module;
342 |
343 | scroll = new OscelotScrollWidget();
344 | scroll->box.size.x = box.size.x;
345 | scroll->box.size.y = box.size.y - scroll->box.pos.y;
346 | scroll->verticalScrollbar->box.size.x = 8.0f;
347 |
348 | addChild(scroll);
349 |
350 | Vec pos;
351 | for (int id = 0; id < MAX_PARAMS; id++) {
352 | CHOICE* choice = createWidget(pos);
353 | choice->box.size.x = box.size.x;
354 | choice->id = id;
355 | choice->setModule(module);
356 | scroll->container->addChild(choice);
357 | choices[id] = choice;
358 |
359 | pos = choice->box.getBottomLeft();
360 | }
361 | }
362 |
363 | void draw(const DrawArgs& args) override {
364 | NVGcolor bgColor = color::BLACK;
365 | bgColor.a=0.0;
366 | // LedDisplay::draw(args);
367 |
368 | nvgBeginPath(args.vg);
369 | nvgRoundedRect(args.vg, 0, 0, box.size.x, box.size.y, 5.0);
370 | nvgFillColor(args.vg, bgColor);
371 | nvgFill(args.vg);
372 |
373 | nvgScissor(args.vg, RECT_ARGS(args.clipBox));
374 | Widget::draw(args);
375 | nvgResetScissor(args.vg);
376 |
377 | if (module && module->locked) {
378 | NVGcolor fColor = color::WHITE;
379 | fColor.a=0.10;
380 | nvgBeginPath(args.vg);
381 | nvgRoundedRect(args.vg, -2, -5, box.size.x + 8, box.size.y + 7, 25.0);
382 | nvgFillColor(args.vg, fColor);
383 | nvgFill(args.vg);
384 | }
385 | }
386 |
387 | void onHoverScroll(const event::HoverScroll& e) override {
388 | if (module && module->locked) {
389 | e.stopPropagating();
390 | }
391 | LedDisplay::onHoverScroll(e);
392 | }
393 | };
394 |
395 |
396 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/OscController.cpp:
--------------------------------------------------------------------------------
1 | #include "osc/OscController.hpp"
2 | #include
3 |
4 | namespace TheModularMind {
5 |
6 | class OscFader : public OscController {
7 | public:
8 | OscFader(std::string address, int controllerId, CONTROLLERMODE controllerMode, float value, uint32_t ts) {
9 | this->setTypeString("FDR");
10 | this->setAddress(address);
11 | this->setControllerId(controllerId);
12 | this->setControllerMode(controllerMode);
13 | OscController::setCurrentValue(value, ts);
14 | }
15 |
16 | virtual bool setCurrentValue(float value, uint32_t ts) override {
17 | if (ts == 0 || ts > this->getTs()) {
18 | return OscController::setCurrentValue(value, ts);
19 | }
20 | return false;
21 | }
22 | };
23 |
24 | class OscEncoder : public OscController {
25 | public:
26 | OscEncoder(std::string address, int controllerId, float value, uint32_t ts, int sensitivity = ENCODER_DEFAULT_SENSITIVITY) {
27 | this->setTypeString("ENC");
28 | this->setAddress(address);
29 | this->setControllerId(controllerId);
30 | this->setControllerMode(CONTROLLERMODE::DIRECT);
31 | this->setSensitivity(sensitivity);
32 | this->setCurrentValue(value, ts);
33 | }
34 |
35 | virtual bool setCurrentValue(float value, uint32_t ts) override {
36 | if (ts == 0) {
37 | OscController::setCurrentValue(value, ts);
38 | } else if (ts > this->getTs()) {
39 | float newValue = this->getCurrentValue() + (value / float(sensitivity));
40 | OscController::setCurrentValue(rack::math::clamp(newValue, 0.f, 1.f), ts);
41 | }
42 | return this->getCurrentValue() >= 0.f;
43 | }
44 |
45 | void setSensitivity(int sensitivity) override { this->sensitivity = sensitivity; }
46 | int getSensitivity() override { return this->sensitivity; }
47 |
48 | private:
49 | int sensitivity = ENCODER_DEFAULT_SENSITIVITY;
50 | };
51 |
52 | class OscButton : public OscController {
53 | public:
54 | OscButton(std::string address, int controllerId, CONTROLLERMODE controllerMode, float value, uint32_t ts) {
55 | this->setTypeString("BTN");
56 | this->setAddress(address);
57 | this->setControllerId(controllerId);
58 | this->setControllerMode(controllerMode);
59 | OscController::setCurrentValue(value, ts);
60 | }
61 |
62 | virtual bool setCurrentValue(float value, uint32_t ts) override {
63 | if (ts == 0) {
64 | OscController::setCurrentValue(value, ts);
65 | } else if (ts > this->getTs()) {
66 | OscController::setCurrentValue(rack::math::clamp(value, 0.f, 1.0f), ts);
67 | }
68 | return this->getCurrentValue() >= 0.f;
69 | }
70 | };
71 |
72 | bool endsWith(std::string const &fullString, std::string const &ending) {
73 | if (fullString.length() >= ending.length()) {
74 | return (0 == fullString.compare(fullString.length() - ending.length(), ending.length(), ending));
75 | } else {
76 | return false;
77 | }
78 | }
79 |
80 | OscController *OscController::Create(std::string address, int controllerId, CONTROLLERMODE controllerMode, float value, uint32_t ts) {
81 | if (endsWith(address, "/fader")) {
82 | return new OscFader(address, controllerId, controllerMode, value, ts);
83 | } else if (endsWith(address, "/encoder")) {
84 | return new OscEncoder(address, controllerId, value, ts);
85 | } else if (endsWith(address, "/button")) {
86 | return new OscButton(address, controllerId, controllerMode, value, ts);
87 | } else
88 | INFO("Not Implemented for address: %s", address.c_str());
89 | return nullptr;
90 | };
91 |
92 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/Oscelot.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "plugin.hpp"
3 | #include "osc/OscSender.hpp"
4 | #include "osc/OscReceiver.hpp"
5 | #include "components/LedTextField.hpp"
6 | #include "components/MeowMory.hpp"
7 | #include "osc/OscController.hpp"
8 |
9 | namespace TheModularMind {
10 | namespace Oscelot {
11 |
12 | static const int MAX_PARAMS = 320;
13 | static const std::string RXPORT_DEFAULT = "8881";
14 | static const std::string TXPORT_DEFAULT = "8880";
15 |
16 | } // namespace Oscelot
17 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/OscelotExpander.cpp:
--------------------------------------------------------------------------------
1 | #include "OscelotExpander.hpp"
2 |
3 | namespace TheModularMind {
4 | namespace Oscelot {
5 |
6 | struct OscelotExpander : Module {
7 | enum ParamIds { NUM_PARAMS };
8 | enum InputIds { NUM_INPUTS };
9 | enum OutputIds { ENUMS(TRIG_OUTPUT, 8), ENUMS(CV_OUTPUT, 8), ENUMS(POLY_OUTPUT, 2), NUM_OUTPUTS };
10 | enum LightIds { NUM_LIGHTS };
11 |
12 | int panelTheme = rand() % 3;
13 | int expanderId;
14 | int startVoltageIndex = 1;
15 | int endVoltageIndex = 7;
16 | float controlVoltages[9] = { -10.0f, -5.0f, -3.0f, -1.0f , 0.0f, 1.0f, 3.0f, 5.0f, 10.0f };
17 | dsp::ClockDivider processDivider;
18 | dsp::PulseGenerator pulseGenerator[8];
19 | simd::float_4 last[2];
20 | std::string labels[8];
21 |
22 | OscelotExpander() {
23 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
24 | onReset();
25 | }
26 |
27 | void onReset() override {
28 | processDivider.setDivision(64);
29 | processDivider.reset();
30 |
31 | for (int i = 0; i < 8; i++) {
32 | labels[i] = "";
33 | last[i / 4][i % 4] = 0.0f;
34 | pulseGenerator[i].reset();
35 | outputs[CV_OUTPUT + i].clearVoltages();
36 | outputs[POLY_OUTPUT_LAST].clearVoltages();
37 | }
38 | expanderId = 0;
39 | rightExpander.producerMessage = NULL;
40 | rightExpander.messageFlipRequested = false;
41 | }
42 |
43 | void process(const ProcessArgs& args) override {
44 | if (processDivider.process()) {
45 | Module* expanderMother = leftExpander.module;
46 | OscelotExpanderBase* module;
47 |
48 | if (!expanderMother || (expanderMother->model != modelOSCelot && expanderMother->model != modelOscelotExpander) || !expanderMother->rightExpander.consumerMessage) {
49 | onReset();
50 | return;
51 | }
52 |
53 | ExpanderPayload* expPayload = reinterpret_cast(expanderMother->rightExpander.consumerMessage);
54 | module = reinterpret_cast(expPayload->base);
55 | expanderId = expPayload->expanderId;
56 |
57 | if (expanderId + 8 > MAX_PARAMS) return;
58 |
59 | float* values = module->expGetValues();
60 | std::string* controllerLabels = module->expGetLabels();
61 | outputs[POLY_OUTPUT].setChannels(8);
62 | outputs[POLY_OUTPUT_LAST].setChannels(8);
63 |
64 | for (int i = 0; i < 8; i++) {
65 | float v = values[i + expanderId];
66 | if (v < 0.0f) return;
67 | labels[i] = controllerLabels[i + expanderId];
68 |
69 | bool trig = simd::ifelse(last[i / 4][i % 4] != v, 1, 0);
70 |
71 | if (trig) {
72 | pulseGenerator[i].trigger(1e-3);
73 | last[i / 4][i % 4] = v;
74 | }
75 |
76 | simd::float_4 trigVoltage = pulseGenerator[i].process(args.sampleTime) ? 10.f : 0.f;
77 | simd::float_4 cvVoltage = simd::rescale(v, 0.0f, 1.0f, controlVoltages[startVoltageIndex], controlVoltages[endVoltageIndex]);
78 |
79 | outputs[TRIG_OUTPUT + i].setVoltage(trigVoltage[0]);
80 | outputs[POLY_OUTPUT].setVoltage(trigVoltage[0], i);
81 | outputs[CV_OUTPUT + i].setVoltage(cvVoltage[0]);
82 | outputs[POLY_OUTPUT_LAST].setVoltage(cvVoltage[0], i);
83 | }
84 |
85 | rightExpander.producerMessage = new ExpanderPayload(module, expanderId + 8);
86 | rightExpander.messageFlipRequested = true;
87 | }
88 | }
89 |
90 | json_t* dataToJson() override {
91 | json_t* rootJ = json_object();
92 | json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme));
93 | json_object_set_new(rootJ, "startVoltageIndex", json_real(startVoltageIndex));
94 | json_object_set_new(rootJ, "endVoltageIndex", json_real(endVoltageIndex));
95 | return rootJ;
96 | }
97 |
98 | void dataFromJson(json_t* rootJ) override {
99 | panelTheme = json_integer_value(json_object_get(rootJ, "panelTheme"));
100 | startVoltageIndex = json_real_value(json_object_get(rootJ, "startVoltageIndex"));
101 | endVoltageIndex = json_real_value(json_object_get(rootJ, "endVoltageIndex"));
102 | }
103 | };
104 |
105 | struct OscLabelWidget : widget::OpaqueWidget {
106 | OscelotExpander* module;
107 | OscelotTextLabel* labelWidgets[8];
108 | OscelotTextLabel* idLabel;
109 | NVGcolor color = nvgRGB(0xDA, 0xa5, 0x20);
110 | NVGcolor white = nvgRGB(0xfe, 0xff, 0xe0);
111 | Vec lblPos = box.pos;
112 |
113 | void step() override {
114 | if (!module) return;
115 |
116 | for (int i = 0; i < 8; i++) {
117 | if (labelWidgets[i]->text != module->labels[i]) labelWidgets[i]->text = module->labels[i];
118 | }
119 | }
120 |
121 | void setLabels() {
122 | clearChildren();
123 |
124 | OscelotTextLabel* l = createWidget(lblPos);
125 | l->box.size = mm2px(Vec(25.4, 1));
126 | l->text = "POLY";
127 | addChild(l);
128 | idLabel = l;
129 | lblPos = lblPos.plus(Vec(0.0f, 16.0f));
130 |
131 | for (int i = 0; i < 8; i++) {
132 | lblPos = lblPos.plus(Vec(0.0f, 36.0f));
133 | OscelotTextLabel* l = createWidget(lblPos);
134 | l->box.size = mm2px(Vec(25.4, 1));
135 | l->text = module->labels[i];
136 | addChild(l);
137 | labelWidgets[i] = l;
138 | }
139 | }
140 | };
141 |
142 | struct OscelotExpanderWidget : ThemedModuleWidget {
143 | OscelotExpanderWidget(OscelotExpander* module) : ThemedModuleWidget(module, "OscelotExpander", "Oscelot.md#expander") {
144 | setModule(module);
145 |
146 | addChild(createWidget(Vec(2 * RACK_GRID_WIDTH, 0)));
147 | addChild(createWidget(Vec(box.size.x - 3 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
148 |
149 | OscLabelWidget* oscLabelWidget = createWidget(mm2px(Vec(0, 7)));
150 | oscLabelWidget->module = module;
151 | if (module) {
152 | oscLabelWidget->setLabels();
153 | }
154 | addChild(oscLabelWidget);
155 |
156 | Vec gatePos = mm2px(Vec(7, 14));
157 | Vec cvPos = mm2px(Vec(18.4, 14));
158 |
159 | addOutput(createOutputCentered(gatePos, module, OscelotExpander::POLY_OUTPUT));
160 | addOutput(createOutputCentered(cvPos, module, OscelotExpander::POLY_OUTPUT_LAST));
161 | gatePos = gatePos.plus(Vec(0.0f, 16.0f));
162 | cvPos = cvPos.plus(Vec(0.0f, 16.0f));
163 |
164 | for (int i = 0; i < 8; i++) {
165 | gatePos = gatePos.plus(Vec(0.0f, 36.0f));
166 | cvPos = cvPos.plus(Vec(0.0f, 36.0f));
167 | addOutput(createOutputCentered(gatePos, module, OscelotExpander::TRIG_OUTPUT + i));
168 | addOutput(createOutputCentered(cvPos, module, OscelotExpander::CV_OUTPUT + i));
169 | }
170 | }
171 | void appendContextMenu(Menu* menu) override {
172 | ThemedModuleWidget::appendContextMenu(menu);
173 | assert(module);
174 |
175 | menu->addChild(new MenuSeparator());
176 | menu->addChild(createMenuLabel(string::f("CV Range: %.0fV to %.0fV", module->controlVoltages[ module->startVoltageIndex], module->controlVoltages[ module->endVoltageIndex])));
177 |
178 | menu->addChild(createSubmenuItem("Configure CV", "", [=](Menu* menu) {
179 | menu->addChild(createSubmenuItem(string::f("Voltage Range (%.0fV)", abs(module->controlVoltages[module->startVoltageIndex] - module->controlVoltages[module->endVoltageIndex])), "", [=](Menu* menu) {
180 | menu->addChild(createCheckMenuItem(
181 | "-1V to 1V", "", [=]() { return module->startVoltageIndex == 3 && module->endVoltageIndex == 5; },
182 | [=]() {
183 | module->startVoltageIndex = 3;
184 | module->endVoltageIndex = 5;
185 | }));
186 | menu->addChild(createCheckMenuItem(
187 | "-3V to 3V", "", [=]() { return module->startVoltageIndex == 2 && module->endVoltageIndex == 6; },
188 | [=]() {
189 | module->startVoltageIndex = 2;
190 | module->endVoltageIndex = 6;
191 | }));
192 | menu->addChild(createCheckMenuItem(
193 | "-5V to 5V", "", [=]() { return module->startVoltageIndex == 1 && module->endVoltageIndex == 7; },
194 | [=]() {
195 | module->startVoltageIndex = 1;
196 | module->endVoltageIndex = 7;
197 | }));
198 | menu->addChild(createCheckMenuItem(
199 | "-10V to 10V", "", [=]() { return module->startVoltageIndex == 0 && module->endVoltageIndex == 8; },
200 | [=]() {
201 | module->startVoltageIndex = 0;
202 | module->endVoltageIndex = 8;
203 | }));
204 | menu->addChild(createCheckMenuItem(
205 | "0V to 1V", "", [=]() { return module->startVoltageIndex == 4 && module->endVoltageIndex == 5; },
206 | [=]() {
207 | module->startVoltageIndex = 4;
208 | module->endVoltageIndex = 5;
209 | }));
210 | menu->addChild(createCheckMenuItem(
211 | "0V to 3V", "", [=]() { return module->startVoltageIndex == 4 && module->endVoltageIndex == 6; },
212 | [=]() {
213 | module->startVoltageIndex = 4;
214 | module->endVoltageIndex = 6;
215 | }));
216 | menu->addChild(createCheckMenuItem(
217 | "0V to 5V", "", [=]() { return module->startVoltageIndex == 4 && module->endVoltageIndex == 7; },
218 | [=]() {
219 | module->startVoltageIndex = 4;
220 | module->endVoltageIndex = 7;
221 | }));
222 | menu->addChild(createCheckMenuItem(
223 | "0V to 10V", "", [=]() { return module->startVoltageIndex == 4 && module->endVoltageIndex == 8; },
224 | [=]() {
225 | module->startVoltageIndex = 4;
226 | module->endVoltageIndex = 8;
227 | }));
228 | }));
229 | menu->addChild(createIndexPtrSubmenuItem("Start Voltage", { "-10 V", "-5 V", "-3 V", "-1 V" , "0 V", "1 V", "3 V", "5 V", "10 V" }, &module->startVoltageIndex));
230 | menu->addChild(createIndexPtrSubmenuItem("End Voltage", { "-10 V", "-5 V", "-3 V", "-1 V" , "0 V", "1 V", "3 V", "5 V", "10 V" }, &module->endVoltageIndex));
231 | }));
232 | }
233 | };
234 | } // namespace Oscelot
235 | } // namespace TheModularMind
236 |
237 | Model* modelOscelotExpander = createModel("OSCelotExpander");
--------------------------------------------------------------------------------
/src/OscelotExpander.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "plugin.hpp"
3 | #include "components/LedTextField.hpp"
4 | #include "Oscelot.hpp"
5 |
6 | namespace TheModularMind {
7 | namespace Oscelot {
8 |
9 | struct OscelotExpanderBase {
10 | virtual float* expGetValues() { return NULL; }
11 | virtual std::string* expGetLabels() { return NULL; }
12 | };
13 |
14 | struct ExpanderPayload {
15 | OscelotExpanderBase* base;
16 | int expanderId;
17 |
18 | ExpanderPayload(OscelotExpanderBase* base, int expanderId) {
19 | this->base = base;
20 | this->expanderId = expanderId;
21 | }
22 | };
23 |
24 | } // namespace Oscelot
25 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/components.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "plugin.hpp"
3 | #include "components/PawButtons.hpp"
4 | #include "ui/ThemedModuleWidget.hpp"
--------------------------------------------------------------------------------
/src/components/LedTextField.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | namespace TheModularMind {
4 |
5 | struct OscelotTextField : LedDisplayTextField {
6 | float textSize = 14.f;
7 | const static unsigned int defaultMaxTextLength = 5;
8 | unsigned int maxTextLength;
9 | NVGcolor bgColor;
10 | bool isFocused = false;
11 | bool doubleClick = false;
12 |
13 | OscelotTextField() {
14 | maxTextLength = defaultMaxTextLength;
15 | textOffset = math::Vec(-0.4f, -2.1f);
16 | color = nvgRGB(0xfe, 0xff, 0xe0);
17 | bgColor = color::BLACK;
18 | bgColor.a = 0.3;
19 | fontPath = asset::plugin(pluginInstance, "res/fonts/NovaMono-Regular.ttf");
20 | }
21 |
22 | void draw(const DrawArgs& args) override {
23 | // Background
24 | if (bgColor.a > 0.0) {
25 | nvgBeginPath(args.vg);
26 | nvgRoundedRect(args.vg, 0, 0, box.size.x, box.size.y, 5.0);
27 | nvgFillColor(args.vg, bgColor);
28 | nvgFill(args.vg);
29 | }
30 |
31 | Widget::draw(args);
32 | }
33 |
34 | void drawLayer(const DrawArgs& args, int layer) override {
35 | if (layer == 1) {
36 | nvgScissor(args.vg, RECT_ARGS(args.clipBox));
37 |
38 | std::shared_ptr font = APP->window->loadFont(fontPath);
39 | // Text
40 | if (font->handle >= 0) {
41 | bndSetFont(font->handle);
42 |
43 | NVGcolor highlightColor = color;
44 | highlightColor.a = 0.5;
45 | color.a = 0.9;
46 | int begin = std::min(cursor, selection);
47 | int end = (this == APP->event->selectedWidget) ? std::max(cursor, selection) : -1;
48 | bndIconLabelCaret(args.vg, textOffset.x, textOffset.y, box.size.x - 2 * textOffset.x, box.size.y - 2 * textOffset.y, -1, color, textSize, text.c_str(),
49 | highlightColor, begin, end);
50 |
51 | bndSetFont(APP->window->uiFont->handle);
52 | }
53 | nvgResetScissor(args.vg);
54 | }
55 | }
56 |
57 | void onSelect(const event::Select& e) override {
58 | isFocused = true;
59 | e.consume(this);
60 | }
61 |
62 | void onDeselect(const event::Deselect& e) override {
63 | isFocused = false;
64 | LedDisplayTextField::setText(TextField::text);
65 | e.consume(NULL);
66 | }
67 |
68 | void onAction(const event::Action& e) override {
69 | // this gets fired when the user types 'enter'
70 | event::Deselect eDeselect;
71 | onDeselect(eDeselect);
72 | APP->event->selectedWidget = NULL;
73 | e.consume(NULL);
74 | }
75 |
76 | void onDoubleClick(const event::DoubleClick& e) override { doubleClick = true; }
77 |
78 | void onButton(const event::Button& e) override {
79 | if (e.button == GLFW_MOUSE_BUTTON_LEFT && e.action == GLFW_RELEASE) {
80 | if (doubleClick) {
81 | doubleClick = false;
82 | selectAll();
83 | }
84 | }
85 | LedDisplayTextField::onButton(e);
86 | }
87 |
88 | void onSelectText(const event::SelectText& e) override {
89 | if (TextField::text.size() < maxTextLength || cursor != selection) {
90 | LedDisplayTextField::onSelectText(e);
91 | } else {
92 | e.consume(NULL);
93 | }
94 | }
95 | };
96 |
97 | struct OscelotTextLabel : ui::Label {
98 | float textSize = 8.f;
99 | bool drawBackground = true;
100 | NVGcolor color;
101 | NVGcolor bgColor;
102 |
103 | OscelotTextLabel() {
104 | color = nvgRGB(0xfe, 0xff, 0xe0);
105 | bgColor = color::WHITE;
106 | color.a = 0.9;
107 | bgColor.a = 0.05;
108 | }
109 |
110 | void drawLabel(const DrawArgs& args, float x0, float y0, float w, const char* label, int size) {
111 | float constexpr padMargin = 3;
112 | nvgBeginPath(args.vg);
113 |
114 | nvgFontSize(args.vg, size);
115 | nvgTextAlign(args.vg, NVG_ALIGN_TOP | NVG_ALIGN_CENTER);
116 | nvgFillColor(args.vg, color);
117 | nvgText(args.vg, x0 + w / 2, y0, label, NULL);
118 |
119 | if (drawBackground) {
120 | float bounds[4];
121 | nvgTextBounds(args.vg, x0 + w / 2, y0, label, NULL, bounds);
122 | nvgBeginPath(args.vg);
123 | nvgRoundedRect(args.vg, 2 * padMargin, bounds[1] - (padMargin / 2.0), w - 4 * padMargin, 3 * (bounds[3] - bounds[1] + padMargin), 5.0);
124 | nvgFillColor(args.vg, bgColor);
125 | nvgFill(args.vg);
126 | }
127 | }
128 |
129 | void draw(const DrawArgs& args) override { drawLabel(args, 0.f, box.size.y, box.size.x, text.c_str(), textSize); }
130 | };
131 |
132 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/components/MeowMory.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "../osc/OscController.hpp"
3 |
4 | namespace TheModularMind {
5 |
6 | struct ModuleMeowMoryParam {
7 | int paramId = -1;
8 | std::string address;
9 | int controllerId = -1;
10 | int encSensitivity = OscController::ENCODER_DEFAULT_SENSITIVITY;
11 | CONTROLLERMODE controllerMode;
12 | std::string label = "";
13 |
14 | void fromMappings(ParamHandle paramHandle, OscController* oscController, std::string textLabel) {
15 | if (paramHandle.moduleId != -1) paramId = paramHandle.paramId;
16 | label = textLabel;
17 |
18 | if (oscController) {
19 | controllerId = oscController->getControllerId();
20 | address = oscController->getAddress();
21 | controllerMode = oscController->getControllerMode();
22 | if (oscController->getSensitivity() != OscController::ENCODER_DEFAULT_SENSITIVITY) encSensitivity = oscController->getSensitivity();
23 | }
24 | }
25 |
26 | void fromJson(json_t* meowMoryParamJ) {
27 | paramId = json_integer_value(json_object_get(meowMoryParamJ, "paramId"));
28 |
29 | json_t* labelJ = json_object_get(meowMoryParamJ, "label");
30 | if (labelJ) label = json_string_value(labelJ);
31 |
32 | json_t* controllerIdJ = json_object_get(meowMoryParamJ, "controllerId");
33 | if (controllerIdJ) {
34 | address = json_string_value(json_object_get(meowMoryParamJ, "address"));
35 | controllerMode = (CONTROLLERMODE)json_integer_value(json_object_get(meowMoryParamJ, "controllerMode"));
36 | controllerId = json_integer_value(controllerIdJ);
37 |
38 | json_t* encSensitivityJ = json_object_get(meowMoryParamJ, "encSensitivity");
39 | if (encSensitivityJ) encSensitivity = json_integer_value(encSensitivityJ);
40 | }
41 | }
42 |
43 | json_t* toJson() {
44 | json_t* meowMoryParamJ = json_object();
45 | if (paramId != -1) {
46 | json_object_set_new(meowMoryParamJ, "paramId", json_integer(paramId));
47 | }
48 | if (controllerId != -1) {
49 | json_object_set_new(meowMoryParamJ, "controllerId", json_integer(controllerId));
50 | json_object_set_new(meowMoryParamJ, "controllerMode", json_integer((int)controllerMode));
51 | json_object_set_new(meowMoryParamJ, "address", json_string(address.c_str()));
52 | if (encSensitivity != OscController::ENCODER_DEFAULT_SENSITIVITY) json_object_set_new(meowMoryParamJ, "encSensitivity", json_integer(encSensitivity));
53 | }
54 | if (label != "") json_object_set_new(meowMoryParamJ, "label", json_string(label.c_str()));
55 |
56 | return meowMoryParamJ;
57 | }
58 | };
59 |
60 | struct BankMeowMoryParam : ModuleMeowMoryParam {
61 | int64_t moduleId = -1;
62 |
63 | void fromMappings(ParamHandle paramHandle, OscController* oscController, std::string textLabel) {
64 | ModuleMeowMoryParam::fromMappings(paramHandle, oscController, textLabel);
65 | if (paramHandle.moduleId != -1) moduleId = paramHandle.moduleId;
66 | }
67 |
68 | void fromJson(json_t* bankMeowMoryParamJ) {
69 | ModuleMeowMoryParam::fromJson(bankMeowMoryParamJ);
70 | moduleId = json_integer_value(json_object_get(bankMeowMoryParamJ, "moduleId"));
71 | }
72 |
73 | json_t* toJson() {
74 | json_t* bankMeowMoryParamJ = json_object();
75 | bankMeowMoryParamJ = ModuleMeowMoryParam::toJson();
76 | if (moduleId > 0) json_object_set_new(bankMeowMoryParamJ, "moduleId", json_integer(moduleId));
77 | return bankMeowMoryParamJ;
78 | }
79 | };
80 |
81 | struct ModuleMeowMory {
82 | std::string pluginName;
83 | std::string moduleName;
84 | std::list paramArray;
85 |
86 | ~ModuleMeowMory() { paramArray.clear(); }
87 |
88 | json_t* toJson() {
89 | json_t* meowMoryJ = json_object();
90 | json_object_set_new(meowMoryJ, "pluginName", json_string(pluginName.c_str()));
91 | json_object_set_new(meowMoryJ, "moduleName", json_string(moduleName.c_str()));
92 | json_t* paramArrayJ = json_array();
93 | for (auto& param : paramArray) {
94 | json_array_append_new(paramArrayJ, param.toJson());
95 | }
96 | json_object_set_new(meowMoryJ, "params", paramArrayJ);
97 | return meowMoryJ;
98 | }
99 |
100 | void fromJson(json_t* meowMoryJ) {
101 | pluginName = json_string_value(json_object_get(meowMoryJ, "pluginName"));
102 | moduleName = json_string_value(json_object_get(meowMoryJ, "moduleName"));
103 | json_t* paramArrayJ = json_object_get(meowMoryJ, "params");
104 | size_t j;
105 | json_t* meowMoryParamJ;
106 | json_array_foreach(paramArrayJ, j, meowMoryParamJ) {
107 | ModuleMeowMoryParam meowMoryParam = ModuleMeowMoryParam();
108 | meowMoryParam.fromJson(meowMoryParamJ);
109 | paramArray.push_back(meowMoryParam);
110 | }
111 | }
112 | };
113 |
114 | struct BankMeowMory {
115 | std::list bankParamArray;
116 |
117 | ~BankMeowMory() { bankParamArray.clear(); }
118 |
119 | json_t* toJson() {
120 | json_t* bankMeowMoryJ = json_object();
121 | json_t* bankParamJ = json_object();
122 | json_t* bankParamArrayJ = json_array();
123 | for (auto& bankParam : bankParamArray) {
124 | bankParamJ = bankParam.toJson();
125 | if (json_object_size(bankParamJ) > 0) json_array_append_new(bankParamArrayJ, bankParamJ);
126 | }
127 | if (json_array_size(bankParamArrayJ) > 0) {
128 | json_object_set_new(bankMeowMoryJ, "params", bankParamArrayJ);
129 | }
130 | return bankMeowMoryJ;
131 | }
132 |
133 | void fromJson(json_t* bankMeowMoryJ) {
134 | json_t* bankParamArrayJ = json_object_get(bankMeowMoryJ, "params");
135 | size_t j;
136 | json_t* bankMeowMoryParamJ;
137 | json_array_foreach(bankParamArrayJ, j, bankMeowMoryParamJ) {
138 | BankMeowMoryParam bankMeowMoryParam;
139 | bankMeowMoryParam.fromJson(bankMeowMoryParamJ);
140 | bankParamArray.push_back(bankMeowMoryParam);
141 | }
142 | }
143 | };
144 |
145 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/components/OscelotParam.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | namespace TheModularMind {
4 |
5 | struct OscelotParam {
6 | ParamQuantity* paramQuantity = NULL;
7 | float limitMin;
8 | float limitMax;
9 | float uninit;
10 | float min = 0.f;
11 | float max = 1.f;
12 |
13 | float valueIn;
14 | float value;
15 | float valueOut;
16 | bool hasChanged;
17 |
18 | OscelotParam() { reset(); }
19 |
20 | bool isNear(float value, float jump = -1.0f) {
21 | if (value == -1.f) return false;
22 | float p = getValue();
23 | float delta3p = (limitMax - limitMin + 1) * 0.01f;
24 | bool r = p - delta3p <= value && value <= p + delta3p;
25 |
26 | if (jump >= 0.f) {
27 | float delta7p = (limitMax - limitMin + 1) * 0.03f;
28 | r = r && p - delta7p <= jump && jump <= p + delta7p;
29 | }
30 |
31 | return r;
32 | }
33 |
34 | void setLimits(float min, float max, float uninit) {
35 | limitMin = min;
36 | limitMax = max;
37 | this->uninit = uninit;
38 | }
39 | float getLimitMin() { return limitMin; }
40 | float getLimitMax() { return limitMax; }
41 |
42 | void reset(bool resetSettings = true) {
43 | paramQuantity = NULL;
44 | valueIn = uninit;
45 | value = -1.f;
46 | valueOut = std::numeric_limits::infinity();
47 | hasChanged = true;
48 |
49 | if (resetSettings) {
50 | min = 0.f;
51 | max = 1.f;
52 | }
53 | }
54 |
55 | void setParamQuantity(ParamQuantity* pq) {
56 | paramQuantity = pq;
57 | if (paramQuantity && valueOut == std::numeric_limits::infinity()) {
58 | valueOut = paramQuantity->getScaledValue();
59 | }
60 | }
61 |
62 | void setMin(float v) {
63 | min = v;
64 | if (paramQuantity && valueIn != -1) setValue(valueIn);
65 | }
66 | float getMin() { return min; }
67 |
68 | void setMax(float v) {
69 | max = v;
70 | if (paramQuantity && valueIn != -1) setValue(valueIn);
71 | }
72 | float getMax() { return max; }
73 |
74 | void setValue(float i) {
75 | float f = rescale(i, limitMin, limitMax, min, max);
76 | f = clamp(f, 0.f, 1.f);
77 | valueIn = i;
78 | value = f;
79 | }
80 |
81 | void process(float sampleTime = -1.f, bool force = false) {
82 | if (valueOut == std::numeric_limits::infinity()) return;
83 |
84 | if (valueOut != value || force) {
85 | paramQuantity->setScaledValue(value);
86 | valueOut = value;
87 | }
88 | }
89 |
90 | float getValue() {
91 | float f = paramQuantity->getScaledValue();
92 | if (valueOut == std::numeric_limits::infinity()) value = valueOut = f;
93 | f = rescale(f, min, max, limitMin, limitMax);
94 | f = clamp(f, limitMin, limitMax);
95 | if (valueIn == uninit) valueIn = f;
96 | return f;
97 | }
98 | }; // struct OscelotParam
99 |
100 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/components/ParamHandleIndicator.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | namespace TheModularMind {
4 |
5 | struct ParamHandleIndicator {
6 | ParamHandle* handle = NULL;
7 | NVGcolor color;
8 |
9 | int indicateCount = 0;
10 | float sampletime;
11 |
12 | void process(float sampleTime, bool force = false) {
13 | if (!handle) return;
14 | if (indicateCount > 0 || force) {
15 | this->sampletime += sampleTime;
16 | if (this->sampletime > 0.2f) {
17 | this->sampletime = 0;
18 | indicateCount--;
19 | handle->color = std::abs(indicateCount) % 2 == 1 ? color::BLACK : color;
20 | }
21 | }
22 | else {
23 | handle->color = color;
24 | }
25 | }
26 |
27 | void indicate(ModuleWidget* mw) {
28 | if (indicateCount > 0) return;
29 | if (mw) {
30 | // Move the view to center the mapped module
31 | TheModularMind::Rack::ViewportCenter{mw,1.5f};
32 | }
33 | indicateCount = 20;
34 | }
35 | }; // struct ParamHandleIndicator
36 |
37 | } // namespace TheModularMind
--------------------------------------------------------------------------------
/src/components/PawButtons.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "LedTextField.hpp"
3 |
4 | namespace TheModularMind {
5 |
6 | struct PawLight : SvgLight {
7 | std::shared_ptr