├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── plugin.json ├── res ├── Array.svg ├── Miniramp.svg ├── Ministep.svg └── fonts │ ├── -CooperHewitt-FontLog.txt │ ├── -OFL-FAQ.txt │ ├── APACHE2.txt │ ├── OFL.txt │ ├── Overpass-Black.ttf │ ├── Overpass-Bold.ttf │ ├── Overpass-ExtraBold.ttf │ ├── Overpass-Regular.ttf │ ├── RobotoMono-Bold.ttf │ └── RobotoMono-Regular.ttf ├── screenshots ├── envelope-chaining.png ├── interface.png ├── main.png ├── miniramp.png ├── ministep.png ├── record.png ├── sample-player.png ├── screenshots-patch.vcv ├── sequencer-envelope.png └── waveshaper.png └── src ├── Array.cpp ├── Miniramp.cpp ├── Ministep.cpp ├── Util.hpp ├── Widgets.cpp ├── Widgets.hpp ├── dr_wav.h ├── plugin.cpp └── plugin.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /plugin.dylib 4 | /plugin.dll 5 | /plugin.so 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for PdArray 2 | 3 | ## v2.1.1 (2024-05-07) 4 | 5 | Fixed used of undefined values in Ministep when module is added while plugin is bypassed (thanks @FalkTX) 6 | 7 | ## v2.1.0 (2023-01-16) 8 | 9 | Fairly large update to Miniramp. 10 | 11 | - New outputs: EOC (End of Cycle) and FINISH (inverse of GATE). This makes chaining Arrays easier (thanks @davephillips for the feature suggestion). 12 | - New input: STOP / reset 13 | - Miniramp is now wider, to fit the new input and outputs 14 | - Polyphonic behavior of Miniramp is now consistent with that of Ministep: if either the trigger or CV input are polyphonic, the output is polyphonic as well, and if the trigger, CV or STOP inputs are monophonic, they affect all channels 15 | - New right click menu option to update trigger duration only when a trigger is received 16 | - Duration and CV amount knob tooltips now show the duration / CV amount correctly in seconds 17 | - Extended Miniramp manual to explain polyphony 18 | 19 | Additional changes: 20 | 21 | - Updated screenshots in manual to Rack V2 22 | 23 | ## v2.0.6 (2021-12-29) 24 | 25 | Ported to Rack v2. 26 | 27 | - Implement v2 API features, such as input/output labels, and improved labels for switches. Also, glow-in-the-dark array cursors. 28 | - If the array contains more than 5000 elements, the array data is saved as a wav file in the patch storage directory 29 | - Improvements to the array size and Ministep step count text boxes 30 | 31 | As part of the v2 update, PdArray is now licensed under the EUPL. 32 | 33 | ## v1.0.6 (2020-11-14) 34 | 35 | - Fix crash when loading a sampe without resizing ([#13](https://github.com/mgunyho/PdArray/issues/13)) 36 | - Add right-click option to set array contents to zero 37 | - Add "Add fade in/out" option to array to prevent clicks ([#14](https://github.com/mgunyho/PdArray/issues/14)) 38 | - Add option to keep Miniramp value at 10V after the ramp has finished 39 | - Add section on clicking prevention in manual 40 | 41 | ## v1.0.5 (2020-06-19) 42 | 43 | - Added option to enable recording either with a gate or trigger signal 44 | - Added option to sort the array ([#11](https://github.com/mgunyho/PdArray/issues/11)) 45 | 46 | ## v1.0.4 (2020-06-11) 47 | 48 | - Add option to resize Array when loading an audio file ([#9](https://github.com/mgunyho/PdArray/issues/9)) 49 | - Add "Data persistence" option - it's now possible to avoid saving the array data to the patch file, or to automatically reload an audio file when the patch is loaded 50 | - Fixed issue with Ministep display (for real this time) ([#8](https://github.com/mgunyho/PdArray/issues/8)) 51 | 52 | ## v1.0.3 (2020-01-25) 53 | 54 | - Added scale parameter to Ministep 55 | 56 | ## v1.0.2 (2019-11-03) 57 | 58 | - Added the Ministep module (based on ideas in [#6](https://github.com/mgunyho/PdArray/issues/6)) 59 | - Fixed a bug in indexing, Array now properly works as a sequencer ([#7](https://github.com/mgunyho/PdArray/issues/7)) 60 | - Array size can now be edited also by right-clicking ([#4](https://github.com/mgunyho/PdArray/issues/4)) 61 | - Fix a typo ([#5](https://github.com/mgunyho/PdArray/issues/5)) 62 | - Update POS and I/O range tooltips 63 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 | EUPL © the European Union 2007, 2016 3 | 4 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 | below) which is provided under the terms of this Licence. Any use of the Work, 6 | other than as authorised under this Licence is prohibited (to the extent such 7 | use is covered by a right of the copyright holder of the Work). 8 | 9 | The Work is provided under the terms of this Licence when the Licensor (as 10 | defined below) has placed the following notice immediately following the 11 | copyright notice for the Work: 12 | 13 | Licensed under the EUPL 14 | 15 | or has expressed by any other means his willingness to license under the EUPL. 16 | 17 | 1. Definitions 18 | 19 | In this Licence, the following terms have the following meaning: 20 | 21 | - ‘The Licence’: this Licence. 22 | 23 | - ‘The Original Work’: the work or software distributed or communicated by the 24 | Licensor under this Licence, available as Source Code and also as Executable 25 | Code as the case may be. 26 | 27 | - ‘Derivative Works’: the works or software that could be created by the 28 | Licensee, based upon the Original Work or modifications thereof. This Licence 29 | does not define the extent of modification or dependence on the Original Work 30 | required in order to classify a work as a Derivative Work; this extent is 31 | determined by copyright law applicable in the country mentioned in Article 15. 32 | 33 | - ‘The Work’: the Original Work or its Derivative Works. 34 | 35 | - ‘The Source Code’: the human-readable form of the Work which is the most 36 | convenient for people to study and modify. 37 | 38 | - ‘The Executable Code’: any code which has generally been compiled and which is 39 | meant to be interpreted by a computer as a program. 40 | 41 | - ‘The Licensor’: the natural or legal person that distributes or communicates 42 | the Work under the Licence. 43 | 44 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 | Licence, or otherwise contributes to the creation of a Derivative Work. 46 | 47 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 | the Work under the terms of the Licence. 49 | 50 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 | renting, distributing, communicating, transmitting, or otherwise making 52 | available, online or offline, copies of the Work or providing access to its 53 | essential functionalities at the disposal of any other natural or legal 54 | person. 55 | 56 | 2. Scope of the rights granted by the Licence 57 | 58 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 | sublicensable licence to do the following, for the duration of copyright vested 60 | in the Original Work: 61 | 62 | - use the Work in any circumstance and for all usage, 63 | - reproduce the Work, 64 | - modify the Work, and make Derivative Works based upon the Work, 65 | - communicate to the public, including the right to make available or display 66 | the Work or copies thereof to the public and perform publicly, as the case may 67 | be, the Work, 68 | - distribute the Work or copies thereof, 69 | - lend and rent the Work or copies thereof, 70 | - sublicense rights in the Work or copies thereof. 71 | 72 | Those rights can be exercised on any media, supports and formats, whether now 73 | known or later invented, as far as the applicable law permits so. 74 | 75 | In the countries where moral rights apply, the Licensor waives his right to 76 | exercise his moral right to the extent allowed by law in order to make effective 77 | the licence of the economic rights here above listed. 78 | 79 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 | any patents held by the Licensor, to the extent necessary to make use of the 81 | rights granted on the Work under this Licence. 82 | 83 | 3. Communication of the Source Code 84 | 85 | The Licensor may provide the Work either in its Source Code form, or as 86 | Executable Code. If the Work is provided as Executable Code, the Licensor 87 | provides in addition a machine-readable copy of the Source Code of the Work 88 | along with each copy of the Work that the Licensor distributes or indicates, in 89 | a notice following the copyright notice attached to the Work, a repository where 90 | the Source Code is easily and freely accessible for as long as the Licensor 91 | continues to distribute or communicate the Work. 92 | 93 | 4. Limitations on copyright 94 | 95 | Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 | any exception or limitation to the exclusive rights of the rights owners in the 97 | Work, of the exhaustion of those rights or of other applicable limitations 98 | thereto. 99 | 100 | 5. Obligations of the Licensee 101 | 102 | The grant of the rights mentioned above is subject to some restrictions and 103 | obligations imposed on the Licensee. Those obligations are the following: 104 | 105 | Attribution right: The Licensee shall keep intact all copyright, patent or 106 | trademarks notices and all notices that refer to the Licence and to the 107 | disclaimer of warranties. The Licensee must include a copy of such notices and a 108 | copy of the Licence with every copy of the Work he/she distributes or 109 | communicates. The Licensee must cause any Derivative Work to carry prominent 110 | notices stating that the Work has been modified and the date of modification. 111 | 112 | Copyleft clause: If the Licensee distributes or communicates copies of the 113 | Original Works or Derivative Works, this Distribution or Communication will be 114 | done under the terms of this Licence or of a later version of this Licence 115 | unless the Original Work is expressly distributed only under this version of the 116 | Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 | (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 | the Work or Derivative Work that alter or restrict the terms of the Licence. 119 | 120 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 | Works or copies thereof based upon both the Work and another work licensed under 122 | a Compatible Licence, this Distribution or Communication can be done under the 123 | terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 | Licence’ refers to the licences listed in the appendix attached to this Licence. 125 | Should the Licensee's obligations under the Compatible Licence conflict with 126 | his/her obligations under this Licence, the obligations of the Compatible 127 | Licence shall prevail. 128 | 129 | Provision of Source Code: When distributing or communicating copies of the Work, 130 | the Licensee will provide a machine-readable copy of the Source Code or indicate 131 | a repository where this Source will be easily and freely available for as long 132 | as the Licensee continues to distribute or communicate the Work. 133 | 134 | Legal Protection: This Licence does not grant permission to use the trade names, 135 | trademarks, service marks, or names of the Licensor, except as required for 136 | reasonable and customary use in describing the origin of the Work and 137 | reproducing the content of the copyright notice. 138 | 139 | 6. Chain of Authorship 140 | 141 | The original Licensor warrants that the copyright in the Original Work granted 142 | hereunder is owned by him/her or licensed to him/her and that he/she has the 143 | power and authority to grant the Licence. 144 | 145 | Each Contributor warrants that the copyright in the modifications he/she brings 146 | to the Work are owned by him/her or licensed to him/her and that he/she has the 147 | power and authority to grant the Licence. 148 | 149 | Each time You accept the Licence, the original Licensor and subsequent 150 | Contributors grant You a licence to their contributions to the Work, under the 151 | terms of this Licence. 152 | 153 | 7. Disclaimer of Warranty 154 | 155 | The Work is a work in progress, which is continuously improved by numerous 156 | Contributors. It is not a finished work and may therefore contain defects or 157 | ‘bugs’ inherent to this type of development. 158 | 159 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 | and without warranties of any kind concerning the Work, including without 161 | limitation merchantability, fitness for a particular purpose, absence of defects 162 | or errors, accuracy, non-infringement of intellectual property rights other than 163 | copyright as stated in Article 6 of this Licence. 164 | 165 | This disclaimer of warranty is an essential part of the Licence and a condition 166 | for the grant of any rights to the Work. 167 | 168 | 8. Disclaimer of Liability 169 | 170 | Except in the cases of wilful misconduct or damages directly caused to natural 171 | persons, the Licensor will in no event be liable for any direct or indirect, 172 | material or moral, damages of any kind, arising out of the Licence or of the use 173 | of the Work, including without limitation, damages for loss of goodwill, work 174 | stoppage, computer failure or malfunction, loss of data or any commercial 175 | damage, even if the Licensor has been advised of the possibility of such damage. 176 | However, the Licensor will be liable under statutory product liability laws as 177 | far such laws apply to the Work. 178 | 179 | 9. Additional agreements 180 | 181 | While distributing the Work, You may choose to conclude an additional agreement, 182 | defining obligations or services consistent with this Licence. However, if 183 | accepting obligations, You may act only on your own behalf and on your sole 184 | responsibility, not on behalf of the original Licensor or any other Contributor, 185 | and only if You agree to indemnify, defend, and hold each Contributor harmless 186 | for any liability incurred by, or claims asserted against such Contributor by 187 | the fact You have accepted any warranty or additional liability. 188 | 189 | 10. Acceptance of the Licence 190 | 191 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 | placed under the bottom of a window displaying the text of this Licence or by 193 | affirming consent in any other similar way, in accordance with the rules of 194 | applicable law. Clicking on that icon indicates your clear and irrevocable 195 | acceptance of this Licence and all of its terms and conditions. 196 | 197 | Similarly, you irrevocably accept this Licence and all of its terms and 198 | conditions by exercising any rights granted to You by Article 2 of this Licence, 199 | such as the use of the Work, the creation by You of a Derivative Work or the 200 | Distribution or Communication by You of the Work or copies thereof. 201 | 202 | 11. Information to the public 203 | 204 | In case of any Distribution or Communication of the Work by means of electronic 205 | communication by You (for example, by offering to download the Work from a 206 | remote location) the distribution channel or media (for example, a website) must 207 | at least provide to the public the information requested by the applicable law 208 | regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 | stored and reproduced by the Licensee. 210 | 211 | 12. Termination of the Licence 212 | 213 | The Licence and the rights granted hereunder will terminate automatically upon 214 | any breach by the Licensee of the terms of the Licence. 215 | 216 | Such a termination will not terminate the licences of any person who has 217 | received the Work from the Licensee under the Licence, provided such persons 218 | remain in full compliance with the Licence. 219 | 220 | 13. Miscellaneous 221 | 222 | Without prejudice of Article 9 above, the Licence represents the complete 223 | agreement between the Parties as to the Work. 224 | 225 | If any provision of the Licence is invalid or unenforceable under applicable 226 | law, this will not affect the validity or enforceability of the Licence as a 227 | whole. Such provision will be construed or reformed so as necessary to make it 228 | valid and enforceable. 229 | 230 | The European Commission may publish other linguistic versions or new versions of 231 | this Licence or updated versions of the Appendix, so far this is required and 232 | reasonable, without reducing the scope of the rights granted by the Licence. New 233 | versions of the Licence will be published with a unique version number. 234 | 235 | All linguistic versions of this Licence, approved by the European Commission, 236 | have identical value. Parties can take advantage of the linguistic version of 237 | their choice. 238 | 239 | 14. Jurisdiction 240 | 241 | Without prejudice to specific agreement between parties, 242 | 243 | - any litigation resulting from the interpretation of this License, arising 244 | between the European Union institutions, bodies, offices or agencies, as a 245 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 | of Justice of the European Union, as laid down in article 272 of the Treaty on 247 | the Functioning of the European Union, 248 | 249 | - any litigation arising between other parties and resulting from the 250 | interpretation of this License, will be subject to the exclusive jurisdiction 251 | of the competent court where the Licensor resides or conducts its primary 252 | business. 253 | 254 | 15. Applicable Law 255 | 256 | Without prejudice to specific agreement between parties, 257 | 258 | - this Licence shall be governed by the law of the European Union Member State 259 | where the Licensor has his seat, resides or has his registered office, 260 | 261 | - this licence shall be governed by Belgian law if the Licensor has no seat, 262 | residence or registered office inside a European Union Member State. 263 | 264 | Appendix 265 | 266 | ‘Compatible Licences’ according to Article 5 EUPL are: 267 | 268 | - GNU General Public License (GPL) v. 2, v. 3 269 | - GNU Affero General Public License (AGPL) v. 3 270 | - Open Software License (OSL) v. 2.1, v. 3.0 271 | - Eclipse Public License (EPL) v. 1.0 272 | - CeCILL v. 2.0, v. 2.1 273 | - Mozilla Public Licence (MPL) v. 2 274 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 | works other than software 277 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 | Reciprocity (LiLiQ-R+). 280 | 281 | The European Commission may update this Appendix to later versions of the above 282 | licences without producing a new version of the EUPL, as long as they provide 283 | the rights granted in Article 2 of this Licence and protect the covered Source 284 | Code from exclusive appropriation. 285 | 286 | All other changes or additions to this Appendix require the production of a new 287 | EUPL version. 288 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # If RACK_DIR is not defined when calling the Makefile, default to two directories above 2 | RACK_DIR ?= ../.. 3 | 4 | # FLAGS will be passed to both the C and C++ compiler 5 | FLAGS += 6 | CFLAGS += 7 | CXXFLAGS += 8 | 9 | # Careful about linking to shared libraries, since you can't assume much about the user's environment and library search path. 10 | # Static libraries are fine. 11 | LDFLAGS += 12 | 13 | # Add .cpp and .c files to the build 14 | SOURCES += $(wildcard src/*.cpp) 15 | 16 | # Add files to the ZIP package when running `make dist` 17 | # The compiled plugin is automatically added. 18 | DISTRIBUTABLES += $(wildcard LICENSE*) res 19 | 20 | # Include the VCV Rack plugin Makefile framework 21 | include $(RACK_DIR)/plugin.mk 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PdArray 2 | 3 | PdArray is a plugin for the [VCV Rack](https://vcvrack.com/) virtual modular 4 | synthesizer that allows creating custom envelopes, much like the `array` object 5 | in [Pure Data](https://puredata.info/). You can enter values into the array 6 | either by manually drawing, recording an input, or loading a sample. A CV input 7 | controls the position of a cursor for reading out the array values. There are 8 | two outputs: one will output the stepped values of the array directly (useful 9 | for e.g. sequencing), and the other will output smooth interpolated values, 10 | using the same interpolation algorithm as the `tabread4~` object in Pd. 11 | 12 | ![main screenshot](screenshots/main.png) 13 | 14 | PdArray can be used as an envelope generator, sequencer, sample player, 15 | waveshaper or in a number of other weird and interesting ways. 16 | 17 | The plugin also contains two small companion modules called Miniramp and 18 | Ministep. Miniramp can be used to to easily record or read out the array 19 | sequentially, while Ministep helps with stepping through the array using a 20 | clock, like a traditional sequencer. 21 | 22 | 23 | ## Array 24 | Array is the main module of the PdArray plugin. This is its interface: 25 | 26 | ![interface](screenshots/interface.png) 27 | 28 | On the top row, you will find one input and two outputs. The POS input takes a 29 | CV (or audio) signal which determines which value from the array to read, or 30 | play back. The value at this position will then be output to OUT STEP and OUT 31 | SMTH. The OUT STEP output contains the array values directly, while OUT SMTH 32 | will output smoothed (interpolated) values. 33 | 34 | The POS input supports polyphony. When a polyphonic cable is inserted into POS, 35 | each voltage in the input will have its own cursor, and the OUT STEP and OUT 36 | SMTH outputs will output polyphonic signals at each of the cursors. 37 | 38 | The POS RANGE switch can be used to select the expected range of the POS input; 39 | use +10V for e.g. unipolar LFO's and Miniramp, +-5V for bipolar LFO's and audio 40 | signals. Similarly, the I/O RANGE switch selects the range of values that you 41 | wish to record and what the outputs will have. Setting the switch to +10V means 42 | that the top edge of the visual representation of the array corresponds to +10V 43 | and the bottom edge to 0V, while for +-5 and +-10 the top and bottom edges of 44 | the array are plus and minus 5 or 10 volts, respectively, and the middle 45 | corresponds to 0V. 46 | 47 | The SIZE screen displays the current number of elements in the array. You can 48 | click on the screen to type a value up to 999999. If the array has more 49 | elements than there are pixels on the display, drawing will affect multiple 50 | array values at once. 51 | 52 | By default, the array values are saved along with the patch file. If the array 53 | has less than 5000 elements, the array data is serialized directly as a JSON 54 | array. Otherwise, the array is saved as a wav file in the patch storage folder. 55 | To reduce disk space, it is also possible to save only the path to a loaded wav 56 | file, or to not save the array data at all, in which case only the array SIZE 57 | is saved in the patch. This behavior can be changed from the "Data persistence" 58 | right-click menu. 59 | 60 | The bottom row contains CV inputs related to recording. The REC POS dictates 61 | the position in the array where a recorded value will be written. Its expected 62 | values are set using POS RANGE, just as for the playback POS. Input the signal 63 | or values you want to record to the REC IN input, and click and hold the REC 64 | LED or send a gate voltage to the REC input to record. Only monophonic 65 | recording is supported. 66 | 67 | Right-clicking on the module will open up some additional options, like 68 | initializing or sorting the array, loading an audio file and setting the 69 | interpolation mode. 70 | 71 | If the Array module is bypassed, the signal that is being sent to REC IN is 72 | directly output to both OUT STEP and OUT SMTH. 73 | 74 | ### Drawing sequences or envelopes 75 | 76 | ![sequence and envelope](screenshots/sequencer-envelope.png) 77 | 78 | The most basic usage of PdArray is to just draw a sequence of values or an 79 | envelope with your mouse. Increase the SIZE to get a smoother array, although 80 | you can get reasonably smooth envelopes even with a small SIZE if you use the 81 | OUT SMTH output. 82 | 83 | ### Recording 84 | 85 | ![recording](screenshots/record.png) 86 | 87 | Input a ramp or LFO signal to REC POS, send audio or CV (or anything really) to 88 | REC IN, send a high gate to the REC input or click on the LED, and watch how 89 | the input signal gets imprinted to the array in real time. 90 | 91 | By right-clicking on the module, you can switch between gate and toggle modes 92 | for recording. In gate mode, the input is recorded while the REC input is high 93 | (or you hold down the LED), while in toggle mode, recording is toggled on or 94 | off by a trigger to the REC input or when clicking the LED, as the name 95 | suggests. 96 | 97 | ### Using PdArray as a waveshaper 98 | 99 | ![waveshaper](screenshots/waveshaper.png) 100 | 101 | PdArray can be easily used as a waveshaper by either drawing or recording a 102 | waveshaper curve, and then sending an audio signal to the POS CV input, with a 103 | POS RANGE of +-5V for typical audio signals. You can even re-record the curve 104 | while the audio is playing for interesting effects! 105 | 106 | ### Loading and playing samples 107 | 108 | ![playing samples](screenshots/sample-player.png) 109 | 110 | By right-clicking on the Array module, you can load a wav sample file. You can 111 | choose to keep the current SIZE of the array when loading the file, or you can 112 | automatically resize the array to the number of samples in the wav file, up to 113 | the maximum of 999999 samples. In the non-resizing version, the number in the 114 | right-click menu shows the duration of the loaded sample at the current sample 115 | rate. After selecting a file, the array will contain the first N samples of the 116 | audio file, where N is the array size shown in the SIZE display. If N is larger 117 | than the number of samples in the wav file, the rest of the array will be left 118 | unchanged. 119 | 120 | The audio file will always be loaded from the beginning, so you will need to 121 | trim your file with an external audio editing application if you wish to load 122 | it starting from another position. 123 | 124 | Playing back a sample works the same way as reading the array in general: input 125 | a voltage to POS and connect the outputs to wherever. 126 | 127 | After loading a sample, drawing will be automatically locked to prevent 128 | accidentally modifying the sample, but it can be unlocked from the right-click 129 | menu. 130 | 131 | If you want to automatically load the audio file the next time you open the 132 | patch, you can set "Data persistence" to "Save path to loaded sample". 133 | 134 | 135 | #### Preventing clicks in sample playback 136 | 137 | The Array module gives you a lot of freedom in the way you can play back 138 | samples, but this comes with some downsides. If the sample you are using 139 | doesn't start or end at a [zero crossing](https://en.wikipedia.org/wiki/Zero_crossing), 140 | you may hear a click during playback. 141 | 142 | There are a couple of ways to deal with this. A simple trick is to click the 143 | Array REC button once with the REC POS and REC IN ports disconnected. This way, 144 | the first sample in the array will be set to zero. A slightly better method is 145 | to use the "Add fade in/out to prevent clicks" option in the right-click menu 146 | of Array, which creates short fades at the array start and end (the length is 147 | indicated in the right-click menu as a number of samples, or data points, in 148 | the array). This should work well for most samples, but note that if you're not 149 | using the "save full array data to patch" setting, this will not be saved the 150 | next time you open the patch. Lastly, if you're playing back the samples using 151 | Miniramp (see below), you can also try changing the "ramp value when finished" 152 | setting from the right-click menu. 153 | 154 | 155 | ## Miniramp 156 | 157 | ![miniramp](screenshots/miniramp.png) 158 | 159 | Miniramp is a small envelope generator that outputs a linear ramp from 0 to 10V 160 | in a given time. The duration of the ramp is shown on a display at the bottom. 161 | The large knob controls the base duration, either with linear or logarithmic 162 | scaling as selected by the LIN/LOG switch. You can also control the ramp 163 | duration / speed with the CV input and CV amount knob. 164 | 165 | When you send a trigger to TRG IN, the RAMP output will output the ramp and the 166 | GATE output will output 10V while the ramp is in progress (useful for e.g. the 167 | REC input of Array). The EOC output outputs a pulse when the ramp ends, and the 168 | FINISH output indicates whether the ramp is finished: it outputs 0V when GATE 169 | outputs 10V and 10V when GATE outputs 0V. You can interrupt (i.e. reset) the 170 | ramp by sending a pulse to the STOP input. 171 | 172 | If the CV modulation or TRIG IN input of Miniramp is polyphonic, all of the 173 | outputs will be polyphonic, with individual ramp, gate, EOC, and finish 174 | indicators for each channel. If any of the CV, TRG IN or STOP inputs are 175 | monophonic, they affect all channels; if they are polyphonic, they affect each 176 | channel individually. For example, if you have a polyphonic signal going to the 177 | CV input (with the CV AMT knob set to a nonzero value), and you send a 178 | monophonic trigger to TRIG IN, you can simultaneously trigger multiple ramps 179 | with different lengths. 180 | 181 | You can right-click on Miniramp to change its behavior. By default, the ramp 182 | output value is 0V when the ramp output is finished, but with the "ramp value 183 | when finished" option set to 10V, the output value will remain at 10V. This can 184 | prevent clicks if you're using Miniramp to play back samples from an Array. If 185 | the "Send EOC on STOP" option is enabled, Miniramp will output a trigger from 186 | the EOC port even if the ramp is interrupted by a trigger input to the STOP 187 | port. If the "Update duration only on trigger" option is enabled, the duration 188 | knob and CV modulation will only affect the ramp duration when Miniramp 189 | receives a trigger from the TRG IN input. By default, the ramp duration is 190 | updated continuously. Enabling this option is equivalent to having a sample and 191 | hold on the ramp duration. With this option enabled, the display shows the ramp 192 | duration that was in effect when the last trigger was received. 193 | 194 | ### Chaining Arrays using Miniramp 195 | 196 | Using the EOC output, it is possible to chain several Arrays one after another, 197 | for example to create an envelope with several stages, or to play samples that 198 | exceed the maximum size of Array. 199 | 200 | ![envelope-chaining](screenshots/envelope-chaining.png) 201 | 202 | Note how in the above screenshot, the segments have different lengths because 203 | of the different duration settings of the ramps. The CV on the VCA mixer is 204 | modulated by the GATE outputs to mute the arrays while they are not playing. To 205 | avoid clicks between the envelope segments, make sure that you join up the 206 | start and end values of the segments, and set the "interpolation at boundary" 207 | mode of the Arrays to "mirror" or "constant". Despite this, you may get some 208 | single-sample spikes (zero values) at the segment boundaries, as you can see in 209 | the screenshot above. These can be removed with e.g. the Process module from 210 | VCV Fundamental with a 1-ms slew. 211 | 212 | 213 | ## Ministep 214 | 215 | ![ministep](screenshots/ministep.png) 216 | 217 | Ministep is a counter that can be used to step through an Array, like a 218 | sequencer. The current value of the counter is shown on the middle display. 219 | Sending a trigger to INC increments the counter, while a trigger to DEC 220 | decrements it. The RST input resets the counter to 1. You can click on the MAX 221 | display to change the maximum number of steps, up to 999. 222 | 223 | The SCL CV input controls how much the counter increments/decrements on each 224 | step. The step size is shown on the top display. When no cable is connected, 225 | the step size is 1, but note that connecting a 0V signal sets the step size to 226 | zero (you can also input negative values!). By default, setting SCL to 10 volts 227 | makes the step size equal to MAX, but the behavior can be changed to make one 228 | volt correspond to one step from the "scale mode" right-click menu. The step 229 | size is always rounded towards zero. 230 | 231 | By default, the output will be 10V divided into N steps, where N is the maximum 232 | number. This way, it is easy to use with an Array whose SIZE is set equal to N. 233 | However, from the "output mode" right-click menu, you can also configure 234 | Ministep to output the step number directly as a voltage. In this mode, step 1 235 | outputs 0V, step 2 outputs 1V and so on. 236 | 237 | If any of the INC, DEC or RST inputs are polyphonic, there will be one counter 238 | per channel. Sending a monophonic trigger to the reset input resets all 239 | channels, while a polyphonic trigger can be used to reset individual channels. 240 | Similarly, a monophonic CV connected to SCL scales the step size of all 241 | channels, while a polyphonic signal affects each channel separately. 242 | 243 | By detecting the falling edge of the output or applying a threshold to the 244 | output value near zero, you can use Ministep as a clock divider! Fiddling with 245 | SCL can also lead to interesting results. A step size that has few divisors can 246 | be used to generate somewhat random voltages, or triggering the increment at 247 | audio rate and adjusting the step size can create interesting harmonics. 248 | 249 | 250 | ## Contributing 251 | If you have suggestions or feedback or find a bug or whatever, feel free to open 252 | an issue or a pull request in this repository! 253 | 254 | Building the modules follows the [standard procedure](https://vcvrack.com/manual/PluginDevelopmentTutorial.html): 255 | ``` 256 | export RACK_DIR=/path/to/Rack_SDK/ 257 | make install 258 | ``` 259 | 260 | 261 | ## Licenses 262 | The source code and panel artwork are copyright 2023 Márton Gunyhó. Licensed 263 | under the EUPL (see [LICENSE](LICENSE.txt)). This repo also contains the fonts 264 | used for creating the panels: Overpass is licensed under the Open Font License 265 | (see [here](res/fonts/OFL.txt)) and Roboto is licensed under the Apache 2 266 | License (see [here](res/fonts/APACHE2.txt)). 267 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "PdArray", 3 | "name": "PdArray", 4 | "version": "2.1.1", 5 | "license": "EUPL-1.2", 6 | "brand": "", 7 | "author": "Márton Gunyhó", 8 | "authorEmail": "", 9 | "authorUrl": "https://github.com/mgunyho", 10 | "pluginUrl": "https://github.com/mgunyho/PdArray", 11 | "manualUrl": "https://github.com/mgunyho/PdArray", 12 | "sourceUrl": "https://github.com/mgunyho/PdArray", 13 | "changelogUrl": "https://github.com/mgunyho/PdArray/blob/master/CHANGELOG.md", 14 | "donateUrl": "", 15 | "modules": [ 16 | { 17 | "slug": "Array", 18 | "name": "Array", 19 | "description": "Draw or record sequences, envelopes or audio", 20 | "tags": [ 21 | "envelope generator", 22 | "function generator", 23 | "sampler", 24 | "visual", 25 | "waveshaper", 26 | "polyphonic" 27 | ] 28 | }, 29 | { 30 | "slug": "Miniramp", 31 | "name": "Miniramp", 32 | "description": "Generate a ramp from 0V to 10V with a given duration", 33 | "tags": [ 34 | "envelope generator", 35 | "utility", 36 | "polyphonic" 37 | ] 38 | }, 39 | { 40 | "slug": "Ministep", 41 | "name": "Ministep", 42 | "description": "Use triggers to step voltages from 0V to 10V in a given number of steps", 43 | "tags": [ 44 | "utility", 45 | "polyphonic" 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /res/fonts/-CooperHewitt-FontLog.txt: -------------------------------------------------------------------------------- 1 | Font Log 2 | Cooper Hewitt Typeface 3 | NOTE: ttf files were downloaded from https://github.com/tom10271/cooper-hewitt-fixed-for-windows/tree/master 4 | / / / / / / / / / / 5 | 6 | This file provides information on the Cooper Hewitt typeface. This information should be distributed along with the Cooper Hewitt fonts and any derivative works. 7 | 8 | 9 | Basic Font Information 10 | / / / / / / / / / / 11 | 12 | Cooper Hewitt is a Unicode sans serif typeface family, created for the Cooper Hewitt Smithsonian Design Museum - cooperhewitt.org - in 2014 as part of the museum's new visual identity. 13 | 14 | The typeface was designed by Chester Jenkins at Village Type and Design LLC, working with Eddie Opara and his group at Pentagram, and Cooper Hewitt Smithsonian Design Museum director Caroline Baumann and her team. 15 | 16 | The typeface at June 2014 has 7 weights in roman and italic, in the Latin script only. 17 | 18 | 19 | Supported Codepages and Languages 20 | / / / / / / / / / / 21 | 22 | ISO 8859-1 / LATIN1 23 | Afrikaans, Albanian, Basque, Breton, Catalan, Catalan, Corsican, Czech, Danish, Dutch, English (UK and US), Estonian, Faroese, Finnish, French, Galician, German, Hungarian, Icelandic, Indonesian, Irish, Irish (new orthography), Italian, Latin (basic classical orthography), Leonese, Luxembourgish (basic classical orthography), Malay, Manx, Māori, Norwegian (Bokmål and Nynorsk), Occitan, Portuguese, Rhaeto-Romanic, Scottish Gaelic, Spanish, Swahili, Swedish, Turkish, Walloon, Welsh 24 | 25 | ISO 8859-2 / LATIN2 26 | Bosnian, Croatian, Czech, German, Hungarian, Polish, Romanian, Serbian (when in the Latin script), Slovak, Slovene, Upper Sorbian, and Lower Sorbian 27 | 28 | ISO 8859-3 / LATIN3 29 | South European: Esperanto, Maltese, Turkish 30 | 31 | ISO 8859-4 / LATIN4 32 | North European: Estonian, Greenlandic, Latvian, Lithuanian, Sami 33 | 34 | ISO 8859-9 / LATIN5 35 | Turkish 36 | 37 | ISO 8859-10 / LATIN6 38 | Nordic 39 | 40 | ISO 8859-13 / LATIN7 41 | Baltic Rim 42 | 43 | ISO 8859-14 / LATIN8 44 | Celtic: Breton, Cornish, Irish, Manx, Scottish Gaelic, Welsh 45 | 46 | ISO 8859-15 / LATIN9 47 | Afrikaans, Albanian, Breton, Catalan, Danish, Dutch, English (US and modern British), Estonian, Faroese, Finnish, French, Galician, German, Icelandic, Irish (New orthography), Italian, Kurdish (Unified Alphabet), Latin (basic classical orthography), Luxembourgish (basic classical orthography), Malay (Rumi script), Norwegian (Bokmål and Nynorsk), Occitan, Portuguese (European and Brazilian), Rhaeto-Romanic, Scottish Gaelic, Scots, Spanish, Swahili, Swedish, Tagalog, Walloon 48 | 49 | ISO 8859-16 / LATIN10 50 | Albanian, Croatian, French, German, Hungarian, Irish Gaelic (new orthography), Italian, Polish, Romanian, Serbian, Slovenian 51 | 52 | 53 | Change Log 54 | / / / / / / / / / / 55 | (This should list both major and minor changes, most recent first.) 56 | 57 | 2014/06/16 58 | Initial release. 59 | 60 | 61 | Information for Developers/Contributors 62 | / / / / / / / / / / 63 | 64 | The release of Cooper Hewitt v1.000 (and any subsequent versions) under the OFL license provides a means for people to modify the fonts to meet their needs and contribute to the project. For information on what you're allowed to 65 | change or modify, consult the OFL and OFL-FAQ. 66 | 67 | Anyone can make their own modified version of Cooper Hewitt (using a different name), but Cooper Hewitt Smithsonian Design Museum and Village Type and Design LLC will continue to maintain and develop the canonical version of the Cooper Hewitt typeface. As the package maintainer, we welcome contributions. Here are some things to keep in mind: 68 | 69 | Format: We are happy to accept contributions, but to maximise the chances of us including your work, please make it available to us (via email or a URL) as a UFO file. 70 | 71 | Source files: The primary source files for the fonts are the UFO files. 72 | 73 | Copyright attribution: If you submit something for inclusion in the main Cooper Hewitt fonts, we will ask you to affirm that it is your original work, and ask you to assign the copyright of your work to Cooper Hewitt Smithsonian Design Museum. This is to ensure that future releases can be made under improved versions of the OFL without requiring further permission. This follows 74 | the same principle used by the FSF. The Cooper Hewitt typeface is property of a not-for-profit organization committed to the dissemination of knowledge and information, and that any contributions incorporated in the fonts will always be available under the OFL or a similar license. 75 | 76 | Quality: Because we want to be able to guarantee a high level of quality for the primary Cooper Hewitt typeface, we will review submissions carefully. Please don't be discouraged if we do not include a submission for this reason, or ask you to make specific revisions, or make revisions we deem necessary or desirable. 77 | 78 | Types of contributions: If you wish to make a contribution - a set of additional glyphs, scripts, code, etc. - please contact us before you do any work to see if it is a contribution we currently need. Every addition adds to the complexity of the project and needs to be carefully planned. This also avoids two people working on the same type of addition at the same time. 79 | 80 | When submissions will be included: We will revise the fonts when major updates are needed - new versions of Unicode, for example - and/or when we have material for a useful addition to the typeface. If you wish to make submissions, please contact us. 81 | 82 | 83 | Acknowledgements 84 | / / / / / / / / / / 85 | 86 | (Here is where contributors can be acknowledged. If you make modifications be sure to add your name (N), email (E), web-address (W) and description (D). This list is sorted by last name in alphabetical order.) 87 | 88 | N: Chester Jenkins 89 | E: chester@vllg.com 90 | W: http://vllg.com/ 91 | D: Original Designer 92 | 93 | N: Tal Leming 94 | E: tal@typesupply.com 95 | W: http://typesupply.com/ 96 | D: UFO file preparation 97 | 98 | The Cooper Hewitt typeface is maintained by Village Type and Design LLC: CooperHewitt@vllg.com 99 | 100 | (We acknowledge SIL, the company behind the Open Font License (OFL); this Font Log is based on the Fontlog created for the Gentium typeface project.) 101 | 102 | -------------------------------------------------------------------------------- /res/fonts/-OFL-FAQ.txt: -------------------------------------------------------------------------------- 1 | OFL FAQ - Frequently Asked Questions about the SIL Open Font License (OFL) 2 | Version 1.1-update3 - Sept 2013 3 | (See http://scripts.sil.org/OFL for updates) 4 | 5 | 6 | CONTENTS OF THIS FAQ 7 | 1 USING AND DISTRIBUTING FONTS LICENSED UNDER THE OFL 8 | 2 USING OFL FONTS FOR WEB PAGES AND ONLINE WEB FONT SERVICES 9 | 3 MODIFYING OFL-LICENSED FONTS 10 | 4 LICENSING YOUR ORIGINAL FONTS UNDER THE OFL 11 | 5 CHOOSING RESERVED FONT NAMES 12 | 6 ABOUT THE FONTLOG 13 | 7 MAKING CONTRIBUTIONS TO OFL PROJECTS 14 | 8 ABOUT THE LICENSE ITSELF 15 | 9 ABOUT SIL INTERNATIONAL 16 | APPENDIX A - FONTLOG EXAMPLE 17 | 18 | 1 USING AND DISTRIBUTING FONTS LICENSED UNDER THE OFL 19 | 20 | 1.1 Can I use the fonts for a book or other print publication, to create logos or other graphics or even to manufacture objects based on their outlines? 21 | Yes. You are very welcome to do so. Authors of fonts released under the OFL allow you to use their font software as such for any kind of design work. No additional license or permission is required, unlike with some other licenses. Some examples of these uses are: logos, posters, business cards, stationery, video titling, signage, t-shirts, personalised fabric, 3D-printed/laser-cut shapes, sculptures, rubber stamps, cookie cutters and lead type. 22 | 23 | 1.1.1 Does that restrict the license or distribution of that artwork? 24 | No. You remain the author and copyright holder of that newly derived graphic or object. You are simply using an open font in the design process. It is only when you redistribute, bundle or modify the font itself that other conditions of the license have to be respected (see below for more details). 25 | 26 | 1.1.2 Is any kind of acknowledgement required? 27 | No. Font authors may appreciate being mentioned in your artwork's acknowledgements alongside the name of the font, possibly with a link to their website, but that is not required. 28 | 29 | 1.2 Can the fonts be included with Free/Libre and Open Source Software collections such as GNU/Linux and BSD distributions and repositories? 30 | Yes! Fonts licensed under the OFL can be freely included alongside other software under FLOSS (Free/Libre and Open Source Software) licenses. Since fonts are typically aggregated with, not merged into, existing software, there is little need to be concerned about incompatibility with existing software licenses. You may also repackage the fonts and the accompanying components in a .rpm or .deb package (or other similar package formats or installers) and include them in distribution CD/DVDs and online repositories. (Also see section 5.9 about rebuilding from source.) 31 | 32 | 1.3 I want to distribute the fonts with my program. Does this mean my program also has to be Free/Libre and Open Source Software? 33 | No. Only the portions based on the Font Software are required to be released under the OFL. The intent of the license is to allow aggregation or bundling with software under restricted licensing as well. 34 | 35 | 1.4 Can I sell a software package that includes these fonts? 36 | Yes, you can do this with both the Original Version and a Modified Version of the fonts. Examples of bundling made possible by the OFL would include: word processors, design and publishing applications, training and educational software, games and entertainment software, mobile device applications, etc. 37 | 38 | 1.5 Can I include the fonts on a CD of freeware or commercial fonts? 39 | Yes, as long some other font or software is also on the disk, so the OFL font is not sold by itself. 40 | 41 | 1.6 Why won't the OFL let me sell the fonts alone? 42 | The intent is to keep people from making money by simply redistributing the fonts. The only people who ought to profit directly from the fonts should be the original authors, and those authors have kindly given up potential direct income to distribute their fonts under the OFL. Please honour and respect their contribution! 43 | 44 | 1.7 What about sharing OFL fonts with friends on a CD, DVD or USB stick? 45 | You are very welcome to share open fonts with friends, family and colleagues through removable media. Just remember to include the full font package, including any copyright notices and licensing information as available in OFL.txt. In the case where you sell the font, it has to come bundled with software. 46 | 47 | 1.8 Can I host the fonts on a web site for others to use? 48 | Yes, as long as you make the full font package available. In most cases it may be best to point users to the main site that distributes the Original Version so they always get the most recent stable and complete version. See also discussion of web fonts in Section 2. 49 | 50 | 1.9 Can I host the fonts on a server for use over our internal network? 51 | Yes. If the fonts are transferred from the server to the client computer by means that allow them to be used even if the computer is no longer attached to the network, the full package (copyright notices, licensing information, etc.) should be included. 52 | 53 | 1.10 Does the full OFL license text always need to accompany the font? 54 | The only situation in which an OFL font can be distributed without the text of the OFL (either in a separate file or in font metadata), is when a font is embedded in a document or bundled within a program. In the case of metadata included within a font, it is legally sufficient to include only a link to the text of the OFL on http://scripts.sil.org/OFL, but we strongly recommend against this. Most modern font formats include metadata fields that will accept the full OFL text, and full inclusion increases the likelihood that users will understand and properly apply the license. 55 | 56 | 1.11 What do you mean by 'embedding'? How does that differ from other means of distribution? 57 | By 'embedding' we mean inclusion of the font in a document or file in a way that makes extraction (and redistribution) difficult or clearly discouraged. In many cases the names of embedded fonts might also not be obvious to those reading the document, the font data format might be altered, and only a subset of the font - only the glyphs required for the text - might be included. Any other means of delivering a font to another person is considered 'distribution', and needs to be accompanied by any copyright notices and licensing information available in OFL.txt. 58 | 59 | 1.12 So can I embed OFL fonts in my document? 60 | Yes, either in full or a subset. The restrictions regarding font modification and redistribution do not apply, as the font is not intended for use outside the document. 61 | 62 | 1.13 Does embedding alter the license of the document itself? 63 | No. Referencing or embedding an OFL font in any document does not change the license of the document itself. The requirement for fonts to remain under the OFL does not apply to any document created using the fonts and their derivatives. Similarly, creating any kind of graphic using a font under OFL does not make the resulting artwork subject to the OFL. 64 | 65 | 1.14 If OFL fonts are extracted from a document in which they are embedded (such as a PDF file), what can be done with them? Is this a risk to author(s)? 66 | The few utilities that can extract fonts embedded in a PDF will typically output limited amounts of outlines - not a complete font. To create a working font from this method is much more difficult and time consuming than finding the source of the original OFL font. So there is little chance that an OFL font would be extracted and redistributed inappropriately through this method. Even so, copyright laws address any misrepresentation of authorship. All Font Software released under the OFL and marked as such by the author(s) is intended to remain under this license regardless of the distribution method, and cannot be redistributed under any other license. We strongly discourage any font extraction - we recommend directly using the font sources instead - but if you extract font outlines from a document, please be considerate: respect the work of the author(s) and the licensing model. 67 | 68 | 1.15 What about distributing fonts with a document? Within a compressed folder structure? Is it distribution, bundling or embedding? 69 | Certain document formats may allow the inclusion of an unmodified font within their file structure which may consist of a compressed folder containing the various resources forming the document (such as pictures and thumbnails). Including fonts within such a structure is understood as being different from embedding but rather similar to bundling (or mere aggregation) which the license explicitly allows. In this case the font is conveyed unchanged whereas embedding a font usually transforms it from the original format. The OFL does not allow anyone to extract the font from such a structure to then redistribute it under another license. The explicit permission to redistribute and embed does not cancel the requirement for the Font Software to remain under the license chosen by its author(s). Even if the font travels inside the document as one of its assets, it should not lose its authorship information and licensing. 70 | 71 | 1.16 What about ebooks shipping with open fonts? 72 | The requirements differ depending on whether the fonts are linked, embedded or distributed (bundled or aggregated). Some ebook formats use web technologies to do font linking via @font-face, others are designed for font embedding, some use fonts distributed with the document or reading software, and a few rely solely on the fonts already present on the target system. The license requirements depend on the type of inclusion as discussed in 1.15. 73 | 74 | 1.17 Can Font Software released under the OFL be subject to URL-based access restrictions methods or DRM (Digital Rights Management) mechanisms? 75 | Yes, but these issues are out-of-scope for the OFL. The license itself neither encourages their use nor prohibits them since such mechanisms are not implemented in the components of the Font Software but through external software. Such restrictions are put in place for many different purposes corresponding to various usage scenarios. One common example is to limit potentially dangerous cross-site scripting attacks. However, in the spirit of libre/open fonts and unrestricted writing systems, we strongly encourage open sharing and reuse of OFL fonts, and the establishment of an environment where such restrictions are unnecessary. Note that whether you wish to use such mechanisms or you prefer not to, you must still abide by the rules set forth by the OFL when using fonts released by their authors under this license. Derivative fonts must be licensed under the OFL, even if they are part of a service for which you charge fees and/or for which access to source code is restricted. You may not sell the fonts on their own - they must be part of a larger software package, bundle or subscription plan. For example, even if the OFL font is distributed in a software package or via an online service using a DRM mechanism, the user would still have the right to extract that font, use, study, modify and redistribute it under the OFL. 76 | 77 | 1.18 I've come across a font released under the OFL. How can I easily get more information about the Original Version? How can I know where it stands compared to the Original Version or other Modified Versions? 78 | Consult the copyright statement(s) in the license for ways to contact the original authors. Consult the FONTLOG (see section 6 for more details and examples) for information on how the font differs from the Original Version, and get in touch with the various contributors via the information in the acknowledgement section. Please consider using the Original Versions of the fonts whenever possible. 79 | 80 | 1.19 What do you mean in condition 4 of the OFL's permissions and conditions? Can you provide examples of abusive promotion / endorsement / advertisement vs. normal acknowledgement? 81 | The intent is that the goodwill and reputation of the author(s) should not be used in a way that makes it sound like the original author(s) endorse or approve of a specific Modified Version or software bundle. For example, it would not be right to advertise a word processor by naming the author(s) in a listing of software features, or to promote a Modified Version on a web site by saying "designed by ...". However, it would be appropriate to acknowledge the author(s) if your software package has a list of people who deserve thanks. We realize that this can seem to be a grey area, but the standard used to judge an acknowledgement is that if the acknowledgement benefits the author(s) it is allowed, but if it primarily benefits other parties, or could reflect poorly on the author(s), then it is not. 82 | 83 | 1.20 I'm writing a small app for mobile platforms, do I need to include the whole package? 84 | If you bundle a font under the OFL with your mobile app you must comply with the terms of the license. At a minimum you must include the copyright statement, the license notice and the license text. A mention of this information in your About box or Changelog, with a link to where the font package is from, is good practice, and the extra space needed to carry these items is very small. You do not, however, need to include the full contents of the font package - only the fonts you use and the copyright and license that apply to them. For example, if you only use the regular weight in your app, you do not need to include the italic and bold versions. 85 | 86 | 1.21 What about including OFL fonts by default in my firmware or dedicated operating system? 87 | Many such systems are restricted and turned into appliances so that users cannot study or modify them. Using open fonts to increase quality and language coverage is a great idea, but you need to be aware that if there is a way for users to extract fonts you cannot legally prevent them from doing that. The fonts themselves, including any changes you make to them, must be distributed under the OFL even if your firmware has a more restrictive license. If you do transform the fonts and change their formats when you include them in your firmware you must respect any names reserved by the font authors via the RFN mechanism and pick your own font name. Alternatively if you directly add a font under the OFL to the font folder of your firmware without modifying or optimizing it you are simply bundling the font like with any other software collection, and do not need to make any further changes. 88 | 89 | 1.22 Can I make and publish CMS themes or templates that use OFL fonts? Can I include the fonts themselves in the themes or templates? Can I sell the whole package? 90 | Yes, you are very welcome to integrate open fonts into themes and templates for your preferred CMS and make them more widely available. Remember that you can only sell the fonts and your CMS add-on as part of a software bundle. (See 1.4 for details and examples about selling bundles). 91 | 92 | 1.23 Can OFL fonts be included in services that deliver fonts to the desktop from remote repositories? Even if they contain both OFL and non-OFL fonts? 93 | Yes. Some foundries have set up services to deliver fonts to subscribers directly to desktops from their online repositories; similarly, plugins are available to preview and use fonts directly in your design tool or publishing suite. These services may mix open and restricted fonts in the same channel, however they should make a clear distinction between them to users. These services should also not hinder users (such as through DRM or obfuscation mechanisms) from extracting and using the OFL fonts in other environments, or continuing to use OFL fonts after subscription terms have ended, as those uses are specifically allowed by the OFL. 94 | 95 | 1.24 Can services that provide or distribute OFL fonts restrict my use of them? 96 | No. The terms of use of such services cannot replace or restrict the terms of the OFL, as that would be the same as distributing the fonts under a different license, which is not allowed. You are still entitled to use, modify and redistribute them as the original authors have intended outside of the sole control of that particular distribution channel. Note, however, that the fonts provided by these services may differ from the Original Versions. 97 | 98 | 99 | 2 USING OFL FONTS FOR WEBPAGES AND ONLINE WEB FONT SERVICES 100 | 101 | NOTE: This section often refers to a separate paper on 'Web Fonts & RFNs'. This is available at http://scripts.sil.org/OFL_web_fonts_and_RFNs 102 | 103 | 2.1 Can I make webpages using these fonts? 104 | Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Your three best options are: 105 | - referring directly in your stylesheet to open fonts which may be available on the user's system 106 | - providing links to download the full package of the font - either from your own website or from elsewhere - so users can install it themselves 107 | - using @font-face to distribute the font directly to browsers. This is recommended and explicitly allowed by the licensing model because it is distribution. The font file itself is distributed with other components of the webpage. It is not embedded in the webpage but referenced through a web address which will cause the browser to retrieve and use the corresponding font to render the webpage (see 1.11 and 1.15 for details related to embedding fonts into documents). As you take advantage of the @font-face cross-platform standard, be aware that web fonts are often tuned for a web environment and not intended for installation and use outside a browser. The reasons in favour of using web fonts are to allow design of dynamic text elements instead of static graphics, to make it easier for content to be localized and translated, indexed and searched, and all this with cross-platform open standards without depending on restricted extensions or plugins. You should check the CSS cascade (the order in which fonts are being called or delivered to your users) when testing. 108 | 109 | 2.2 Can I make and use WOFF (Web Open Font Format) versions of OFL fonts? 110 | Yes, but you need to be careful. A change in font format normally is considered modification, and Reserved Font Names (RFNs) cannot be used. Because of the design of the WOFF format, however, it is possible to create a WOFF version that is not considered modification, and so would not require a name change. You are allowed to create, use and distribute a WOFF version of an OFL font without changing the font name, but only if: 111 | 112 | - the original font data remains unchanged except for WOFF compression, and 113 | - WOFF-specific metadata is either omitted altogether or present and includes, unaltered, the contents of all equivalent metadata in the original font. 114 | 115 | If the original font data or metadata is changed, or the WOFF-specific metadata is incomplete, the font must be considered a Modified Version, the OFL restrictions would apply and the name of the font must be changed: any RFNs cannot be used and copyright notices and licensing information must be included and cannot be deleted or modified. You must come up with a unique name - we recommend one corresponding to your domain or your particular web application. Be aware that only the original author(s) can use RFNs. This is to prevent collisions between a derivative tuned to your audience and the original upstream version and so to reduce confusion. 116 | 117 | Please note that most WOFF conversion tools and online services do not meet the two requirements listed above, and so their output must be considered a Modified Version. So be very careful and check to be sure that the tool or service you're using is compressing unchanged data and completely and accurately reflecting the original font metadata. 118 | 119 | 2.3 What about other web font formats such as EOT/EOTLite/CWT/etc.? 120 | In most cases these formats alter the original font data more than WOFF, and do not completely support appropriate metadata, so their use must be considered modification and RFNs may not be used. However, there may be certain formats or usage scenarios that may allow the use of RFNs. See http://scripts.sil.org/OFL_web_fonts_and_RFNs 121 | 122 | 2.4 Can I make OFL fonts available through web font online services? 123 | Yes, you are welcome to include OFL fonts in online web font services as long as you properly meet all the conditions of the license. The origin and open status of the font should be clear among the other fonts you are hosting. Authorship, copyright notices and license information must be sufficiently visible to your users or subscribers so they know where the font comes from and the rights granted by the author(s). Make sure the font file contains the needed copyright notice(s) and licensing information in its metadata. Please double-check the accuracy of every field to prevent contradictory information. Other font formats, including EOT/EOTLite/CWT and superior alternatives like WOFF, already provide fields for this information. Remember that if you modify the font within your library or convert it to another format for any reason the OFL restrictions apply and you need to change the names accordingly. Please respect the author's wishes as expressed in the OFL and do not misrepresent original designers and their work. Don't lump quality open fonts together with dubious freeware or public domain fonts. Consider how you can best work with the original designers and foundries, support their efforts and generate goodwill that will benefit your service. (See 1.17 for details related to URL-based access restrictions methods or DRM mechanisms). 124 | 125 | 2.5 Some web font formats and services provide ways of "optimizing" the font for a particular website or web application; is that allowed? 126 | Yes, it is permitted, but remember that these optimized versions are Modified Versions and so must follow OFL requirements like appropriate renaming. Also you need to bear in mind the other important parameters beyond compression, speed and responsiveness: you need to consider the audience of your particular website or web application, as choosing some optimization parameters may turn out to be less than ideal for them. Subsetting by removing certain glyphs or features may seriously limit functionality of the font in various languages that your users expect. It may also introduce degradation of quality in the rendering or specific bugs on the various target platforms compared to the original font from upstream. In other words, remember that one person's optimized font may be another person's missing feature. Various advanced typographic features (OpenType, Graphite or AAT) are also available through CSS and may provide the desired effects without the need to modify the font. 127 | 128 | 2.6 Is subsetting a web font considered modification? 129 | Yes. Removing any parts of the font when delivering a web font to a browser, including unused glyphs and smart font code, is considered modification. This is permitted by the OFL but would not normally allow the use of RFNs. Some newer subsetting technologies may be able to subset in a way that allows users to effectively have access to the complete font, including smart font behaviour. See 2.8 and http://scripts.sil.org/OFL_web_fonts_and_RFNs 130 | 131 | 2.7 Are there any situations in which a modified web font could use RFNs? 132 | Yes. If a web font is optimized only in ways that preserve Functional Equivalence (see 2.8), then it may use RFNs, as it reasonably represents the Original Version and respects the intentions of the author(s) and the main purposes of the RFN mechanism (avoids collisions, protects authors, minimizes support, encourages derivatives). However this is technically very difficult and often impractical, so a much better scenario is for the web font service or provider to sign a separate agreement with the author(s) that allows the use of RFNs for Modified Versions. 133 | 134 | 2.8 How do you know if an optimization to a web font preserves Functional Equivalence? 135 | Functional Equivalence is described in full in the 'Web fonts and RFNs' paper at http://scripts.sil.org/OFL_web_fonts_and_RFNs, in general, an optimized font is deemed to be Functionally Equivalent (FE) to the Original Version if it: 136 | 137 | - Supports the same full character inventory. If a character can be properly displayed using the Original Version, then that same character, encoded correctly on a web page, will display properly. 138 | - Provides the same smart font behavior. Any dynamic shaping behavior that works with the Original Version should work when optimized, unless the browser or environment does not support it. There does not need to be guaranteed support in the client, but there should be no forced degradation of smart font or shaping behavior, such as the removal or obfuscation of OpenType, Graphite or AAT tables. 139 | - Presents text with no obvious degradation in visual quality. The lettershapes should be equally (or more) readable, within limits of the rendering platform. 140 | - Preserves original author, project and license metadata. At a minimum, this should include: Copyright and authorship; The license as stated in the Original Version, whether that is the full text of the OFL or a link to the web version; Any RFN declarations; Information already present in the font or documentation that points back to the Original Version, such as a link to the project or the author's website. 141 | 142 | If an optimized font meets these requirements, and so is considered to be FE, then it's very likely that the original author would feel that the optimized font is a good and reasonable equivalent. If it falls short of any of these requirements, the optimized font does not reasonably represent the Original Version, and so should be considered to be a Modified Version. Like other Modified Versions, it would not be allowed to use any RFNs and you simply need to pick your own font name. 143 | 144 | 2.9 Isn't use of web fonts another form of embedding? 145 | No. Unlike embedded fonts in a PDF, web fonts are not an integrated part of the document itself. They are not specific to a single document and are often applied to thousands of documents around the world. The font data is not stored alongside the document data and often originates from a different location. The ease by which the web fonts used by a document may be identified and downloaded for desktop use demonstrates that they are philosophically and technically separate from the web pages that specify them. See http://scripts.sil.org/OFL_web_fonts_and_RFNs 146 | 147 | 2.10 So would it be better to not use RFNs at all if you want your font to be distributed by a web fonts service? 148 | No. Although the OFL does not require authors to use RFNs, the RFN mechanism is an important part of the OFL model and completely compatible with web font services. If that web font service modifies the fonts, then the best solution is to sign a separate agreement for the use of any RFNs. It is perfectly valid for an author to not declare any RFNs, but before they do so they need to fully understand the benefits they are giving up, and the overall negative effect of allowing many different versions bearing the same name to be widely distributed. As a result, we don't generally recommend it. 149 | 150 | 2.11 What should an agreement for the use of RFNs say? Are there any examples? 151 | There is no prescribed format for this agreement, as legal systems vary, and no recommended examples. Authors may wish to add specific clauses to further restrict use, require author review of Modified Versions, establish user support mechanisms or provide terms for ending the agreement. Such agreements are usually not public, and apply only to the main parties. However, it would be very beneficial for web font services to clearly state when they have established such agreements, so that the public understands clearly that their service is operating appropriately. 152 | 153 | See the separate paper on 'Web Fonts & RFNs' for in-depth discussion of issues related to the use of RFNs for web fonts. This is available at http://scripts.sil.org/OFL_web_fonts_and_RFNs 154 | 155 | 156 | 3 MODIFYING OFL-LICENSED FONTS 157 | 158 | 3.1 Can I change the fonts? Are there any limitations to what things I can and cannot change? 159 | You are allowed to change anything, as long as such changes do not violate the terms of the license. In other words, you are not allowed to remove the copyright statement(s) from the font, but you could put additional information into it that covers your contribution. See the placeholders in the OFL header template for recommendations on where to add your own statements. (Remember that, when authors have reserved names via the RFN mechanism, you need to change the internal names of the font to your own font name when making your modified version even if it is just a small change.) 160 | 161 | 3.2 I have a font that needs a few extra glyphs - can I take them from an OFL licensed font and copy them into mine? 162 | Yes, but if you distribute that font to others it must be under the OFL, and include the information mentioned in condition 2 of the license. 163 | 164 | 3.3 Can I charge people for my additional work? In other words, if I add a bunch of special glyphs or OpenType/Graphite/AAT code, can I sell the enhanced font? 165 | Not by itself. Derivative fonts must be released under the OFL and cannot be sold by themselves. It is permitted, however, to include them in a larger software package (such as text editors, office suites or operating systems), even if the larger package is sold. In that case, you are strongly encouraged, but not required, to also make that derived font easily and freely available outside of the larger package. 166 | 167 | 3.4 Can I pay someone to enhance the fonts for my use and distribution? 168 | Yes. This is a good way to fund the further development of the fonts. Keep in mind, however, that if the font is distributed to others it must be under the OFL. You won't be able to recover your investment by exclusively selling the font, but you will be making a valuable contribution to the community. Please remember how you have benefited from the contributions of others. 169 | 170 | 3.5 I need to make substantial revisions to the font to make it work with my program. It will be a lot of work, and a big investment, and I want to be sure that it can only be distributed with my program. Can I restrict its use? 171 | No. If you redistribute a Modified Version of the font it must be under the OFL. You may not restrict it in any way beyond what the OFL permits and requires. This is intended to ensure that all released improvements to the fonts become available to everyone. But you will likely get an edge over competitors by being the first to distribute a bundle with the enhancements. Again, please remember how you have benefited from the contributions of others. 172 | 173 | 3.6 Do I have to make any derivative fonts (including extended source files, build scripts, documentation, etc.) publicly available? 174 | No, but please consider sharing your improvements with others. You may find that you receive in return more than what you gave. 175 | 176 | 3.7 If a trademark is claimed in the OFL font, does that trademark need to remain in modified fonts? 177 | Yes. Any trademark notices must remain in any derivative fonts to respect trademark laws, but you may add any additional trademarks you claim, officially registered or not. For example if an OFL font called "Foo" contains a notice that "Foo is a trademark of Acme", then if you rename the font to "Bar" when creating a Modified Version, the new trademark notice could say "Foo is a trademark of Acme Inc. - Bar is a trademark of Roadrunner Technologies Ltd.". Trademarks work alongside the OFL and are not subject to the terms of the licensing agreement. The OFL does not grant any rights under trademark law. Bear in mind that trademark law varies from country to country and that there are no international trademark conventions as there are for copyright. You may need to significantly invest in registering and defending a trademark for it to remain valid in the countries you are interested in. This may be costly for an individual independent designer. 178 | 179 | 3.8 If I commit changes to a font (or publish a branch in a DVCS) as part of a public open source software project, do I have to change the internal font names? 180 | Only if there are declared RFNs. Making a public commit or publishing a public branch is effectively redistributing your modifications, so any change to the font will require that you do not use the RFNs. Even if there are no RFNs, it may be useful to change the name or add a suffix indicating that a particular version of the font is still in development and not released yet. This will clearly indicate to users and fellow designers that this particular font is not ready for release yet. See section 5 for more details. 181 | 182 | 183 | 4 LICENSING YOUR ORIGINAL FONTS UNDER THE OFL 184 | 185 | 4.1 Can I use the SIL OFL for my own fonts? 186 | Yes! We heartily encourage everyone to use the OFL to distribute their own original fonts. It is a carefully constructed license that allows great freedom along with enough artistic integrity protection for the work of the authors as well as clear rules for other contributors and those who redistribute the fonts. The licensing model is used successfully by various organisations, both for-profit and not-for-profit, to release fonts of varying levels of scope and complexity. 187 | 188 | 4.2 What do I have to do to apply the OFL to my font? 189 | If you want to release your fonts under the OFL, we recommend you do the following: 190 | 191 | 4.2.1 Put your copyright and Reserved Font Names information at the beginning of the main OFL.txt file in place of the dedicated placeholders (marked with the <> characters). Include this file in your release package. 192 | 193 | 4.2.2 Put your copyright and the OFL text with your chosen Reserved Font Name(s) into your font files (the copyright and license fields). A link to the OFL text on the OFL web site is an acceptable (but not recommended) alternative. Also add this information to any other components (build scripts, glyph databases, documentation, test files, etc). Accurate metadata in your font files is beneficial to you as an increasing number of applications are exposing this information to the user. For example, clickable links can bring users back to your website and let them know about other work you have done or services you provide. Depending on the format of your fonts and sources, you can use template human-readable headers or machine-readable metadata. You should also double-check that there is no conflicting metadata in the font itself contradicting the license, such as the fstype bits in the os2 table or fields in the name table. 194 | 195 | 4.2.3 Write an initial FONTLOG.txt for your font and include it in the release package (see Section 6 and Appendix A for details including a template). 196 | 197 | 4.2.4 Include the relevant practical documentation on the license by adding the current OFL-FAQ.txt file in your package. 198 | 199 | 4.2.5 If you wish you can use the OFL graphics (http://scripts.sil.org/OFL_logo) on your website. 200 | 201 | 4.3 Will you make my font OFL for me? 202 | We won't do the work for you. We can, however, try to answer your questions, unfortunately we do not have the resources to review and check your font packages for correct use of the OFL. We recommend you turn to designers, foundries or consulting companies with experience in doing open font design to provide this service to you. 203 | 204 | 4.4 Will you distribute my OFL font for me? 205 | No, although if the font is of sufficient quality and general interest we may include a link to it on our partial list of OFL fonts on the OFL web site. You may wish to consider other open font catalogs or hosting services, such as the Unifont Font Guide (http://unifont.org/fontguide), The League of Movable Type (http://theleagueofmovabletype.com) or the Open Font Library (http://openfontlibrary.org/), which despite the name has no direct relationship to the OFL or SIL. We do not endorse any particular catalog or hosting service - it is your responsibility to determine if the service is right for you and if it treats authors with fairness. 206 | 207 | 4.5 Why should I use the OFL for my fonts? 208 | - to meet needs for fonts that can be modified to support lesser-known languages 209 | - to provide a legal and clear way for people to respect your work but still use it (and reduce piracy) 210 | - to involve others in your font project 211 | - to enable your fonts to be expanded with new weights and improved writing system/language support 212 | - to allow more technical font developers to add features to your design (such as OpenType, Graphite or AAT support) 213 | - to renew the life of an old font lying on your hard drive with no business model 214 | - to allow your font to be included in Libre Software operating systems like Ubuntu 215 | - to give your font world status and wide, unrestricted distribution 216 | - to educate students about quality typeface and font design 217 | - to expand your test base and get more useful feedback 218 | - to extend your reach to new markets when users see your metadata and go to your website 219 | - to get your font more easily into one of the web font online services 220 | - to attract attention for your commercial fonts 221 | - to make money through web font services 222 | - to make money by bundling fonts with applications 223 | - to make money adjusting and extending existing open fonts 224 | - to get a better chance that foundations/NGOs/charities/companies who commission fonts will pick you 225 | - to be part of a sharing design and development community 226 | - to give back and contribute to a growing body of font sources 227 | 228 | 229 | 5 CHOOSING RESERVED FONT NAMES 230 | 231 | 5.1 What are Reserved Font Names? 232 | These are font names, or portions of font names, that the author has chosen to reserve for use only with the Original Version of the font, or for Modified Version(s) created by the original author. 233 | 234 | 5.2 Why can't I use the Reserved Font Names in my derivative font names? I'd like people to know where the design came from. 235 | The best way to acknowledge the source of the design is to thank the original authors and any other contributors in the files that are distributed with your revised font (although no acknowledgement is required). The FONTLOG is a natural place to do this. Reserved Font Names ensure that the only fonts that have the original names are the unmodified Original Versions. This allows designers to maintain artistic integrity while allowing collaboration to happen. It eliminates potential confusion and name conflicts. When choosing a name, be creative and avoid names that reuse almost all the same letters in the same order or sound like the original. It will help everyone if Original Versions and Modified Versions can easily be distinguished from one another and from other derivatives. Any substitution and matching mechanism is outside the scope of the license. 236 | 237 | 5.3 What do you mean by "primary name as presented to the user"? Are you referring to the font menu name? 238 | Yes, this applies to the font menu name and other mechanisms that specify a font in a document. It would be fine, however, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement). Users who install derivatives (Modified Versions) on their systems should not see any of the original Reserved Font Names in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake one font for another and so expect features only another derivative or the Original Version can actually offer. 239 | 240 | 5.4 Am I not allowed to use any part of the Reserved Font Names? 241 | You may not use individual words from the Reserved Font Names, but you would be allowed to use parts of words, as long as you do not use any word from the Reserved Font Names entirely. We do not recommend using parts of words because of potential confusion, but it is allowed. For example, if "Foobar" was a Reserved Font Name, you would be allowed to use "Foo" or "bar", although we would not recommend it. Such an unfortunate choice would confuse the users of your fonts as well as make it harder for other designers to contribute. 242 | 243 | 5.5 So what should I, as an author, identify as Reserved Font Names? 244 | Original authors are encouraged to name their fonts using clear, distinct names, and only declare the unique parts of the name as Reserved Font Names. For example, the author of a font called "Foobar Sans" would declare "Foobar" as a Reserved Font Name, but not "Sans", as that is a common typographical term, and may be a useful word to use in a derivative font name. Reserved Font Names should also be single words for simplicity and legibility. A font called "Flowing River" should have Reserved Font Names "Flowing" and "River", not "Flowing River". You also need to be very careful about reserving font names which are already linked to trademarks (whether registered or not) which you do not own. 245 | 246 | 5.6 Do I, as an author, have to identify any Reserved Font Names? 247 | No. RFNs are optional and not required, but we encourage you to use them. This is primarily to avoid confusion between your work and Modified Versions. As an author you can release a font under the OFL and not declare any Reserved Font Names. There may be situations where you find that using no RFNs and letting your font be changed and modified - including any kind of modification - without having to change the original name is desirable. However you need to be fully aware of the consequences. There will be no direct way for end-users and other designers to distinguish your Original Version from many Modified Versions that may be created. You have to trust whoever is making the changes and the optimizations to not introduce problematic changes. The RFNs you choose for your own creation have value to you as an author because they allow you to maintain artistic integrity and keep some control over the distribution channel to your end-users. For discussion of RFNs and web fonts see section 2. 248 | 249 | 5.7 Are any names (such as the main font name) reserved by default? 250 | No. That is a change to the license as of version 1.1. If you want any names to be Reserved Font Names, they must be specified after the copyright statement(s). 251 | 252 | 5.8 Is there any situation in which I can use Reserved Font Names for a Modified Version? 253 | The Copyright Holder(s) can give certain trusted parties the right to use any of the Reserved Font Names through separate written agreements. For example, even if "Foobar" is a RFN, you could write up an agreement to give company "XYZ" the right to distribute a modified version with a name that includes "Foobar". This allows for freedom without confusion. The existence of such an agreement should be made as clear as possible to downstream users and designers in the distribution package and the relevant documentation. They need to know if they are a party to the agreement or not and what they are practically allowed to do or not even if all the details of the agreement are not public. 254 | 255 | 5.9 Do font rebuilds require a name change? Do I have to change the name of the font when my packaging workflow includes a full rebuild from source? 256 | Yes, all rebuilds which change the font data and the smart code are Modified Versions and the requirements of the OFL apply: you need to respect what the Author(s) have chosen in terms of Reserved Font Names. However if a package (or installer) is simply a wrapper or a compressed structure around the final font - leaving them intact on the inside - then no name change is required. Please get in touch with the author(s) and copyright holder(s) to inquire about the presence of font sources beyond the final font file(s) and the recommended build path. That build path may very well be non-trivial and hard to reproduce accurately by the maintainer. If a full font build path is made available by the upstream author(s) please be aware that any regressions and changes you may introduce when doing a rebuild for packaging purposes is your own responsibility as a package maintainer since you are effectively creating a separate branch. You should make it very clear to your users that your rebuilt version is not the canonical one from upstream. 257 | 258 | 5.10 Can I add other Reserved Font Names when making a derivative font? 259 | Yes. List your additional Reserved Font Names after your additional copyright statement, as indicated with example placeholders at the top of the OFL.txt file. Be sure you do not remove any existing RFNs but only add your own. RFN statements should be placed next to the copyright statement of the relevant author as indicated in the OFL.txt template to make them visible to designers wishing to make their separate version. 260 | 261 | 262 | 6 ABOUT THE FONTLOG 263 | 264 | 6.1 What is this FONTLOG thing exactly? 265 | It has three purposes: 1) to provide basic information on the font to users and other designers and developers, 2) to document changes that have been made to the font or accompanying files, either by the original authors or others, and 3) to provide a place to acknowledge authors and other contributors. Please use it! 266 | 267 | 6.2 Is the FONTLOG required? 268 | It is not a requirement of the license, but we strongly recommend you have one. 269 | 270 | 6.3 Am I required to update the FONTLOG when making Modified Versions? 271 | No, but users, designers and other developers might get very frustrated with you if you don't. People need to know how derivative fonts differ from the original, and how to take advantage of the changes, or build on them. There are utilities that can help create and maintain a FONTLOG, such as the FONTLOG support in FontForge. 272 | 273 | 6.4 What should the FONTLOG look like? 274 | It is typically a separate text file (FONTLOG.txt), but can take other formats. It commonly includes these four sections: 275 | 276 | - brief header describing the FONTLOG itself and name of the font family 277 | - Basic Font Information - description of the font family, purpose and breadth 278 | - ChangeLog - chronological listing of changes 279 | - Acknowledgements - list of authors and contributors with contact information 280 | 281 | It could also include other sections, such as: where to find documentation, how to make contributions, information on contributing organizations, source code details, and a short design guide. See Appendix A for an example FONTLOG. 282 | 283 | 284 | 7 MAKING CONTRIBUTIONS TO OFL PROJECTS 285 | 286 | 7.1 Can I contribute work to OFL projects? 287 | In many cases, yes. It is common for OFL fonts to be developed by a team of people who welcome contributions from the wider community. Contact the original authors for specific information on how to participate in their projects. 288 | 289 | 7.2 Why should I contribute my changes back to the original authors? 290 | It would benefit many people if you contributed back in response to what you've received. Your contributions and improvements to the fonts and other components could be a tremendous help and would encourage others to contribute as well and 'give back'. You will then benefit from other people's contributions as well. Sometimes maintaining your own separate version takes more effort than merging back with the original. Be aware that any contributions, however, must be either your own original creation or work that you own, and you may be asked to affirm that clearly when you contribute. 291 | 292 | 7.3 I've made some very nice improvements to the font. Will you consider adopting them and putting them into future Original Versions? 293 | Most authors would be very happy to receive such contributions. Keep in mind that it is unlikely that they would want to incorporate major changes that would require additional work on their end. Any contributions would likely need to be made for all the fonts in a family and match the overall design and style. Authors are encouraged to include a guide to the design with the fonts. It would also help to have contributions submitted as patches or clearly marked changes - the use of smart source revision control systems like subversion, mercurial, git or bzr is a good idea. Please follow the recommendations given by the author(s) in terms of preferred source formats and configuration parameters for sending contributions. If this is not indicated in a FONTLOG or other documentation of the font, consider asking them directly. Examples of useful contributions are bug fixes, additional glyphs, stylistic alternates (and the smart font code to access them) or improved hinting. Keep in mind that some kinds of changes (esp. hinting) may be technically difficult to integrate. 294 | 295 | 7.4 How can I financially support the development of OFL fonts? 296 | It is likely that most authors of OFL fonts would accept financial contributions - contact them for instructions on how to do this. Such contributions would support future development. You can also pay for others to enhance the fonts and contribute the results back to the original authors for inclusion in the Original Version. 297 | 298 | 299 | 8 ABOUT THE LICENSE ITSELF 300 | 301 | 8.1 I see that this is version 1.1 of the license. Will there be later changes? 302 | Version 1.1 is the first minor revision of the OFL. We are confident that version 1.1 will meet most needs, but are open to future improvements. Any revisions would be for future font releases, and previously existing licenses would remain in effect. No retroactive changes are possible, although the Copyright Holder(s) can re-release the font under a revised OFL. All versions will be available on our web site: http://scripts.sil.org/OFL. 303 | 304 | 8.2 Does this license restrict the rights of the Copyright Holder(s)? 305 | No. The Copyright Holder(s) still retain(s) all the rights to their creation; they are only releasing a portion of it for use in a specific way. For example, the Copyright Holder(s) may choose to release a 'basic' version of their font under the OFL, but sell a restricted 'enhanced' version. Only the Copyright Holder(s) can do this. 306 | 307 | 8.3 Is the OFL a contract or a license? 308 | The OFL is a license and not a contract and so does not require you to sign it to have legal validity. By using, modifying and redistributing components under the OFL you indicate that you accept the license. 309 | 310 | 8.4 I really like the terms of the OFL, but want to change it a little. Am I allowed to take ideas and actual wording from the OFL and put them into my own custom license for distributing my fonts? 311 | We strongly recommend against creating your very own unique open licensing model. Using a modified or derivative license will likely cut you off - along with the font(s) under that license - from the community of designers using the OFL, potentially expose you and your users to legal liabilities, and possibly put your work and rights at risk. The OFL went though a community and legal review process that took years of effort, and that review is only applicable to an unmodified OFL. The text of the OFL has been written by SIL (with review and consultation from the community) and is copyright (c) 2005-2013 SIL International. You may re-use the ideas and wording (in part, not in whole) in another non-proprietary license provided that you call your license by another unambiguous name, that you do not use the preamble, that you do not mention SIL and that you clearly present your license as different from the OFL so as not to cause confusion by being too similar to the original. If you feel the OFL does not meet your needs for an open license, please contact us. 312 | 313 | 8.5 Can I translate the license and the FAQ into other languages? 314 | SIL certainly recognises the need for people who are not familiar with English to be able to understand the OFL and its use. Making the license very clear and readable has been a key goal for the OFL, but we know that people understand their own language best. 315 | 316 | If you are an experienced translator, you are very welcome to translate the OFL and OFL-FAQ so that designers and users in your language community can understand the license better. But only the original English version of the license has legal value and has been approved by the community. Translations do not count as legal substitutes and should only serve as a way to explain the original license. SIL - as the author and steward of the license for the community at large - does not approve any translation of the OFL as legally valid because even small translation ambiguities could be abused and create problems. 317 | 318 | SIL gives permission to publish unofficial translations into other languages provided that they comply with the following guidelines: 319 | 320 | - Put the following disclaimer in both English and the target language stating clearly that the translation is unofficial: 321 | 322 | "This is an unofficial translation of the SIL Open Font License into . It was not published by SIL International, and does not legally state the distribution terms for fonts that use the OFL. A release under the OFL is only valid when using the original English text. However, we recognize that this unofficial translation will help users and designers not familiar with English to better understand and use the OFL. We encourage designers who consider releasing their creation under the OFL to read the OFL-FAQ in their own language if it is available. Please go to http://scripts.sil.org/OFL for the official version of the license and the accompanying OFL-FAQ." 323 | 324 | - Keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion. 325 | 326 | If you start such a unofficial translation effort of the OFL and OFL-FAQ please let us know. 327 | 328 | 329 | 9 ABOUT SIL INTERNATIONAL 330 | 331 | 9.1 Who is SIL International and what do they do? 332 | SIL serves language communities worldwide, building their capacity for sustainable language development, by means of research, translation, training and materials development. SIL makes its services available to all without regard to religious belief, political ideology, gender, race, or ethnic background. SIL's members and volunteers share a Christian commitment. 333 | 334 | 9.2 What does this have to do with font licensing? 335 | The ability to read, write, type and publish in one's own language is one of the most critical needs for millions of people around the world. This requires fonts that are widely available and support lesser-known languages. SIL develops - and encourages others to develop - a complete stack of writing systems implementation components available under open licenses. This open stack includes input methods, smart fonts, smart rendering libraries and smart applications. There has been a need for a common open license that is specifically applicable to fonts and related software (a crucial component of this stack), so SIL developed the SIL Open Font License with the help of the Free/Libre and Open Source Software community. 336 | 337 | 9.3 How can I contact SIL? 338 | Our main web site is: http://www.sil.org/ 339 | Our site about complex scripts is: http://scripts.sil.org/ 340 | Information about this license (and contact information) is at: http://scripts.sil.org/OFL 341 | 342 | 343 | APPENDIX A - FONTLOG EXAMPLE 344 | 345 | Here is an example of the recommended format for a FONTLOG, although other formats are allowed. 346 | 347 | ----- 348 | FONTLOG for the GlobalFontFamily fonts 349 | 350 | This file provides detailed information on the GlobalFontFamily Font Software. This information should be distributed along with the GlobalFontFamily fonts and any derivative works. 351 | 352 | Basic Font Information 353 | 354 | GlobalFontFamily is a Unicode typeface family that supports all languages that use the Latin script and its variants, and could be expanded to support other scripts. 355 | 356 | NewWorldFontFamily is based on the GlobalFontFamily and also supports Greek, Hebrew, Cyrillic and Armenian. 357 | 358 | More specifically, this release supports the following Unicode ranges... 359 | This release contains... 360 | Documentation can be found at... 361 | To contribute to the project... 362 | 363 | ChangeLog 364 | 365 | 10 December 2010 (Fred Foobar) GlobalFontFamily-devel version 1.4 366 | - fix new build and testing system (bug #123456) 367 | 368 | 1 August 2008 (Tom Parker) GlobalFontFamily version 1.2.1 369 | - Tweaked the smart font code (Branch merged with trunk version) 370 | - Provided improved build and debugging environment for smart behaviours 371 | 372 | 7 February 2007 (Pat Johnson) NewWorldFontFamily Version 1.3 373 | - Added Greek and Cyrillic glyphs 374 | 375 | 7 March 2006 (Fred Foobar) NewWorldFontFamily Version 1.2 376 | - Tweaked contextual behaviours 377 | 378 | 1 Feb 2005 (Jane Doe) NewWorldFontFamily Version 1.1 379 | - Improved build script performance and verbosity 380 | - Extended the smart code documentation 381 | - Corrected minor typos in the documentation 382 | - Fixed position of combining inverted breve below (U+032F) 383 | - Added OpenType/Graphite smart code for Armenian 384 | - Added Armenian glyphs (U+0531 -> U+0587) 385 | - Released as "NewWorldFontFamily" 386 | 387 | 1 Jan 2005 (Joe Smith) GlobalFontFamily Version 1.0 388 | - Initial release 389 | 390 | Acknowledgements 391 | 392 | If you make modifications be sure to add your name (N), email (E), web-address (if you have one) (W) and description (D). This list is in alphabetical order. 393 | 394 | N: Jane Doe 395 | E: jane@university.edu 396 | W: http://art.university.edu/projects/fonts 397 | D: Contributor - Armenian glyphs and code 398 | 399 | N: Fred Foobar 400 | E: fred@foobar.org 401 | W: http://foobar.org 402 | D: Contributor - misc Graphite fixes 403 | 404 | N: Pat Johnson 405 | E: pat@fontstudio.org 406 | W: http://pat.fontstudio.org 407 | D: Designer - Greek & Cyrillic glyphs based on Roman design 408 | 409 | N: Tom Parker 410 | E: tom@company.com 411 | W: http://www.company.com/tom/projects/fonts 412 | D: Engineer - original smart font code 413 | 414 | N: Joe Smith 415 | E: joe@fontstudio.org 416 | W: http://joe.fontstudio.org 417 | D: Designer - original Roman glyphs 418 | 419 | Fontstudio.org is an not-for-profit design group whose purpose is... 420 | Foobar.org is a distributed community of developers... 421 | Company.com is a small business who likes to support community designers... 422 | University.edu is a renowned educational institution with a strong design department... 423 | ----- 424 | 425 | 426 | -------------------------------------------------------------------------------- /res/fonts/APACHE2.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /res/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 by Red Hat, Inc. All rights reserved. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /res/fonts/Overpass-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/res/fonts/Overpass-Black.ttf -------------------------------------------------------------------------------- /res/fonts/Overpass-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/res/fonts/Overpass-Bold.ttf -------------------------------------------------------------------------------- /res/fonts/Overpass-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/res/fonts/Overpass-ExtraBold.ttf -------------------------------------------------------------------------------- /res/fonts/Overpass-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/res/fonts/Overpass-Regular.ttf -------------------------------------------------------------------------------- /res/fonts/RobotoMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/res/fonts/RobotoMono-Bold.ttf -------------------------------------------------------------------------------- /res/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/res/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /screenshots/envelope-chaining.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/envelope-chaining.png -------------------------------------------------------------------------------- /screenshots/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/interface.png -------------------------------------------------------------------------------- /screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/main.png -------------------------------------------------------------------------------- /screenshots/miniramp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/miniramp.png -------------------------------------------------------------------------------- /screenshots/ministep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/ministep.png -------------------------------------------------------------------------------- /screenshots/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/record.png -------------------------------------------------------------------------------- /screenshots/sample-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/sample-player.png -------------------------------------------------------------------------------- /screenshots/screenshots-patch.vcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/screenshots-patch.vcv -------------------------------------------------------------------------------- /screenshots/sequencer-envelope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/sequencer-envelope.png -------------------------------------------------------------------------------- /screenshots/waveshaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgunyho/PdArray/8bd796f40fb77f78c22a8cbbbcf619c389bc9e65/screenshots/waveshaper.png -------------------------------------------------------------------------------- /src/Array.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | #include //NOTE: not officially part of the public Rack v2 API, but other plugins (including Fundamental) do it this way 3 | #include // std::min, std::swap, std::transform 4 | #define DR_WAV_IMPLEMENTATION 5 | #include "dr_wav.h" // for reading wav files 6 | 7 | #include "Widgets.hpp" 8 | 9 | #include 10 | 11 | //TODO: load buffer from text/csv file? 12 | //TODO: prevent audio clicking at the last sample 13 | //TODO: undo history? hard? memory intensive? 14 | //TODO: visual representation choice right-click submenu (stairs (current), lines, points, bars) 15 | //TODO: reinterpolate array on resize (+right-click menu option for that) 16 | 17 | struct Array : Module { 18 | enum ParamIds { 19 | PHASE_RANGE_PARAM, 20 | OUTPUT_RANGE_PARAM, 21 | REC_ENABLE_PARAM, 22 | NUM_PARAMS 23 | }; 24 | enum InputIds { 25 | PHASE_INPUT, 26 | REC_SIGNAL_INPUT, 27 | REC_PHASE_INPUT, 28 | REC_ENABLE_INPUT, 29 | NUM_INPUTS 30 | }; 31 | enum OutputIds { 32 | STEP_OUTPUT, 33 | INTERP_OUTPUT, 34 | NUM_OUTPUTS 35 | }; 36 | enum LightIds { 37 | REC_LIGHT, 38 | NUM_LIGHTS 39 | }; 40 | 41 | enum InterpBoundaryMode { 42 | INTERP_CONSTANT, 43 | INTERP_MIRROR, 44 | INTERP_PERIODIC, 45 | NUM_INTERP_MODES 46 | }; 47 | 48 | enum RecordingMode { 49 | GATE, 50 | TOGGLE, 51 | NUM_REC_MODES 52 | }; 53 | 54 | enum DataSaveMode { 55 | SAVE_FULL_DATA, 56 | SAVE_PATH_TO_SAMPLE, 57 | DONT_SAVE_DATA, 58 | NUM_DATA_SAVING_MODES, 59 | }; 60 | 61 | float phases[MAX_POLY_CHANNELS]; 62 | int nChannels = 1; 63 | float recPhase = 0.f; 64 | float sampleRate; // so that it can be read by the UI 65 | RecordingMode recMode = GATE; 66 | dsp::SchmittTrigger recTrigger; 67 | dsp::SchmittTrigger recClickTrigger; 68 | bool isRecording = false; 69 | std::vector buffer; 70 | std::string lastLoadedPath; 71 | bool enableEditing = true; 72 | DataSaveMode saveMode = SAVE_FULL_DATA; 73 | InterpBoundaryMode boundaryMode = INTERP_PERIODIC; 74 | 75 | // If the array size is smaller than this, serialize as JSON, otherwise 76 | // serialize as wav in the patch storage folder. Floats are serialized in 77 | // json as ~20 bytes, so 5k elements will be 100 KB, which is the limit 78 | // recommended by the manual. 79 | const static unsigned int directSerializationThreshold = 5000; 80 | const static std::string arrayDataFileName; 81 | 82 | void initBuffer() { 83 | buffer.clear(); 84 | int default_steps = 10; 85 | for(int i = 0; i < default_steps; i++) { 86 | buffer.push_back(i / (default_steps - 1.f)); 87 | } 88 | } 89 | 90 | Array() { 91 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 92 | configSwitch(Array::OUTPUT_RANGE_PARAM, 0, 2, 0, "Recording and output range", { 93 | "-10..10 V", 94 | "-5..5 V", 95 | "0..10 V", 96 | }); 97 | configSwitch(Array::PHASE_RANGE_PARAM, 0, 2, 2, "Position CV range", { 98 | "-10..10 V", 99 | "-5..5 V", 100 | "0..10 V", 101 | }); 102 | configSwitch(Array::REC_ENABLE_PARAM, 0.f, 1.f, 0.f, "Record", {"Off", "On"}); 103 | 104 | configInput(PHASE_INPUT, "Playback position"); 105 | configInput(REC_SIGNAL_INPUT, "Signal to record"); 106 | configInput(REC_PHASE_INPUT, "Recording position"); 107 | configInput(REC_ENABLE_INPUT, "Recording enable"); 108 | 109 | configOutput(STEP_OUTPUT, "Direct (step)"); 110 | configOutput(INTERP_OUTPUT, "Smooth (interpolated)"); 111 | 112 | configBypass(REC_SIGNAL_INPUT, STEP_OUTPUT); 113 | configBypass(REC_SIGNAL_INPUT, INTERP_OUTPUT); 114 | 115 | for(int i = 0; i < MAX_POLY_CHANNELS; i++) phases[i] = 0.f; 116 | initBuffer(); 117 | } 118 | 119 | void process(const ProcessArgs &args) override; 120 | 121 | float getZeroValue() { 122 | // The buffer internal values are always 0..1. Depending on the 123 | // signedness of the output, the buffer value corresponding to 0V 124 | // output is either 0.0 or 0.5. This function returns that value. 125 | bool outputIsSigned = params[OUTPUT_RANGE_PARAM].getValue() < 1.5f; 126 | return outputIsSigned ? 0.5f : 0.f; 127 | } 128 | 129 | void resizeBuffer(unsigned int newSize) { 130 | buffer.resize(newSize, getZeroValue()); 131 | } 132 | 133 | size_t numFadeSamples() { 134 | // Calculate the clicking prevention fade size (in samples) 135 | // based on the current buffer size. 136 | size_t n = buffer.size(); 137 | if(n < 5) { 138 | return 0; 139 | } else { 140 | return std::min(n / 100 + 2, 200); 141 | } 142 | } 143 | 144 | void loadSample(std::string path, bool resizeBuf = false); 145 | void saveWav(std::string path); 146 | 147 | json_t *dataToJson() override { 148 | json_t *root = json_object(); 149 | json_object_set_new(root, "enableEditing", json_boolean(enableEditing)); 150 | json_object_set_new(root, "boundaryMode", json_integer(boundaryMode)); 151 | json_object_set_new(root, "recMode", json_integer(recMode)); 152 | json_object_set_new(root, "lastLoadedPath", json_string(lastLoadedPath.c_str())); 153 | 154 | // we want to delete the wav file created by onSave in most cases, see below 155 | bool deleteWavFile = true; 156 | 157 | if(saveMode == SAVE_FULL_DATA) { 158 | if(buffer.size() <= directSerializationThreshold) { 159 | 160 | json_t *arr = json_array(); 161 | for(float x : buffer) { 162 | json_array_append_new(arr, json_real(x)); 163 | } 164 | json_object_set(root, "arrayData", arr); 165 | json_decref(arr); 166 | 167 | } else { 168 | deleteWavFile = false; 169 | } 170 | } else if(saveMode == SAVE_PATH_TO_SAMPLE) { 171 | json_object_set_new(root, "arrayData", json_string(lastLoadedPath.c_str())); 172 | } else if(saveMode == DONT_SAVE_DATA) { 173 | json_object_set_new(root, "arrayData", json_integer(buffer.size())); 174 | } 175 | 176 | if(deleteWavFile) { 177 | std::string path = system::join(createPatchStorageDirectory(), arrayDataFileName); 178 | // make sure that the wav file doesn't exist, so we don't read from the file the next time we load the patch 179 | if(system::isFile(path)) { 180 | system::remove(path); 181 | } 182 | } 183 | return root; 184 | } 185 | 186 | void dataFromJson(json_t *root) override { 187 | json_t *enableEditing_J = json_object_get(root, "enableEditing"); 188 | json_t *boundaryMode_J = json_object_get(root, "boundaryMode"); 189 | json_t *recMode_J = json_object_get(root, "recMode"); 190 | json_t *arrayData_J = json_object_get(root, "arrayData"); 191 | json_t *lastLoadedPath_J = json_object_get(root, "lastLoadedPath"); 192 | 193 | if(enableEditing_J) { 194 | enableEditing = json_boolean_value(enableEditing_J); 195 | } 196 | if(boundaryMode_J) { 197 | int bm = int(json_integer_value(boundaryMode_J)); 198 | if(bm < NUM_INTERP_MODES) { 199 | boundaryMode = static_cast(bm); 200 | } 201 | } 202 | if(recMode_J) { 203 | int rm = int(json_integer_value(recMode_J)); 204 | if(rm < NUM_REC_MODES) { 205 | recMode = static_cast(rm); 206 | } 207 | } 208 | if(lastLoadedPath_J) { 209 | lastLoadedPath = std::string(json_string_value(lastLoadedPath_J)); 210 | } 211 | 212 | if(json_array_size(arrayData_J) > 0) { 213 | buffer.clear(); 214 | size_t i; 215 | json_t *val; 216 | json_array_foreach(arrayData_J, i, val) { 217 | buffer.push_back(json_real_value(val)); 218 | } 219 | saveMode = SAVE_FULL_DATA; 220 | } else if(json_string_value(arrayData_J) != NULL) { 221 | lastLoadedPath = std::string(json_string_value(arrayData_J)); 222 | loadSample(lastLoadedPath, true); 223 | enableEditing = false; 224 | saveMode = SAVE_PATH_TO_SAMPLE; 225 | } else if(json_integer_value(arrayData_J) > 0) { 226 | buffer.clear(); 227 | resizeBuffer(json_integer_value(arrayData_J)); 228 | saveMode = DONT_SAVE_DATA; 229 | } 230 | // else, arrayData was missing from JSON, so we assume it's loaded from wav file in patch storage folder 231 | } 232 | 233 | void onAdd(const AddEvent& e) override { 234 | std::string path = system::join(createPatchStorageDirectory(), arrayDataFileName); 235 | if(system::isFile(path)) { 236 | // if the file exists, we assume that we're supposed to load the 237 | // data from there instead of the JSON, i.e., it was above the 238 | // directSerializationThreshold. 239 | loadSample(path, true); 240 | } 241 | } 242 | 243 | void onSave(const SaveEvent& e) override { 244 | if(buffer.size() > directSerializationThreshold) { 245 | std::string path = system::join(createPatchStorageDirectory(), arrayDataFileName); 246 | saveWav(path); 247 | } 248 | } 249 | 250 | void onReset() override { 251 | boundaryMode = INTERP_PERIODIC; 252 | enableEditing = true; 253 | initBuffer(); 254 | } 255 | 256 | void onRandomize() override { 257 | Module::onRandomize(); 258 | for(unsigned int i = 0; i < buffer.size(); i++) { 259 | buffer[i] = random::uniform(); 260 | } 261 | } 262 | }; 263 | 264 | const std::string Array::arrayDataFileName = "arraydata.wav"; 265 | 266 | void Array::loadSample(std::string path, bool resizeBuf) { 267 | unsigned int channels, sampleRate; 268 | drwav_uint64 totalPCMFrameCount; 269 | float* pSampleData = drwav_open_file_and_read_pcm_frames_f32(path.c_str(), &channels, &sampleRate, &totalPCMFrameCount); 270 | 271 | if (pSampleData != NULL) { 272 | unsigned long nSamplesToRead = std::min((unsigned long) totalPCMFrameCount, 999999UL); 273 | unsigned long newSize = resizeBuf ? nSamplesToRead : buffer.size(); 274 | buffer.resize(newSize, 0); 275 | unsigned long max_i = std::min(newSize, nSamplesToRead); 276 | for(unsigned long i = 0; i < max_i; i++) { 277 | int ii = i * channels; 278 | float s = pSampleData[ii]; 279 | if(channels == 2) { 280 | s = (s + pSampleData[ii + 1]) * 0.5f; // mix stereo channels, good idea? 281 | } 282 | buffer[i] = (s + 1.f) * 0.5f; 283 | } 284 | } 285 | 286 | drwav_free(pSampleData); 287 | } 288 | 289 | void Array::saveWav(std::string path) { 290 | // use drwav to save the buffer as a wav file. 291 | // based on VCV Fundamental wavetable.save(); 292 | drwav_data_format format; 293 | format.container = drwav_container_riff; 294 | format.format = DR_WAVE_FORMAT_PCM; 295 | format.channels = 1; 296 | format.sampleRate = sampleRate; // note: this doesn't really matter, because we're not using the info when reading the sample back 297 | format.bitsPerSample = 16; 298 | 299 | drwav wav; 300 | if(!drwav_init_file_write(&wav, path.c_str(), &format)) 301 | return; 302 | 303 | // Rescale from the range 0..1 to -1..1 304 | std::vector buffer_rescaled = buffer; 305 | std::transform(buffer_rescaled.begin(), buffer_rescaled.end(), buffer_rescaled.begin(), 306 | [](float y) -> float { return (y - 0.5f) * 2.f; }); 307 | 308 | size_t len = buffer_rescaled.size(); 309 | int16_t* buf = new int16_t[len]; 310 | drwav_f32_to_s16(buf, buffer_rescaled.data(), len); 311 | drwav_write_pcm_frames(&wav, len, buf); 312 | delete[] buf; 313 | 314 | drwav_uninit(&wav); 315 | } 316 | 317 | void Array::process(const ProcessArgs &args) { 318 | sampleRate = args.sampleRate; 319 | 320 | float phaseMin, phaseMax; 321 | float prange = params[PHASE_RANGE_PARAM].getValue(); 322 | if(prange > 1.5f) { 323 | phaseMin = 0.f; 324 | phaseMax = 10.f; 325 | } else if(prange > 0.5f) { 326 | phaseMin = -5.f; 327 | phaseMax = 5.f; 328 | } else { 329 | phaseMin = -10.f; 330 | phaseMax = 10.f; 331 | } 332 | 333 | int size = buffer.size(); 334 | 335 | 336 | float inOutMin, inOutMax; 337 | float orange = params[OUTPUT_RANGE_PARAM].getValue(); 338 | if(orange > 1.5f) { 339 | inOutMin = 0.f; 340 | inOutMax = 10.f; 341 | } else if(orange > 0.5f) { 342 | inOutMin = -5.f; 343 | inOutMax = 5.f; 344 | } else { 345 | inOutMin = -10.f; 346 | inOutMax = 10.f; 347 | } 348 | 349 | // recording 350 | recPhase = clamp(rescale(inputs[REC_PHASE_INPUT].getVoltage(), phaseMin, phaseMax, 0.f, 1.f), 0.f, 1.f); 351 | int ri = int(recPhase * size); 352 | bool recWasTriggered = recTrigger.process(rescale(inputs[REC_ENABLE_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); 353 | bool recWasClicked = recClickTrigger.process(params[REC_ENABLE_PARAM].getValue()); 354 | 355 | if(recMode == GATE) { 356 | isRecording = recTrigger.isHigh() || recClickTrigger.isHigh(); 357 | } else if(recMode == TOGGLE && (recWasTriggered || recWasClicked)) { 358 | isRecording = !isRecording; 359 | } 360 | if(isRecording) { 361 | buffer[ri] = clamp(rescale(inputs[REC_SIGNAL_INPUT].getVoltage(), inOutMin, inOutMax, 0.f, 1.f), 0.f, 1.f); 362 | } 363 | lights[REC_LIGHT].setBrightness(isRecording); 364 | 365 | nChannels = inputs[PHASE_INPUT].getChannels(); 366 | outputs[STEP_OUTPUT].setChannels(nChannels); 367 | outputs[INTERP_OUTPUT].setChannels(nChannels); 368 | for(int chan = 0; chan < nChannels; chan++) { 369 | float phase = clamp(rescale(inputs[PHASE_INPUT].getVoltage(chan), phaseMin, phaseMax, 0.f, 1.f), 0.f, 1.f); 370 | phases[chan] = phase; 371 | // direct output 372 | int i_step = clamp((int) std::floor(phase * size), 0, size - 1); 373 | outputs[STEP_OUTPUT].setVoltage(rescale(buffer[i_step], 0.f, 1.f, inOutMin, inOutMax), chan); 374 | 375 | // interpolated output, based on tabread4_tilde_perform() in 376 | // https://github.com/pure-data/pure-data/blob/master/src/d_array.c 377 | //TODO: adjust symmetry of surrounding indices (based on range polarity)? 378 | int i = i_step; 379 | int ia, ib, ic, id; 380 | switch(boundaryMode) { 381 | case INTERP_CONSTANT: 382 | { 383 | ia = clamp(i - 1, 0, size - 1); 384 | ib = clamp(i + 0, 0, size - 1); 385 | ic = clamp(i + 1, 0, size - 1); 386 | id = clamp(i + 2, 0, size - 1); 387 | break; 388 | } 389 | case INTERP_MIRROR: 390 | { 391 | ia = i < 1 ? 1 : i - 1; 392 | ib = i; 393 | ic = i + 1 < size ? i + 1 : size - 1; 394 | id = i + 2 < size ? i + 2 : 2*size - (i + 3); 395 | break; 396 | } 397 | case INTERP_PERIODIC: 398 | default: 399 | { 400 | ia = (i - 1 + size) % size; 401 | ib = (i + 0) % size; 402 | ic = (i + 1) % size; 403 | id = (i + 2) % size; 404 | break; 405 | } 406 | } 407 | float a = buffer[ia]; 408 | float b = buffer[ib]; 409 | float c = buffer[ic]; 410 | float d = buffer[id]; 411 | 412 | // Pd algorithm magic 413 | float frac = phase * size - i; // fractional part of phase 414 | float y = b + frac * ( 415 | c - b - 0.1666667f * (1.f - frac) * ( 416 | (d - a - 3.f * (c - b)) * frac + (d + 2.f * a - 3.f * b) 417 | ) 418 | ); 419 | outputs[INTERP_OUTPUT].setVoltage(rescale(y, 0.f, 1.f, inOutMin, inOutMax), chan); 420 | } 421 | 422 | } 423 | 424 | struct ArrayDisplay : OpaqueWidget { 425 | Array *module; 426 | Vec dragPosition; 427 | bool dragging = false; 428 | 429 | ArrayDisplay(Array *module): OpaqueWidget() { 430 | this->module = module; 431 | box.size = Vec(230, 205); 432 | } 433 | 434 | void draw(const DrawArgs &args) override { 435 | OpaqueWidget::draw(args); 436 | const auto vg = args.vg; 437 | 438 | if(module) { 439 | 440 | // draw the array contents 441 | int s = module->buffer.size(); 442 | float w = box.size.x * 1.f / s; 443 | nvgBeginPath(vg); 444 | if(s < box.size.x) { 445 | for(int i = 0; i < s; i++) { 446 | float x1 = i * w; 447 | float x2 = (i + 1) * w; 448 | float y = (1.f - module->buffer[i]) * box.size.y; 449 | 450 | if(i == 0) nvgMoveTo(vg, x1, y); 451 | else nvgLineTo(vg, x1, y); 452 | 453 | nvgLineTo(vg, x2, y); 454 | } 455 | } else { 456 | for(int i = 0; i < box.size.x; i++) { 457 | //int i1 = clamp(int(rescale(i, 0, box.size.x - 1, 0, s - 1)), 0, s - 1); 458 | // just use the left edge (should really use average over i1..i2 instead... 459 | int ii = clamp(int(rescale(i, 0, box.size.x - 1, 0, s - 1)), 0, s - 1); 460 | float y = (1.f - module->buffer[ii]) * box.size.y; 461 | if(i == 0) nvgMoveTo(vg, 0, y); 462 | else nvgLineTo(vg, i, y); 463 | } 464 | } 465 | nvgStrokeWidth(vg, 2.f); 466 | nvgStrokeColor(vg, nvgRGB(0x0, 0x0, 0x0)); 467 | nvgStroke(vg); 468 | 469 | } 470 | 471 | nvgBeginPath(vg); 472 | nvgStrokeColor(vg, nvgRGB(0x0, 0x0, 0x0)); 473 | nvgStrokeWidth(vg, 2.f); 474 | nvgRect(vg, 0, 0, box.size.x, box.size.y); 475 | nvgStroke(vg); 476 | 477 | } 478 | 479 | void drawLayer(const DrawArgs &args, int layer) override { 480 | const auto vg = args.vg; 481 | 482 | if(layer == 1) { 483 | // draw playback & recording position 484 | if(module) { 485 | 486 | // draw playback position 487 | int nc = module->nChannels; 488 | int alpha = int(0xff * rescale(1.0f/nc, 0.f, 1.f, 0.5f, 1.0f)); 489 | for(int c = 0; c < nc; c++) { 490 | // Offset by the thickness of the box border so we don't draw over it. Same for y. 491 | // (could/should also use nvgScissor, but this is ok) 492 | float px = module->phases[c] * (box.size.x - 4) + 2; 493 | nvgBeginPath(vg); 494 | nvgStrokeWidth(vg, 2.f); 495 | nvgStrokeColor(vg, nvgRGBA(0x26, 0x8b, 0xd2, alpha)); 496 | nvgMoveTo(vg, px, 1); 497 | nvgLineTo(vg, px, box.size.y - 1); 498 | nvgStroke(vg); 499 | } 500 | 501 | // draw recording position 502 | if(module->inputs[Array::REC_PHASE_INPUT].isConnected()) { 503 | float rpx = module->recPhase * (box.size.x - 4) + 2; 504 | nvgBeginPath(vg); 505 | nvgStrokeWidth(vg, 2.f); 506 | nvgStrokeColor(vg, nvgRGB(0xdc, 0x32, 0x2f)); 507 | nvgMoveTo(vg, rpx, 1); 508 | nvgLineTo(vg, rpx, box.size.y - 1); 509 | nvgStroke(vg); 510 | } 511 | } 512 | } 513 | OpaqueWidget::drawLayer(args, layer); 514 | } 515 | 516 | 517 | void onButton(const event::Button &e) override { 518 | bool ctrl = (APP->window->getMods() & RACK_MOD_MASK) == RACK_MOD_CTRL; 519 | if(e.button == GLFW_MOUSE_BUTTON_LEFT && e.action == GLFW_PRESS 520 | && module->enableEditing && !ctrl) { 521 | e.consume(this); 522 | dragPosition = e.pos; 523 | } 524 | } 525 | 526 | void onDragStart(const event::DragStart &e) override { 527 | OpaqueWidget::onDragStart(e); 528 | dragging = true; 529 | } 530 | 531 | void onDragEnd(const event::DragEnd &e) override { 532 | OpaqueWidget::onDragEnd(e); 533 | dragging = false; 534 | } 535 | 536 | void onDragMove(const event::DragMove &e) override { 537 | OpaqueWidget::onDragMove(e); 538 | if(!module->enableEditing) return; 539 | Vec dragPosition_old = dragPosition; 540 | float zoom = getAbsoluteZoom(); 541 | dragPosition = dragPosition.plus(e.mouseDelta.div(zoom)); // take zoom into account 542 | 543 | // int() rounds down, so the upper limit of rescale is buffer.size() without -1. 544 | int s = module->buffer.size(); 545 | math::Vec bs = box.size; 546 | int i1 = clamp(int(rescale(dragPosition_old.x, 0, bs.x, 0, s)), 0, s - 1); 547 | int i2 = clamp(int(rescale(dragPosition.x, 0, bs.x, 0, s)), 0, s - 1); 548 | 549 | if(abs(i1 - i2) < 2) { 550 | float y = clamp(rescale(dragPosition.y, 0, bs.y, 1.f, 0.f), 0.f, 1.f); 551 | module->buffer[i2] = y; 552 | } else { 553 | // mouse moved more than one index, interpolate 554 | float y1 = clamp(rescale(dragPosition_old.y, 0, bs.y, 1.f, 0.f), 0.f, 1.f); 555 | float y2 = clamp(rescale(dragPosition.y, 0, bs.y, 1.f, 0.f), 0.f, 1.f); 556 | if(i2 < i1) { 557 | std::swap(i1, i2); 558 | std::swap(y1, y2); 559 | } 560 | for(int i = i1; i <= i2; i++) { 561 | float y = y1 + rescale(i, i1, i2, 0.f, 1.0f) * (y2 - y1); 562 | module->buffer[i] = y; 563 | } 564 | } 565 | } 566 | 567 | void step() override { 568 | OpaqueWidget::step(); 569 | } 570 | }; 571 | 572 | 573 | struct ArraySizeSelector : NumberTextBox { 574 | Array *module; 575 | 576 | ArraySizeSelector(Array *m) : NumberTextBox() { 577 | module = m; 578 | TextBox::text = string::f("%lu", module ? module->buffer.size() : 1); 579 | TextField::text = TextBox::text; 580 | TextBox::box.size.x = 54; 581 | textOffset = Vec(TextBox::box.size.x / 2, TextBox::box.size.y / 2); 582 | letterSpacing = -1.5f; // tighten text to fit in six characters at this width 583 | }; 584 | 585 | void onNumberSet(const int n) override { 586 | if(module) { 587 | module->resizeBuffer(n); 588 | } 589 | } 590 | 591 | }; 592 | 593 | struct ArrayResetBufferItem : MenuItem { 594 | Array *module; 595 | void onAction(const event::Action &e) override { 596 | module->initBuffer(); 597 | } 598 | }; 599 | 600 | struct ArraySetBufferToZeroItem : MenuItem { 601 | Array *module; 602 | void onAction(const event::Action &e) override { 603 | auto& buf = module->buffer; 604 | std::fill(buf.begin(), buf.end(), module->getZeroValue()); 605 | } 606 | }; 607 | 608 | struct ArraySortBufferItem : MenuItem { 609 | Array *module; 610 | void onAction(const event::Action &e) override { 611 | std::sort(module->buffer.begin(), module->buffer.end()); 612 | } 613 | }; 614 | 615 | struct ArrayAddFadesMenuItem : MenuItem { 616 | Array *module; 617 | ArrayAddFadesMenuItem(Array *pModule) { 618 | module = pModule; 619 | rightText = string::f("%lu samples", module->numFadeSamples()); 620 | } 621 | 622 | void onAction(const event::Action &e) override { 623 | size_t nFade = module->numFadeSamples(); 624 | auto& buf = module->buffer; 625 | size_t bufSize = buf.size(); 626 | float zero = module->getZeroValue(); 627 | if(nFade > 1) { 628 | for(unsigned int i = 0; i < nFade; i++) { 629 | float fac = i * 1.f / (nFade - 1); 630 | buf[i] = crossfade(zero, buf[i], fac); 631 | buf[bufSize - 1 - i] = crossfade(zero, buf[bufSize - 1 - i], fac); 632 | } 633 | } 634 | } 635 | }; 636 | 637 | // file selection dialog, based on PLAYERItem in cf 638 | // https://github.com/cfoulc/cf/blob/master/src/PLAYER.cpp 639 | struct ArrayFileSelectItem : MenuItem { 640 | Array *module; 641 | bool resizeBuffer; 642 | 643 | void onAction(const event::Action &e) override { 644 | std::string dir = module->lastLoadedPath.empty() ? asset::user("") : rack::system::getDirectory(module->lastLoadedPath); 645 | osdialog_filters* filters = osdialog_filters_parse(".wav files:wav"); 646 | char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters); 647 | if(path) { 648 | module->loadSample(path, resizeBuffer); 649 | module->lastLoadedPath = path; 650 | module->enableEditing = false; // disable editing for loaded wav files 651 | free(path); 652 | } 653 | osdialog_filters_free(filters); 654 | } 655 | }; 656 | 657 | struct ArrayEnableEditingMenuItem : MenuItem { 658 | Array *module; 659 | bool valueToSet; 660 | void onAction(const event::Action &e) override { 661 | module->enableEditing = valueToSet; 662 | } 663 | }; 664 | 665 | // Generic child menu item for selecting one of many enum values 666 | // E is the enum, memberToSet is a pointer to the member variable which is a variant of said enum 667 | template 668 | struct ArrayEnumSettingChildMenuItem : MenuItem { 669 | Array *module; 670 | E mode; 671 | E *memberToSet; 672 | ArrayEnumSettingChildMenuItem(Array *pModule, 673 | E pMode, 674 | std::string label, 675 | E *pMemberToSet): 676 | MenuItem() { 677 | module = pModule; 678 | mode = pMode; 679 | text = label; 680 | memberToSet = pMemberToSet; 681 | rightText = CHECKMARK(*memberToSet == mode); 682 | } 683 | void onAction(const event::Action &e) override { 684 | *memberToSet = mode; 685 | } 686 | }; 687 | 688 | struct ArrayRecordingModeMenuItem : MenuItemWithRightArrow { 689 | Array *module; 690 | Menu* createChildMenu() override { 691 | Menu *menu = new Menu(); 692 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::GATE, "Gate", &module->recMode)); 693 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::TOGGLE, "Toggle", &module->recMode)); 694 | 695 | return menu; 696 | } 697 | }; 698 | 699 | struct ArrayDataSaveModeMenuItem : MenuItemWithRightArrow { 700 | Array *module; 701 | Menu* createChildMenu() override { 702 | Menu* menu = new Menu(); 703 | 704 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::SAVE_FULL_DATA, "Save full array data to patch file", &module->saveMode)); 705 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::SAVE_PATH_TO_SAMPLE, "Save path to loaded sample", &module->saveMode)); 706 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::DONT_SAVE_DATA, "Don't save array data", &module->saveMode)); 707 | 708 | return menu; 709 | } 710 | }; 711 | 712 | struct ArrayInterpModeMenuItem : MenuItemWithRightArrow { 713 | Array* module; 714 | Menu* createChildMenu() override { 715 | Menu* menu = new Menu(); 716 | 717 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::INTERP_CONSTANT, "Constant", &module->boundaryMode)); 718 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::INTERP_MIRROR, "Mirror", &module->boundaryMode)); 719 | menu->addChild(new ArrayEnumSettingChildMenuItem(this->module, Array::INTERP_PERIODIC, "Periodic", &module->boundaryMode)); 720 | 721 | return menu; 722 | } 723 | }; 724 | 725 | struct ArrayModuleWidget : ModuleWidget { 726 | ArrayDisplay *display; 727 | ArraySizeSelector *sizeSelector; 728 | Array *module; 729 | 730 | ArrayModuleWidget(Array *module) { 731 | setModule(module); 732 | this->module = module; 733 | 734 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Array.svg"))); 735 | 736 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 737 | //addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); 738 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 739 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 740 | 741 | //TPort *createInputCentered(Vec pos, Module *module, int inputId) { 742 | addInput(createInputCentered(Vec(27.5f, 247.5f), module, Array::PHASE_INPUT)); 743 | addOutput(createOutputCentered(Vec(97.5f, 247.5f), module, Array::STEP_OUTPUT)); 744 | addOutput(createOutputCentered(Vec(175.f, 247.5f), module, Array::INTERP_OUTPUT)); 745 | 746 | addInput(createInputCentered(Vec(27.5f, 347.5f), module, Array::REC_PHASE_INPUT)); 747 | addInput(createInputCentered(Vec(97.5f, 347.5f), module, Array::REC_SIGNAL_INPUT)); 748 | addInput(createInputCentered(Vec(175.f, 347.5f), module, Array::REC_ENABLE_INPUT)); 749 | 750 | addParam(createParam(Vec(107.5f, 290), module, Array::OUTPUT_RANGE_PARAM)); 751 | addParam(createParam(Vec(27.5f, 290), module, Array::PHASE_RANGE_PARAM)); 752 | 753 | addParam(createLightParamCentered>>(Vec(207.5f, 347.5f), module, Array::REC_ENABLE_PARAM, Array::REC_LIGHT)); 754 | 755 | display = new ArrayDisplay(module); 756 | display->box.pos = Vec(5, 20); 757 | addChild(display); 758 | 759 | sizeSelector = new ArraySizeSelector(module); 760 | sizeSelector->TextBox::box.pos = Vec(174, 295); 761 | addChild(static_cast(sizeSelector)); 762 | } 763 | 764 | void appendContextMenu(ui::Menu *menu) override { 765 | 766 | Array *arr = dynamic_cast(module); 767 | if(arr){ 768 | menu->addChild(new MenuLabel()); // spacer 769 | 770 | auto *bufResetItem = new ArrayResetBufferItem(); 771 | bufResetItem->text = "Reset array contents"; 772 | bufResetItem->module = arr; 773 | menu->addChild(bufResetItem); 774 | 775 | auto *bufSetToZeroItem = new ArraySetBufferToZeroItem(); 776 | bufSetToZeroItem->text = "Set array contents to zero"; 777 | bufSetToZeroItem->module = arr; 778 | menu->addChild(bufSetToZeroItem); 779 | 780 | auto *bufSortItem = new ArraySortBufferItem(); 781 | bufSortItem->text = "Sort array contents"; 782 | bufSortItem->module = arr; 783 | menu->addChild(bufSortItem); 784 | 785 | auto *addFadesItem = new ArrayAddFadesMenuItem(arr); 786 | addFadesItem->text = "Add fade in/out to prevent clicks"; 787 | menu->addChild(addFadesItem); 788 | 789 | auto *edItem = new ArrayEnableEditingMenuItem(); 790 | edItem->text = "Disable drawing"; 791 | edItem->module = arr; 792 | edItem->rightText = CHECKMARK(!arr->enableEditing); 793 | edItem->valueToSet = !arr->enableEditing; 794 | menu->addChild(edItem); 795 | 796 | auto *rmItem = new ArrayRecordingModeMenuItem(); 797 | rmItem->text = "Recording mode"; 798 | rmItem->module = this->module; 799 | menu->addChild(rmItem); 800 | 801 | { 802 | auto *fsItem = new ArrayFileSelectItem(); 803 | float duration = arr->buffer.size() * 1.f / arr->sampleRate; 804 | fsItem->resizeBuffer = false; 805 | fsItem->text = "Load .wav file..."; 806 | fsItem->rightText = string::f("(%.2f s)", duration); 807 | fsItem->module = arr; 808 | menu->addChild(fsItem); 809 | } 810 | 811 | { 812 | auto *fsItem = new ArrayFileSelectItem(); 813 | fsItem->resizeBuffer = true; 814 | fsItem->text = "Load .wav file and resize array..."; 815 | fsItem->module = arr; 816 | menu->addChild(fsItem); 817 | } 818 | 819 | auto *saveModeSubMenu = new ArrayDataSaveModeMenuItem(); 820 | saveModeSubMenu->text = "Data persistence"; 821 | saveModeSubMenu->module = this->module; 822 | menu->addChild(saveModeSubMenu); 823 | 824 | auto *interpModeSubMenu = new ArrayInterpModeMenuItem(); 825 | interpModeSubMenu->text = "Interpolation at boundary"; 826 | interpModeSubMenu->module = this->module; 827 | menu->addChild(interpModeSubMenu); 828 | } 829 | 830 | } 831 | 832 | }; 833 | 834 | 835 | Model *modelArray = createModel("Array"); 836 | -------------------------------------------------------------------------------- /src/Miniramp.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | #include "Widgets.hpp" 3 | #include "Util.hpp" 4 | 5 | #include // std::replace 6 | 7 | const float MIN_EXPONENT = -3.0f; 8 | const float MAX_EXPONENT = 1.0f; 9 | 10 | // quick and dirty copy-paste of LittleUtils PulseGenerator. 11 | // TODO: move modules that are shared with LittleUtils common folder 12 | // TODO: show sample count corresponding to duration on mouse over (see ParamWidget::onEnter) 13 | // TODO: add trigger buttton (another reason to remove cv) 14 | 15 | // based on PulseGeneraotr in include/util/digital.hpp 16 | struct CustomPulseGenerator { 17 | float time; 18 | float triggerDuration; 19 | bool finished; // the output is the inverse of this 20 | 21 | CustomPulseGenerator() { 22 | reset(); 23 | } 24 | /** Immediately resets the state to LOW */ 25 | void reset() { 26 | time = 0.f; 27 | triggerDuration = 0.f; 28 | finished = true; 29 | } 30 | /** Advances the state by `deltaTime`. Returns whether the pulse is in the HIGH state. */ 31 | bool process(float deltaTime) { 32 | time += deltaTime; 33 | if(!finished) finished = time >= triggerDuration; 34 | return !finished; 35 | } 36 | /** Begins a trigger with the given `triggerDuration`. */ 37 | void trigger(float triggerDuration) { 38 | // retrigger even with a shorter duration 39 | time = 0.f; 40 | finished = false; 41 | this->triggerDuration = triggerDuration; 42 | } 43 | }; 44 | 45 | struct Miniramp : Module { 46 | enum ParamIds { 47 | RAMP_LENGTH_PARAM, 48 | CV_AMT_PARAM, 49 | LIN_LOG_MODE_PARAM, 50 | NUM_PARAMS 51 | }; 52 | enum InputIds { 53 | TRIG_INPUT, 54 | RAMP_LENGTH_INPUT, 55 | STOP_INPUT, 56 | NUM_INPUTS 57 | }; 58 | enum OutputIds { 59 | RAMP_OUTPUT, 60 | GATE_OUTPUT, 61 | EOC_OUTPUT, 62 | FINISH_OUTPUT, 63 | NUM_OUTPUTS 64 | }; 65 | enum LightIds { 66 | RAMP_LIGHT, 67 | GATE_LIGHT, 68 | EOC_LIGHT, 69 | FINISH_LIGHT, 70 | NUM_LIGHTS 71 | }; 72 | enum RampFinishedMode { 73 | RAMP_FINISHED_0, 74 | RAMP_FINISHED_10, 75 | NUM_RAMP_FINISHED_MODES 76 | }; 77 | 78 | dsp::SchmittTrigger inputTrigger[MAX_POLY_CHANNELS]; 79 | dsp::SchmittTrigger stopTrigger[MAX_POLY_CHANNELS]; 80 | // Main pulse generator. The duration of these determinse the ramp duration. 81 | CustomPulseGenerator gateGen[MAX_POLY_CHANNELS]; 82 | CustomPulseGenerator eocGen[MAX_POLY_CHANNELS]; 83 | bool sendEOConStop = false; // if the stop port is triggered, should we send an EOC? 84 | bool updateDurationOnlyOnTrigger = false; 85 | RampFinishedMode rampFinishedMode = RAMP_FINISHED_0; 86 | 87 | Miniramp() { 88 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 89 | 90 | struct RampLengthParamQuantity : ParamQuantity { 91 | float getDisplayValue() override { 92 | auto *module = reinterpret_cast(this->module); 93 | return module->get_ramp_base_duration(); 94 | } 95 | }; 96 | 97 | struct RampLengthCVParamQuantity : ParamQuantity { 98 | float getDisplayValue() override { 99 | auto *module = reinterpret_cast(this->module); 100 | // have to multiply by 10 to get seconds (CV scale is seconds per 10V) 101 | return module->get_cv_scale() * 10; 102 | } 103 | }; 104 | 105 | 106 | configParam( 107 | Miniramp::RAMP_LENGTH_PARAM, 108 | 0.f, 10.f, // minvalue, maxvalue 109 | 5.f, // default value, 0.1s in log mode, 5s in lin mode 110 | "Ramp duration without CV", // name 111 | " s" // unit 112 | ); 113 | 114 | configParam( 115 | Miniramp::CV_AMT_PARAM, 116 | -1.f, 1.f, 117 | 0.f, 118 | "Ramp duration CV mod amount", 119 | " s" 120 | ); 121 | 122 | configSwitch(Miniramp::LIN_LOG_MODE_PARAM, 0.f, 1.f, 1.f, "Ramp duration adjust mode", { "Linear", "Logarithmic" }); 123 | configInput(TRIG_INPUT, "Trigger"); 124 | configInput(RAMP_LENGTH_INPUT, "Ramp duration CV modulation"); 125 | configInput(STOP_INPUT, "Stop ramp"); 126 | configOutput(RAMP_OUTPUT, "Ramp"); 127 | configOutput(GATE_OUTPUT, "Gate"); 128 | configOutput(EOC_OUTPUT, "End of cycle"); 129 | configOutput(FINISH_OUTPUT, "Ramp finished"); 130 | 131 | for(int c = 0; c < MAX_POLY_CHANNELS; c++) { 132 | gateGen[c].reset(); 133 | eocGen[c].reset(); 134 | } 135 | } 136 | 137 | json_t *dataToJson() override { 138 | json_t *root = json_object(); 139 | json_object_set_new(root, "rampFinishedMode", json_integer(rampFinishedMode)); 140 | json_object_set_new(root, "sendEOConStop", json_boolean(sendEOConStop)); 141 | json_object_set_new(root, "updateDurationOnlyOnTrigger", json_boolean(updateDurationOnlyOnTrigger)); 142 | 143 | return root; 144 | } 145 | 146 | void dataFromJson(json_t *root) override { 147 | json_t *rampFinished_J = json_object_get(root, "rampFinishedMode"); 148 | if(rampFinished_J) { 149 | int rfm = int(json_integer_value(rampFinished_J)); 150 | if(rfm < NUM_RAMP_FINISHED_MODES) { 151 | rampFinishedMode = static_cast(rfm); 152 | } 153 | } 154 | 155 | json_t *sendEOConStop_J = json_object_get(root, "sendEOConStop"); 156 | if(sendEOConStop_J) { 157 | sendEOConStop = json_boolean_value(sendEOConStop_J); 158 | } 159 | 160 | json_t *updateDurationOnlyOnTrigger_J = json_object_get(root, "updateDurationOnlyOnTrigger"); 161 | if(updateDurationOnlyOnTrigger_J) { 162 | updateDurationOnlyOnTrigger = json_boolean_value(updateDurationOnlyOnTrigger_J); 163 | } 164 | 165 | // Set initial ramp durations, otherwise they are not updated if 166 | // updateDurationOnlyOnTrigger is enabled, and the display looks wrong. 167 | float ramp_duration = get_ramp_base_duration(); 168 | for(int c = 0; c < MAX_POLY_CHANNELS; c++) { 169 | gateGen[c].triggerDuration = ramp_duration; 170 | } 171 | } 172 | 173 | // Calculate the ramp base duration from the value of the main knob and 174 | // based on the lin/log mode switch. 175 | float get_ramp_base_duration() { 176 | float knob_value = params[RAMP_LENGTH_PARAM].getValue(); 177 | if(params[LIN_LOG_MODE_PARAM].getValue() < 0.5f) { 178 | // linear mode 179 | return knob_value; 180 | } else { 181 | // logarithmic mode 182 | float exponent = rescale(knob_value, 183 | 0.f, 10.f, MIN_EXPONENT, MAX_EXPONENT); 184 | return powf(10.0f, exponent); 185 | } 186 | } 187 | 188 | // Calculate the CV modulation scale factor (in units of 1/(10V)) 189 | float get_cv_scale() { 190 | float cv_amt = params[CV_AMT_PARAM].getValue(); 191 | if(params[LIN_LOG_MODE_PARAM].getValue() < 0.5f) { 192 | // linear mode 193 | return cv_amt; 194 | } else { 195 | // logarithmic mode 196 | float exponent = rescale(fabs(cv_amt), 0.f, 1.f, 197 | MIN_EXPONENT, MAX_EXPONENT); 198 | 199 | // decrease exponent by one so that 10V maps to 1.0 (100%) CV. 200 | return powf(10.0f, exponent - 1.f) * signum(cv_amt); // take sign into account 201 | } 202 | } 203 | 204 | void process(const ProcessArgs &args) override; 205 | 206 | }; 207 | 208 | void Miniramp::process(const ProcessArgs &args) { 209 | float deltaTime = args.sampleTime; 210 | const int channels = std::max( 211 | inputs[TRIG_INPUT].getChannels(), 212 | inputs[CV_AMT_PARAM].getChannels() 213 | ); 214 | 215 | // handle duration knob and CV 216 | float ramp_base_duration = get_ramp_base_duration(); 217 | float cv_scale = get_cv_scale(); 218 | 219 | for(int c = 0; c < std::max(channels, 1); c++) { 220 | float cv_voltage = inputs[RAMP_LENGTH_INPUT].getPolyVoltage(c); 221 | float ramp_duration = clamp(ramp_base_duration + cv_voltage * cv_scale, 0.f, 10.f); 222 | 223 | bool triggered = inputTrigger[c].process(rescale( 224 | inputs[TRIG_INPUT].getPolyVoltage(c), 0.1f, 2.f, 0.f, 1.f)); 225 | 226 | bool eoc_triggered = false; 227 | 228 | if (stopTrigger[c].process(rescale( 229 | inputs[STOP_INPUT].getPolyVoltage(c), 230 | 0.1f, 2.0f, 231 | 0.0f, 1.0f 232 | ))) { 233 | // reset everything 234 | gateGen[c].reset(); 235 | eocGen[c].reset(); 236 | eoc_triggered = sendEOConStop; 237 | } else if(triggered) { 238 | gateGen[c].trigger(ramp_duration); 239 | } 240 | 241 | // update trigger duration even in the middle of a trigger if applicable 242 | if(!updateDurationOnlyOnTrigger) { 243 | gateGen[c].triggerDuration = ramp_duration; 244 | } 245 | 246 | bool gate_prev = !gateGen[c].finished; 247 | bool gate = gateGen[c].process(deltaTime); 248 | eoc_triggered |= gate_prev && !gate; // gate was finished, start EOC 249 | 250 | if(eoc_triggered) { 251 | eocGen[c].trigger(1e-3f); 252 | } 253 | 254 | bool eoc_pulse = eocGen[c].process(deltaTime); 255 | 256 | float ramp_v; 257 | if(gate) { 258 | ramp_v = clamp(gateGen[c].time/gateGen[c].triggerDuration * 10.f, 0.f, 10.f); 259 | } else { 260 | if(rampFinishedMode == RAMP_FINISHED_0) { 261 | ramp_v = 0; 262 | } else { 263 | ramp_v = 10; 264 | } 265 | } 266 | 267 | outputs[RAMP_OUTPUT].setVoltage(ramp_v, c); 268 | outputs[GATE_OUTPUT].setVoltage(gate ? 10.0f : 0.0f, c); 269 | outputs[EOC_OUTPUT].setVoltage(eoc_pulse ? 10.0f : 0.0f, c); 270 | outputs[FINISH_OUTPUT].setVoltage(gate ? 0.0f : 10.0f, c); 271 | 272 | //TODO: figure out lights for polyphonic mode 273 | lights[RAMP_LIGHT].setSmoothBrightness(outputs[RAMP_OUTPUT].value * 1e-1f, deltaTime); 274 | lights[GATE_LIGHT].setSmoothBrightness(outputs[GATE_OUTPUT].value, deltaTime); 275 | lights[EOC_LIGHT].setSmoothBrightness(outputs[EOC_OUTPUT].value, deltaTime); 276 | lights[FINISH_LIGHT].setSmoothBrightness(outputs[FINISH_OUTPUT].value, deltaTime); 277 | 278 | } 279 | outputs[RAMP_OUTPUT].setChannels(channels); 280 | outputs[GATE_OUTPUT].setChannels(channels); 281 | outputs[EOC_OUTPUT].setChannels(channels); 282 | outputs[FINISH_OUTPUT].setChannels(channels); 283 | } 284 | 285 | struct MsDisplayWidget : TextBox { 286 | Miniramp *module; 287 | bool cvLabelStatus = false; // whether to show 'cv' 288 | float previous_displayed_value = 0.f; 289 | float cvDisplayTime = 2.f; 290 | 291 | GUITimer cvDisplayTimer; 292 | 293 | MsDisplayWidget(Miniramp *m) : TextBox() { 294 | module = m; 295 | box.size = Vec(65, 20); 296 | letterSpacing = -1.5f; 297 | textAlign = NVG_ALIGN_LEFT | NVG_ALIGN_TOP; 298 | textOffset.x = 3; 299 | fontSize = 19; 300 | updateDisplayValue(1.23456f); 301 | } 302 | 303 | void updateDisplayValue(float v) { 304 | // only update/do stringf if value is changed 305 | if(v != previous_displayed_value) { 306 | std::string s; 307 | previous_displayed_value = v; 308 | s = string::f("%#.4f", v); 309 | // hacky way to make monospace fonts prettier 310 | std::replace(s.begin(), s.end(), '0', 'O'); 311 | // if the displayed value is 10.0, we will have too many decimal digits, truncate 312 | s = s.substr(0, 6); 313 | setText(s); 314 | } 315 | } 316 | 317 | void draw(const DrawArgs &args) override { 318 | TextBox::draw(args); 319 | const auto vg = args.vg; 320 | nvgScissor(vg, 0, 0, box.size.x, box.size.y); 321 | 322 | std::shared_ptr font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/RobotoMono-Bold.ttf")); 323 | if(font) { 324 | nvgFillColor(vg, textColor); 325 | nvgFontFaceId(vg, font->handle); 326 | 327 | nvgFontSize(vg, 14); 328 | nvgTextLetterSpacing(vg, 0.f); 329 | nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM); 330 | nvgText(vg, box.size.x - 5, box.size.y - 1.5, "s", NULL); 331 | 332 | if(cvLabelStatus) { 333 | nvgFontSize(vg, 11); 334 | nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP); 335 | nvgText(vg, box.size.x - 3, 0, "cv", NULL); 336 | } 337 | } 338 | 339 | nvgResetScissor(vg); 340 | } 341 | 342 | void triggerCVDisplay() { 343 | cvDisplayTimer.trigger(cvDisplayTime); 344 | } 345 | 346 | void step() override { 347 | TextBox::step(); 348 | cvLabelStatus = cvDisplayTimer.process(); 349 | if(module) { 350 | //TODO: don't show cv-modulated value if turning main knob 351 | updateDisplayValue(cvLabelStatus ? fabs(module->get_cv_scale()) * 10.f : module->gateGen[0].triggerDuration); 352 | } 353 | } 354 | 355 | }; 356 | 357 | struct CustomTrimpot : Trimpot { 358 | MsDisplayWidget *display; 359 | CustomTrimpot(): Trimpot() {}; 360 | 361 | void onDragMove(const event::DragMove &e) override { 362 | Trimpot::onDragMove(e); 363 | display->triggerCVDisplay(); 364 | } 365 | }; 366 | 367 | template 368 | struct MinirampEnumChildMenuItem : MenuItem { 369 | Miniramp *module; 370 | // Miniramp::RampFinishedMode 371 | T mode; 372 | // Miniramp->rampFinishedMode 373 | T *modeParam; 374 | 375 | MinirampEnumChildMenuItem( 376 | Miniramp *m, 377 | T pMode, 378 | T *pModeParam, 379 | std::string label 380 | ) : MenuItem() { 381 | module = m; 382 | mode = pMode; 383 | modeParam = pModeParam; 384 | text = label; 385 | rightText = CHECKMARK(*modeParam == mode); 386 | } 387 | 388 | void onAction(const event::Action &e) override { 389 | *modeParam = mode; 390 | } 391 | }; 392 | 393 | 394 | struct MinirampFinishedModeChildMenuItem : MinirampEnumChildMenuItem { 395 | MinirampFinishedModeChildMenuItem( 396 | Miniramp *m, 397 | Miniramp::RampFinishedMode pMode, 398 | std::string label 399 | ) : MinirampEnumChildMenuItem(m, pMode, &m->rampFinishedMode, label) {}; 400 | }; 401 | 402 | struct MinirampFinishedModeMenuItem : MenuItemWithRightArrow { 403 | Miniramp *module; 404 | Menu *createChildMenu() override { 405 | Menu *menu = new Menu(); 406 | menu->addChild(new MinirampFinishedModeChildMenuItem( 407 | module, 408 | Miniramp::RAMP_FINISHED_0, 409 | "0V" 410 | )); 411 | menu->addChild(new MinirampFinishedModeChildMenuItem( 412 | module, 413 | Miniramp::RAMP_FINISHED_10, 414 | "10V" 415 | )); 416 | return menu; 417 | } 418 | }; 419 | 420 | /* 421 | * Wrap a boolean parameter and toggle it when this menu item is clicked. 422 | */ 423 | struct BoolToggleMenuItem : MenuItem { 424 | bool *boolParam; 425 | 426 | BoolToggleMenuItem ( 427 | std::string label, 428 | bool *pBoolParam 429 | ) : MenuItem() { 430 | text = label; 431 | boolParam = pBoolParam; 432 | rightText = CHECKMARK(*pBoolParam); 433 | } 434 | void onAction(const event::Action &e) override { 435 | *boolParam = !*boolParam; 436 | } 437 | }; 438 | 439 | struct MinirampWidget : ModuleWidget { 440 | Miniramp *module; 441 | MsDisplayWidget *msDisplay; 442 | 443 | MinirampWidget(Miniramp *module) { 444 | setModule(module); 445 | this->module = module; 446 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Miniramp.svg"))); 447 | 448 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 449 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 3, 0))); 450 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 451 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 3, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 452 | 453 | addParam(createParamCentered(Vec(37.5, 40), module, 454 | Miniramp::RAMP_LENGTH_PARAM)); 455 | 456 | addParam(createParam(Vec(20, 105), module, Miniramp::LIN_LOG_MODE_PARAM)); 457 | 458 | addInput(createInputCentered(Vec(20, 157), module, Miniramp::RAMP_LENGTH_INPUT)); 459 | addInput(createInputCentered(Vec(20, 202), module, Miniramp::TRIG_INPUT)); 460 | addInput(createInputCentered(Vec(55, 202), module, Miniramp::STOP_INPUT)); 461 | addOutput(createOutputCentered(Vec(20, 250), module, Miniramp::RAMP_OUTPUT)); 462 | addOutput(createOutputCentered(Vec(55, 250), module, Miniramp::GATE_OUTPUT)); 463 | addOutput(createOutputCentered(Vec(20, 298), module, Miniramp::EOC_OUTPUT)); 464 | addOutput(createOutputCentered(Vec(55, 298), module, Miniramp::FINISH_OUTPUT)); 465 | 466 | addChild(createTinyLightForPort(Vec(20, 250), module, Miniramp::RAMP_LIGHT)); 467 | addChild(createTinyLightForPort(Vec(55, 250), module, Miniramp::GATE_LIGHT)); 468 | addChild(createTinyLightForPort(Vec(20, 298), module, Miniramp::EOC_LIGHT)); 469 | addChild(createTinyLightForPort(Vec(55, 298), module, Miniramp::FINISH_LIGHT)); 470 | 471 | msDisplay = new MsDisplayWidget(module); 472 | msDisplay->box.pos = Vec(5, 318); 473 | addChild(msDisplay); 474 | 475 | auto cvKnob = createParamCentered(Vec(55, 157), module, 476 | Miniramp::CV_AMT_PARAM); 477 | cvKnob->display = msDisplay; 478 | addParam(cvKnob); 479 | 480 | } 481 | 482 | void appendContextMenu(ui::Menu *menu) override { 483 | if(module) { 484 | auto *finishModeMenuItem = new MinirampFinishedModeMenuItem(); 485 | finishModeMenuItem->text = "Ramp value when finished"; 486 | finishModeMenuItem->module = module; 487 | menu->addChild(finishModeMenuItem); 488 | 489 | menu->addChild(new BoolToggleMenuItem( 490 | "Send EOC on STOP", 491 | &module->sendEOConStop 492 | )); 493 | 494 | menu->addChild(new BoolToggleMenuItem( 495 | "Update duration only on trigger", 496 | &module->updateDurationOnlyOnTrigger 497 | )); 498 | 499 | } 500 | } 501 | 502 | }; 503 | 504 | 505 | Model *modelMiniramp = createModel("Miniramp"); 506 | -------------------------------------------------------------------------------- /src/Ministep.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | 3 | #include "Widgets.hpp" 4 | #include "Util.hpp" 5 | 6 | #include // std::replace 7 | 8 | constexpr int DEFAULT_NSTEPS = 10; 9 | 10 | struct Ministep : Module { 11 | enum ParamIds { 12 | NUM_PARAMS 13 | }; 14 | enum InpuIds { 15 | RESET_INPUT, 16 | INCREMENT_INPUT, 17 | DECREMENT_INPUT, 18 | SCALE_INPUT, 19 | NUM_INPUTS 20 | }; 21 | enum OutputIds { 22 | STEP_OUTPUT, 23 | NUM_OUTPUTS 24 | }; 25 | enum LightIds { 26 | NUM_LIGHTS 27 | }; 28 | 29 | enum StepScaleMode { 30 | SCALE_ABSOLUTE, 31 | SCALE_RELATIVE 32 | }; 33 | 34 | enum OutputScaleMode { 35 | SCALE_10V_PER_NSTEPS, 36 | SCALE_1V_PER_STEP 37 | }; 38 | 39 | dsp::SchmittTrigger rstTrigger[MAX_POLY_CHANNELS]; 40 | dsp::SchmittTrigger incTrigger[MAX_POLY_CHANNELS]; 41 | dsp::SchmittTrigger decTrigger[MAX_POLY_CHANNELS]; 42 | int nSteps = DEFAULT_NSTEPS; 43 | int currentStep[MAX_POLY_CHANNELS]; 44 | int currentScale[MAX_POLY_CHANNELS]; 45 | int nChannels = 1; 46 | bool offsetByHalfStep = false; 47 | StepScaleMode stepScaleMode = SCALE_RELATIVE; 48 | OutputScaleMode outputScaleMode = SCALE_10V_PER_NSTEPS; 49 | 50 | Ministep() { 51 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 52 | 53 | configInput(RESET_INPUT, "Reset"); 54 | configInput(INCREMENT_INPUT, "Increment"); 55 | configInput(DECREMENT_INPUT, "Decrement"); 56 | configInput(SCALE_INPUT, "Increment/decrement scale"); 57 | configOutput(STEP_OUTPUT, "Step"); 58 | 59 | nSteps = DEFAULT_NSTEPS; 60 | for(int i = 0; i < MAX_POLY_CHANNELS; i++) { 61 | currentStep[i] = 0; 62 | currentScale[i] = 1; 63 | } 64 | } 65 | 66 | void onReset() override { 67 | nSteps = DEFAULT_NSTEPS; 68 | for(int i = 0; i < MAX_POLY_CHANNELS; i++) currentStep[i] = 0; 69 | } 70 | 71 | json_t *dataToJson() override { 72 | json_t *root = json_object(); 73 | 74 | json_object_set_new(root, "nSteps", json_integer(nSteps)); 75 | json_object_set_new(root, "offsetByHalfStep", json_boolean(offsetByHalfStep)); 76 | json_object_set_new(root, "stepScaleMode", json_integer(stepScaleMode)); 77 | json_object_set_new(root, "outputScaleMode", json_integer(outputScaleMode)); 78 | 79 | json_t *currentStep_J = json_array(); 80 | for(int i = 0; i < MAX_POLY_CHANNELS; i++) { 81 | json_array_append_new(currentStep_J, json_integer(currentStep[i])); 82 | } 83 | json_object_set(root, "currentStep", currentStep_J); 84 | json_decref(currentStep_J); 85 | 86 | return root; 87 | } 88 | 89 | void dataFromJson(json_t *root) override { 90 | json_t *nSteps_J = json_object_get(root, "nSteps"); 91 | json_t *offsetByHalfStep_J = json_object_get(root, "offsetByHalfStep"); 92 | json_t *currentStep_J = json_object_get(root, "currentStep"); 93 | json_t *stepScaleMode_J = json_object_get(root, "stepScaleMode"); 94 | json_t *outputScaleMode_J = json_object_get(root, "outputScaleMode"); 95 | 96 | if(nSteps_J) { 97 | nSteps = json_integer_value(nSteps_J); 98 | if(nSteps < 1) { 99 | nSteps = DEFAULT_NSTEPS; 100 | } 101 | } 102 | 103 | if(offsetByHalfStep_J) { 104 | offsetByHalfStep = json_boolean_value(offsetByHalfStep_J); 105 | } 106 | 107 | if(stepScaleMode_J) { 108 | int sm = json_integer_value(stepScaleMode_J); 109 | stepScaleMode = static_cast(sm); 110 | } 111 | 112 | if(outputScaleMode_J) { 113 | int sm = json_integer_value(outputScaleMode_J); 114 | outputScaleMode = static_cast(sm); 115 | } 116 | 117 | if(currentStep_J) { 118 | for(int i = 0; i < MAX_POLY_CHANNELS; i++) { 119 | json_t *step = json_array_get(currentStep_J, i); 120 | if(step) { 121 | currentStep[i] = json_integer_value(step); 122 | } 123 | } 124 | } 125 | } 126 | 127 | void process(const ProcessArgs &args) override; 128 | 129 | }; 130 | 131 | void Ministep::process(const ProcessArgs &args) { 132 | nChannels = std::max( 133 | std::max( 134 | inputs[INCREMENT_INPUT].getChannels(), 135 | inputs[DECREMENT_INPUT].getChannels() 136 | ), 137 | std::max(inputs[RESET_INPUT].getChannels(), 1) 138 | ); 139 | 140 | 141 | for(int c = 0; c < nChannels; c++) { 142 | bool rstTriggered = rstTrigger[c].process( 143 | rescale(inputs[RESET_INPUT].getPolyVoltage(c), 0.1f, 2.f, 0.f, 1.f) 144 | ); 145 | bool incTriggered = incTrigger[c].process( 146 | rescale(inputs[INCREMENT_INPUT].getPolyVoltage(c), 0.1f, 2.f, 0.f, 1.f) 147 | ); 148 | bool decTriggered = decTrigger[c].process( 149 | rescale(inputs[DECREMENT_INPUT].getPolyVoltage(c), 0.1f, 2.f, 0.f, 1.f) 150 | ); 151 | 152 | if(inputs[SCALE_INPUT].isConnected()) { 153 | float s = inputs[SCALE_INPUT].getPolyVoltage(c); 154 | if(stepScaleMode == SCALE_RELATIVE) { 155 | s *= nSteps / 10.0f; 156 | } 157 | currentScale[c] = int(s); // round towards zero 158 | } else { 159 | currentScale[c] = 1; 160 | } 161 | 162 | int step = currentStep[c]; 163 | if(rstTriggered) { 164 | step = 0; 165 | } else { 166 | if(incTriggered && !decTriggered) { 167 | step += currentScale[c]; 168 | } else if (decTriggered && !incTriggered) { 169 | step -= currentScale[c]; 170 | } // if both are triggered, do nothing 171 | 172 | step = (step + nSteps) % nSteps; 173 | } 174 | currentStep[c] = step; 175 | 176 | float phase = (currentStep[c] + (offsetByHalfStep ? 0.5f : 0.0f)); 177 | float v = outputScaleMode == SCALE_10V_PER_NSTEPS ? phase * 10.f / nSteps : phase; 178 | outputs[STEP_OUTPUT].setVoltage(v, c); 179 | } 180 | outputs[STEP_OUTPUT].setChannels(nChannels); 181 | } 182 | 183 | struct PolyIntDisplayWidget : TextBox { 184 | Ministep *module; 185 | int *polyValueToDisplay; 186 | int previousDisplayValue = 1; 187 | 188 | PolyIntDisplayWidget(Ministep *m, int *valueToDisplay) : TextBox() { 189 | module = m; 190 | polyValueToDisplay = valueToDisplay; 191 | box.size = Vec(30, 21); 192 | textAlign = NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE; 193 | textOffset = Vec(box.size.x / 2 + 0.5f, box.size.y / 2); 194 | letterSpacing = -1.5f; // tighten text to fit in three characters at this width 195 | 196 | if(module) { 197 | previousDisplayValue = polyValueToDisplay[0]; 198 | } 199 | 200 | setText(string::f("%i", previousDisplayValue)); 201 | } 202 | 203 | // determine the value to display in monophonic mode 204 | virtual int getSingleValue() { return polyValueToDisplay[0]; } 205 | // this function set s the vertical position of the bar for each value in polyValueToDisplay 206 | virtual void getBarVPos(int i, float *h, float *y) { *h = 0; *y = box.size.y; } 207 | 208 | void draw(const DrawArgs &args) override { 209 | if(!module || module->nChannels == 1) { 210 | if(previousDisplayValue < -99) { 211 | fontSize = 16; 212 | } else { 213 | fontSize = 20; 214 | } 215 | TextBox::draw(args); 216 | } else { 217 | setText(""); 218 | TextBox::draw(args); 219 | 220 | const auto vg = args.vg; 221 | int n = module->nChannels; 222 | float xrange = box.size.x - 4; 223 | float w = xrange / n; 224 | for(int i = 0; i < n; i++) { 225 | float h, y; 226 | getBarVPos(i, &h, &y); 227 | float x = i * 1.f / n * xrange + 2; 228 | nvgFillColor(vg, textColor); 229 | nvgBeginPath(vg); 230 | nvgRect(vg, x, y, w, h); 231 | nvgFill(vg); 232 | } 233 | } 234 | } 235 | 236 | void step() override { 237 | TextBox::step(); 238 | if(module) { 239 | int v = getSingleValue(); 240 | if(v != previousDisplayValue) { 241 | std::string s = string::f("%i", v); 242 | std::replace(s.begin(), s.end(), '0', 'O'); 243 | setText(s); 244 | } 245 | previousDisplayValue = v; 246 | } 247 | } 248 | }; 249 | 250 | struct ScaleDisplayWidget : PolyIntDisplayWidget { 251 | int maxValue = 1; 252 | ScaleDisplayWidget(Ministep *m, int *valueToDisplay) 253 | : PolyIntDisplayWidget(m, valueToDisplay) {}; 254 | 255 | void getBarVPos(int i, float *h, float *y) override { 256 | if(i == 0) { 257 | // hacky way to avoid repeating this loop for all i 258 | maxValue = 1; 259 | for(int j = 0; j < module->nChannels; j++) { 260 | maxValue = std::max(std::abs(polyValueToDisplay[j]), maxValue); 261 | } 262 | } 263 | //return polyValueToDisplay[i] * 0.5f / maxVal + 0.5; 264 | int val = polyValueToDisplay[i]; 265 | if(val == 0) { 266 | *h = 1.0f; 267 | *y = 0.5f * box.size.y - 0.5f; 268 | } else { 269 | *h = val * 0.5f / maxValue * box.size.y; 270 | *y = 0.5f * box.size.y - *h + 0.5f * signum(1.0f * val); 271 | } 272 | } 273 | }; 274 | 275 | struct CurrentStepDisplayWidget : PolyIntDisplayWidget { 276 | CurrentStepDisplayWidget(Ministep *m, int *valueToDisplay) 277 | : PolyIntDisplayWidget(m, valueToDisplay) {}; 278 | 279 | // display step 0 as '1' 280 | int getSingleValue() override { return polyValueToDisplay[0] + 1; } 281 | 282 | void getBarVPos(int i, float *h, float *y) override { 283 | *h = (polyValueToDisplay[i] + 1) * 1.f / module->nSteps * box.size.y; 284 | *y = box.size.y - *h; 285 | } 286 | }; 287 | 288 | 289 | 290 | 291 | struct NStepsSelector : NumberTextBox { 292 | Ministep *module; 293 | 294 | NStepsSelector(Ministep *m) : NumberTextBox() { 295 | module = m; 296 | TextBox::text = string::f("%u", module ? module->nSteps : DEFAULT_NSTEPS); 297 | TextField::text = TextBox::text; 298 | maxTextLength = 3; 299 | TextBox::box.size = Vec(30, 21); 300 | textOffset = Vec(TextBox::box.size.x / 2 + 0.5f, TextBox::box.size.y / 2); 301 | letterSpacing = -1.5f; // tighten text to fit in three characters at this width 302 | }; 303 | 304 | void onNumberSet(const int n) override { 305 | if(module) { 306 | module->nSteps = n; 307 | } 308 | } 309 | 310 | }; 311 | 312 | template 313 | struct ScaleModeChildMenuItem : MenuItem { 314 | Ministep *module; 315 | //T is either Ministep::OutputScaleMode or Ministep::StepScaleMode 316 | T mode; 317 | // modeParam is either Ministep->outputScaleMode or Ministep->stepScaleMode 318 | T *modeParam; 319 | ScaleModeChildMenuItem(Ministep *m, T pMode, T *pModeParam, 320 | std::string label) : MenuItem() { 321 | module = m; 322 | mode = pMode; 323 | modeParam = pModeParam; 324 | text = label; 325 | rightText = CHECKMARK(*modeParam == mode); 326 | } 327 | 328 | void onAction(const event::Action &e) override { 329 | //module->outputScaleMode = mode; 330 | *modeParam = mode; 331 | } 332 | }; 333 | 334 | struct StepScaleModeChildMenuItem : ScaleModeChildMenuItem { 335 | StepScaleModeChildMenuItem(Ministep *m, Ministep::StepScaleMode pMode, 336 | std::string label) 337 | : ScaleModeChildMenuItem(m, pMode, &m->stepScaleMode, label) {}; 338 | }; 339 | 340 | struct StepScaleModeMenuItem : MenuItem { 341 | Ministep *module; 342 | Menu *createChildMenu() override { 343 | Menu *menu = new Menu(); 344 | menu->addChild(new StepScaleModeChildMenuItem(module, 345 | Ministep::SCALE_RELATIVE, 346 | "10V = max")); 347 | menu->addChild(new StepScaleModeChildMenuItem(module, 348 | Ministep::SCALE_ABSOLUTE, 349 | "1V per step")); 350 | return menu; 351 | } 352 | }; 353 | 354 | struct OutputScaleModeChildMenuItem : ScaleModeChildMenuItem { 355 | OutputScaleModeChildMenuItem(Ministep *m, Ministep::OutputScaleMode pMode, 356 | std::string label) 357 | : ScaleModeChildMenuItem(m, pMode, &m->outputScaleMode, label) {}; 358 | }; 359 | 360 | struct OutputScaleModeMenuItem : MenuItem { 361 | Ministep *module; 362 | Menu *createChildMenu() override { 363 | Menu *menu = new Menu(); 364 | 365 | menu->addChild(new OutputScaleModeChildMenuItem(module, 366 | Ministep::SCALE_10V_PER_NSTEPS, 367 | "max = 10V")); 368 | menu->addChild(new OutputScaleModeChildMenuItem(module, 369 | Ministep::SCALE_1V_PER_STEP, 370 | "1V per step")); 371 | return menu; 372 | } 373 | }; 374 | 375 | struct OffsetByHalfStepMenuItem : MenuItem { 376 | Ministep *module; 377 | bool valueToSet; 378 | void onAction(const event::Action &e) override { 379 | module->offsetByHalfStep = valueToSet; 380 | } 381 | }; 382 | 383 | struct MinistepWidget : ModuleWidget { 384 | Ministep *module; 385 | ScaleDisplayWidget *scaleDisplay; 386 | CurrentStepDisplayWidget *currentStepDisplay; 387 | NStepsSelector *nStepsSelector; 388 | 389 | MinistepWidget(Ministep *module) { 390 | setModule(module); 391 | this->module = module; 392 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Ministep.svg"))); 393 | 394 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 395 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 396 | 397 | addInput(createInputCentered(Vec(22.5, 1*47), module, Ministep::INCREMENT_INPUT)); 398 | addInput(createInputCentered(Vec(22.5, 2*47), module, Ministep::DECREMENT_INPUT)); 399 | addInput(createInputCentered(Vec(22.5, 3*47), module, Ministep::RESET_INPUT)); 400 | addInput(createInputCentered(Vec(22.5, 4*47), module, Ministep::SCALE_INPUT)); 401 | addOutput(createOutputCentered(Vec(22.5, 261), module, Ministep::STEP_OUTPUT)); 402 | 403 | scaleDisplay = new ScaleDisplayWidget(module, module->currentScale); 404 | scaleDisplay->box.pos = Vec(7.5, 208); 405 | addChild(scaleDisplay); 406 | 407 | currentStepDisplay = new CurrentStepDisplayWidget(module, module->currentStep); 408 | currentStepDisplay->box.pos = Vec(7.5, 280); 409 | addChild(currentStepDisplay); 410 | 411 | nStepsSelector = new NStepsSelector(module); 412 | nStepsSelector->TextBox::box.pos = Vec(7.5, 317); 413 | addChild(static_cast(nStepsSelector)); 414 | 415 | } 416 | 417 | void appendContextMenu(ui::Menu *menu) override { 418 | if(module) { 419 | menu->addChild(new MenuLabel()); 420 | 421 | auto *stepSMMenuItem = new StepScaleModeMenuItem(); 422 | stepSMMenuItem->text = "Scale mode"; 423 | stepSMMenuItem->rightText = RIGHT_ARROW; 424 | stepSMMenuItem->module = module; 425 | menu->addChild(stepSMMenuItem); 426 | 427 | auto *outSMMenuItem = new OutputScaleModeMenuItem(); 428 | outSMMenuItem->text = "Output mode"; 429 | outSMMenuItem->rightText = RIGHT_ARROW; 430 | outSMMenuItem->module = module; 431 | menu->addChild(outSMMenuItem); 432 | 433 | auto *offsetMenuItem = new OffsetByHalfStepMenuItem(); 434 | offsetMenuItem->text = "Offset output by half step"; 435 | offsetMenuItem->module = module; 436 | offsetMenuItem->rightText = CHECKMARK(module->offsetByHalfStep); 437 | offsetMenuItem->valueToSet = !module->offsetByHalfStep; 438 | menu->addChild(offsetMenuItem); 439 | } 440 | } 441 | }; 442 | 443 | Model *modelMinistep = createModel("Ministep"); 444 | -------------------------------------------------------------------------------- /src/Util.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | //sgn() function based on https://stackoverflow.com/questions/1903954/is-there-a-standard-sign-function-signum-sgn-in-c-c 4 | inline constexpr 5 | int signum(float val) { 6 | return (0.f < val) - (val < 0.f); 7 | } 8 | 9 | // Helper function for adding a small LED to the upper right corner of a port 10 | // usage in module widget constructor: 11 | // addChild(createTinyLightForPort(position_of_port_center, ... other params as in createLightCentered() ...)) 12 | template 13 | TinyLight *createTinyLightForPort(Vec portCenterPos, Module *module, int firstLightId) { 14 | constexpr float offset = 3.6f; 15 | return createLightCentered>( 16 | portCenterPos.plus(Vec(15 - offset, offset - 15)), 17 | module, firstLightId); 18 | } 19 | 20 | struct GUITimer { 21 | // Kinda like dsp::PulseGenerator, but uses std::chrono for timing events, since 22 | // we don't have args.sampleTime for Widget::step(). 23 | 24 | //TODO: does rack provide a GUI timer? 25 | bool status = false; // true == high, false == low 26 | std::chrono::steady_clock::time_point finishTime; 27 | 28 | void trigger(float duration) { 29 | // duration in seconds, will be rounded to ms 30 | finishTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(int(duration * 1000.f)); 31 | status = true; 32 | } 33 | 34 | bool process() { 35 | // proecss() takes no parameters, use internal clock to figure out when to go from high to low 36 | if(status) { 37 | status = std::chrono::steady_clock::now() < finishTime; 38 | } 39 | 40 | return status; 41 | } 42 | 43 | void reset() { status = false; } 44 | }; 45 | -------------------------------------------------------------------------------- /src/Widgets.cpp: -------------------------------------------------------------------------------- 1 | #include "Widgets.hpp" 2 | #include // std::replace 3 | 4 | void TextBox::draw(const DrawArgs &args) { 5 | // based on LedDisplayChoice::draw() in Rack/src/app/LedDisplay.cpp 6 | const auto vg = args.vg; 7 | nvgScissor(vg, 0, 0, box.size.x, box.size.y); 8 | nvgBeginPath(vg); 9 | nvgRoundedRect(vg, 0, 0, box.size.x, box.size.y, 3.0); 10 | nvgFillColor(vg, backgroundColor); 11 | nvgFill(vg); 12 | 13 | std::shared_ptr font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/RobotoMono-Bold.ttf")); 14 | if (font) { 15 | 16 | nvgFillColor(vg, textColor); 17 | nvgFontFaceId(vg, font->handle); 18 | 19 | nvgFontSize(vg, fontSize); 20 | nvgTextLetterSpacing(vg, letterSpacing); 21 | nvgTextAlign(vg, textAlign); 22 | nvgText(vg, textOffset.x, textOffset.y, text.c_str(), NULL); 23 | } 24 | 25 | nvgResetScissor(vg); 26 | }; 27 | 28 | 29 | void NumberTextBox::draw(const DrawArgs &args) { 30 | auto vg = args.vg; 31 | backgroundColor = state == BND_HOVER ? hoverColor : defaultColor; 32 | 33 | std::string originalText = TextBox::text; 34 | 35 | if(isFocused) { 36 | // if we're editing, display TextField::text 37 | TextBox::setText(TextField::text); 38 | } 39 | std::string s = TextBox::text; 40 | std::replace(s.begin(), s.end(), '0', 'O'); 41 | TextBox::setText(s); 42 | TextBox::draw(args); 43 | 44 | TextBox::setText(originalText); 45 | 46 | if(isFocused) { 47 | // This color is used for highlighting the selection and for the caret 48 | NVGcolor highlightColor = nvgRGB(0x0, 0x90, 0xd8); 49 | highlightColor.a = 0.5; 50 | 51 | int begin = std::min(cursor, selection); 52 | int end = std::max(cursor, selection); 53 | int len = end - begin; 54 | 55 | // font face, size, alignment etc should be the same as for TextBox after the above draw call 56 | 57 | // hacky way of measuring character width 58 | NVGglyphPosition glyphs[4]; 59 | nvgTextGlyphPositions(vg, 0.f, 0.f, "0", NULL, glyphs, 4); 60 | float char_width = -2*glyphs[0].x; 61 | 62 | float ymargin = 2.f; 63 | nvgBeginPath(vg); 64 | nvgFillColor(vg, highlightColor); 65 | 66 | // draw highlight / caret. note that this assumes that the text is center-aligned. 67 | nvgRect(vg, 68 | textOffset.x + (begin - 0.5f * TextField::text.size()) * char_width - 1, 69 | ymargin, 70 | (len > 0 ? char_width * len : 1) + 1, 71 | TextBox::box.size.y - 2.f * ymargin); 72 | nvgFill(vg); 73 | 74 | } 75 | } 76 | 77 | void NumberTextBox::onAction(const event::Action &e) { 78 | // this gets fired when the user confirms the text input 79 | 80 | bool success = false; 81 | std::string inp = TextField::text; 82 | inp.erase(0, inp.find_first_not_of("0")); // trim leading zeros 83 | if(inp.size() > 0) { 84 | // here we assume that the text contains only digits 85 | int n = stoi(inp); 86 | if(n > 0) { 87 | // the number was valid, call onNumberSet and update display text 88 | onNumberSet(n); 89 | TextBox::setText(inp); 90 | TextField::setText(inp); // update trimmed leading zeros to TextField also 91 | success = true; 92 | } 93 | } 94 | 95 | if(!success) { 96 | // the input number was invalid, revert TextField to the text we were displaying before 97 | cursor = 0; 98 | selection = 0; 99 | TextField::setText(TextBox::text); 100 | } 101 | 102 | event::Deselect eDeselect; 103 | onDeselect(eDeselect); 104 | APP->event->setSelectedWidget(NULL); 105 | e.consume(NULL); // or should we consume it with 'this'? 106 | } 107 | 108 | void NumberTextBox::onSelectText(const event::SelectText &e) { 109 | // only accpet numeric input 110 | 111 | // cursor != selection means that something is selected, so we may input 112 | // even if the textbox is full. 113 | if((TextField::text.size() < maxTextLength || cursor != selection) 114 | // assuming GLFW number keys are contiguous 115 | && GLFW_KEY_0 <= e.codepoint 116 | && e.codepoint <= GLFW_KEY_9) { 117 | TextField::onSelectText(e); 118 | } else { 119 | e.consume(NULL); 120 | } 121 | } 122 | 123 | void NumberTextBox::onSelectKey(const event::SelectKey &e) { 124 | // handle actual typing 125 | 126 | bool act = e.action == GLFW_PRESS || e.action == GLFW_REPEAT; 127 | if(act && e.key == GLFW_KEY_V && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { 128 | // TextField::pasteClipboard() can't be overridden, so we must handle 129 | // ctrl+V manually to do validation. Prevent pasting too long text and 130 | // text containing non-numeric characters. 131 | size_t pasteLength = maxTextLength - TextField::text.size() + abs(selection - cursor); 132 | if(pasteLength > 0) { 133 | std::string newText(glfwGetClipboardString(APP->window->win)); 134 | if(newText.size() > pasteLength) newText.erase(pasteLength); 135 | 136 | if(isNumber(newText)) { 137 | insertText(newText); 138 | } 139 | } 140 | // e is consumed below 141 | 142 | } else if(act && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT && e.key == GLFW_KEY_HOME) { 143 | // don't move selection 144 | cursor = 0; 145 | } else if(act && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT && e.key == GLFW_KEY_END) { 146 | cursor = TextField::text.size(); 147 | } else if(act && e.key == GLFW_KEY_ESCAPE) { 148 | // escape is the same as pressing enter. onAction calls onDeselect(). 149 | event::Action eAction; 150 | onAction(eAction); 151 | 152 | // e is consumed below 153 | 154 | } else { 155 | TextField::onSelectKey(e); 156 | } 157 | 158 | if(!e.isConsumed()) 159 | e.consume(static_cast(this)); 160 | } 161 | -------------------------------------------------------------------------------- /src/Widgets.hpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | 3 | // TODO: same as in LittleUtils, move to common folder? 4 | struct TextBox : TransparentWidget { 5 | // Kinda like TextField except not editable. Using Roboto Mono Bold font, 6 | // numbers look okay. 7 | // based on LedDisplayChoice 8 | std::string text; 9 | float fontSize; 10 | float letterSpacing; 11 | Vec textOffset; 12 | NVGcolor defaultTextColor; 13 | NVGcolor textColor; // This can be used to temporarily override text color 14 | NVGcolor backgroundColor; 15 | int textAlign; 16 | 17 | //TODO: create<...>() thing with position as argument? 18 | TextBox() { 19 | defaultTextColor = nvgRGB(0x23, 0x23, 0x23); 20 | textColor = defaultTextColor; 21 | backgroundColor = nvgRGB(0x78, 0x78, 0x78); 22 | box.size = Vec(30, 18); 23 | // size 20 with spacing -2 will fit 3 characters on a 30px box with Roboto mono 24 | fontSize = 20; 25 | letterSpacing = 0.f; 26 | textOffset = Vec(box.size.x * 0.5f, 0.f); 27 | textAlign = NVG_ALIGN_CENTER | NVG_ALIGN_TOP; 28 | } 29 | 30 | virtual void setText(std::string s) { text = s; } 31 | 32 | virtual void draw(const DrawArgs &args) override; 33 | 34 | }; 35 | 36 | /* 37 | * Editable TextBox that only allows inputting positive numbers. 38 | * 39 | * Based on EditableTextbox and HoverableTextbox in LittleUtils. TextField is 40 | * inherited so that the default behavior of onSelectKey and some click events 41 | * can be handled by it. Otherwise everything should come from TextBox. For 42 | * example the box size is determined by TextBox::box.size, so that should be 43 | * the box to manipulate if necessary. TextField::text holds the string that is 44 | * being edited and TextBox::text holds the string being displayed, which 45 | * should always be a valid number. 46 | */ 47 | struct NumberTextBox : TextBox, TextField { 48 | 49 | // attributes from HoverableTextBox 50 | BNDwidgetState state = BND_DEFAULT; 51 | NVGcolor defaultColor; 52 | NVGcolor hoverColor; 53 | 54 | // attributes from EditableTextBox 55 | bool isFocused = false; 56 | const static unsigned int defaultMaxTextLength = 6; 57 | unsigned int maxTextLength; 58 | 59 | NumberTextBox(): TextBox(), TextField() { 60 | defaultColor = backgroundColor; 61 | hoverColor = nvgRGB(0x90, 0x90, 0x90); 62 | maxTextLength = defaultMaxTextLength; 63 | 64 | // Default alignment and offset. Caret breaks with left-align, so let's 65 | // go with center for now. textOffset should be overridden by 66 | // subclasses if they change the box size. 67 | textAlign = NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE; 68 | textOffset = Vec(TextBox::box.size.x / 2, TextBox::box.size.y / 2); 69 | } 70 | 71 | bool isNumber(const std::string& s) { 72 | // shamelessly copypasted from https://stackoverflow.com/questions/4654636/how-to-determine-if-a-string-is-a-number-with-c 73 | std::string::const_iterator it = s.begin(); 74 | while (it != s.end() && std::isdigit(*it)) ++it; 75 | return !s.empty() && it == s.end(); 76 | } 77 | 78 | void onHover(const event::Hover &e) override { 79 | TextField::onHover(e); 80 | e.consume(static_cast(this)); // to catch onEnter and onLeave 81 | } 82 | 83 | void onDragHover(const event::DragHover &e) override { 84 | TextField::onDragHover(e); 85 | } 86 | 87 | void onHoverScroll(const event::HoverScroll &e) override { 88 | TextField::onHoverScroll(e); 89 | } 90 | 91 | void onEnter(const event::Enter &e) override { 92 | state = BND_HOVER; 93 | } 94 | 95 | void onLeave(const event::Leave &e) override { 96 | state = BND_DEFAULT; 97 | } 98 | 99 | void onButton(const event::Button &e) override { 100 | ButtonEvent ee(e); 101 | 102 | // Hacky way to make right-click behave the same as left click. We have 103 | // to do this, because TextField::pasteClipboard() is not virtual, so 104 | // we can't override it, so this is the only way to prevent pasting 105 | // text into the number field. 106 | // Note that in the case of a normal left click, setSelectedWidget(), 107 | // which calls onSelect() is called by the event loop (?) already 108 | // before handling the onButton event, but we can't intercept the 109 | // left/right click there. so we have to manually do it again here. 110 | if(e.button == GLFW_MOUSE_BUTTON_RIGHT) { 111 | ee.button = GLFW_MOUSE_BUTTON_LEFT; 112 | } 113 | 114 | if(ee.action == GLFW_PRESS && ee.button == GLFW_MOUSE_BUTTON_LEFT) { 115 | // hack, see above 116 | APP->event->setSelectedWidget(static_cast(this)); 117 | } 118 | 119 | TextField::onButton(ee); // this handles consuming the event 120 | 121 | if(ee.isConsumed()) { 122 | e.consume(ee.getTarget()); 123 | } 124 | } 125 | 126 | void onSelect(const event::Select &e) override { 127 | isFocused = true; 128 | e.consume(static_cast(this)); //TODO 129 | } 130 | 131 | void onDeselect(const event::Deselect &e) override { 132 | isFocused = false; 133 | e.consume(NULL); 134 | } 135 | 136 | void onAction(const event::Action &e) override; 137 | 138 | void onSelectText(const event::SelectText &e) override; 139 | void onSelectKey(const event::SelectKey &e) override; 140 | 141 | void step() override { 142 | TextField::step(); 143 | } 144 | 145 | void draw(const DrawArgs &args) override; 146 | 147 | // custom event, called from within onAction 148 | virtual void onNumberSet(const int n) {}; 149 | 150 | }; 151 | 152 | // For slight DRY 153 | struct MenuItemWithRightArrow : MenuItem { 154 | MenuItemWithRightArrow(): MenuItem() { 155 | rightText = RIGHT_ARROW; 156 | }; 157 | }; 158 | -------------------------------------------------------------------------------- /src/plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | 3 | 4 | Plugin *pluginInstance; 5 | 6 | 7 | void init(Plugin *p) { 8 | pluginInstance = p; 9 | 10 | // Add all Models defined throughout the plugin 11 | p->addModel(modelArray); 12 | p->addModel(modelMiniramp); 13 | p->addModel(modelMinistep); 14 | 15 | // Any other plugin initialization may go here. 16 | // As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack. 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "rack.hpp" 3 | 4 | static const int MAX_POLY_CHANNELS = 16; 5 | 6 | using namespace rack; 7 | 8 | // Forward-declare the Plugin, defined in Array.cpp 9 | extern Plugin *pluginInstance; 10 | 11 | // Forward-declare each Model, defined in each module source file 12 | extern Model *modelArray; 13 | extern Model *modelMiniramp; 14 | extern Model *modelMinistep; 15 | --------------------------------------------------------------------------------