@gorilla-engine-sdk/create-gorilla-engine-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +64 -0
- package/README.md +103 -0
- package/generator-gorilla-engine-plugin/app/index.js +338 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/SKILL.md +295 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/effects-filters.md +186 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/indexing.md +129 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/instrument-files.md +547 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/key-rules.md +81 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/modulation.md +210 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/module-control.md +217 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/module-reference-bus-effects.md +663 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/module-reference-core.md +145 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/module-reference-misc.md +59 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/module-reference-modulation.md +85 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/module-reference-voice-effects.md +407 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/note-events.md +278 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/properties.md +230 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/script-patterns.md +494 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-dsp/references/time-tempo.md +209 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/SKILL.md +104 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/advanced-controls.md +194 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/base-props.md +167 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/basic-controls.md +230 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/bindings.md +183 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/composite-components.md +258 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/containers.md +149 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/editors.md +191 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/event-handlers.md +150 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/getting-started.md +143 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/gorilla-engine-api.md +147 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/instrument-api.md +250 -0
- package/generator-gorilla-engine-plugin/app/templates/.agents/skills/gorilla-engine-ui/references/visualization.md +209 -0
- package/generator-gorilla-engine-plugin/app/templates/.prettierignore +13 -0
- package/generator-gorilla-engine-plugin/app/templates/.vscode/settings.json +14 -0
- package/generator-gorilla-engine-plugin/app/templates/.vscode_input/launch.json +89 -0
- package/generator-gorilla-engine-plugin/app/templates/README.md +36 -0
- package/generator-gorilla-engine-plugin/app/templates/assets/fonts/OFL.txt +93 -0
- package/generator-gorilla-engine-plugin/app/templates/assets/fonts/Roboto-Bold.otf +0 -0
- package/generator-gorilla-engine-plugin/app/templates/assets/fonts/Roboto-Medium.otf +0 -0
- package/generator-gorilla-engine-plugin/app/templates/assets/fonts/Roboto-Regular.otf +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/Presets/Factory/Category 1/Init.preset +1 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/Presets/Factory/Category 1/Three Quarters.preset +1 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/Horizontal Slider.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/Horizontal Slider@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/Rotary Bipolar.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/Rotary-Bipolar@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/Rotary.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/Rotary@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-down-default.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-down-default@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-down-hover.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-down-hover@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-down-pressed.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-down-pressed@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-up-default.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-up-default@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-up-hover.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-up-hover@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-up-pressed.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/arrow-up-pressed@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/toggle-default.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/toggle-default@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/toggle-pressed.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/images/toggle-pressed@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/instruments/Basic Synth Example_part1.blob +0 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/assets/instruments/Basic Synth Example_part2.blob +1 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/instrument/Basic Synth Example.inst +157 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/instrument/Basic Synth Example.txt +184 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/app/Root.tsx +35 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/app/components/ErrorBoundary.tsx +138 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/app/components/UserInterface.tsx +338 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/app/pluginConfig.ts +4 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/dialogsCustomStyle.ts +98 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/index.tsx +217 -0
- package/generator-gorilla-engine-plugin/app/templates/basic-synth/src/presetBrowserCustomStyle.ts +124 -0
- package/generator-gorilla-engine-plugin/app/templates/build.ugep +173 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/Presets/Factory/Category 1/01 - All The Way Down.preset +1 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/Presets/Factory/Category 1/02 - About Half.preset +1 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/Presets/Factory/Category 1/03 - Three Quarters.preset +1 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/Presets/Factory/Category 1/04 - All The Way Up.preset +1 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/images/Rotary.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/images/Rotary@2x.png +0 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/instruments/Reverb Example_part1.blob +0 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/assets/instruments/Reverb Example_part2.blob +1 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/instrument/Reverb Example.inst +73 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/instrument/Reverb Example.txt +21 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/app/Root.tsx +35 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/app/components/ErrorBoundary.tsx +114 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/app/components/UserInterface.tsx +85 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/app/pluginConfig.ts +4 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/dialogsCustomStyle.ts +101 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/index.tsx +214 -0
- package/generator-gorilla-engine-plugin/app/templates/effect/src/presetBrowserCustomStyle.ts +124 -0
- package/generator-gorilla-engine-plugin/app/templates/gitignore +9 -0
- package/generator-gorilla-engine-plugin/app/templates/licence.txt +0 -0
- package/generator-gorilla-engine-plugin/app/templates/package.json +38 -0
- package/generator-gorilla-engine-plugin/app/templates/prettierrc +9 -0
- package/generator-gorilla-engine-plugin/app/templates/tsconfig.json +20 -0
- package/generator-gorilla-engine-plugin/package.json +17 -0
- package/index.js +72 -0
- package/package.json +42 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Properties Reference
|
|
2
|
+
|
|
3
|
+
Properties are declared in `on init` and serve as both script variables and plugin parameters. They bridge the script to the UI — when a user changes a knob/slider bound to a property, the script receives the new value in an `on <property>` callback.
|
|
4
|
+
|
|
5
|
+
## Property Types
|
|
6
|
+
|
|
7
|
+
### prop_linear — Linear Range
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
prop_linear <name>(min, max, scaling) [= default]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Display = value / scaling. Most common property type.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
prop_linear Volume(0, 1000, 10) = 500 ' display 0.0 to 100.0, default 50.0
|
|
17
|
+
prop_linear Tune(-1200, 1200, 100) ' display -12.00 to 12.00
|
|
18
|
+
prop_linear Mix(0, 100, 1) = 50 ' display 0 to 100
|
|
19
|
+
set_prop_param(Mix, PROP_UNITS, "%")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### prop_boolean — On/Off Switch
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
prop_boolean <name> [= default]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Value is 0 or 1.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
prop_boolean Mute
|
|
32
|
+
prop_boolean Retrigger = 1
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### prop_exp — Exponential (Frequencies, Rates, Envelope Times)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
prop_exp <name>(max, displayMin, displayMax) [= default]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Value is linear 0..max in script. Display is exponential displayMin/1000 to displayMax/1000. Negative displayMin means display 0 at zero and -displayMin/1000 at 1% position.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
prop_exp Cutoff(1000000, 20000, 25000000) = 549000 ' 20 Hz to 25 kHz, default ~1 kHz
|
|
45
|
+
prop_exp Rate(1000000, 10, 100000) = 500000 ' 0.01 to 100 Hz, default 1 Hz
|
|
46
|
+
prop_exp Decay(1000000, -10, 16000) ' 0.0 to 16.0 sec, 10ms at 1%
|
|
47
|
+
prop_exp Glide_Time(1000000, 0, 10000) = 250000
|
|
48
|
+
set_prop_param(Glide_Time, PROP_UNITS, "sec")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### prop_decibels — Volume in dB
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
prop_decibels <name>(max, dBmax) [= default]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Cube-law dB display. Value is linear 0..max. Display is -inf to dBmax.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
prop_decibels Volume(1000000, 12) ' 0 to +12.0 dB
|
|
61
|
+
prop_decibels Master(1000000, 0) ' -inf to 0.0 dB
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### prop_squared — Square-Law (Pre-Delay, Ratios)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
prop_squared <name>(min, max, displayMin, displayMax) [= default]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Square-law display. Midpoint of slider = 25% of max.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
prop_squared Delay(0, 1000000, 0, 25) ' 0.0 to 25.0 ms
|
|
74
|
+
prop_squared Ratio(0, 1000000, 1, 20) ' 1.0 to 20.0
|
|
75
|
+
prop_squared Depth(-1000000, 1000000, -100, 100) ' bipolar with square scaling
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### prop_stepped — Discrete Named Steps
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
prop_stepped <name> [= default]
|
|
82
|
+
add_step(<name>, "label")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Values are 0, 1, 2... in order of `add_step()` calls.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
prop_stepped Mode = 1
|
|
89
|
+
add_step(Mode, "Retrig"); const MODE_RETRIG = 0
|
|
90
|
+
add_step(Mode, "Legato"); const MODE_LEGATO = 1
|
|
91
|
+
add_step(Mode, "Repitch"); const MODE_REPITCH = 2
|
|
92
|
+
|
|
93
|
+
prop_stepped Rate = 6
|
|
94
|
+
add_step(Rate, "1/4")
|
|
95
|
+
add_step(Rate, "1/8")
|
|
96
|
+
add_step(Rate, "1/16")
|
|
97
|
+
var rateToPPQ[3] = (960, 480, 240)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### prop_array — Array Property
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
prop_array <name>[size](min, max, scaling) [= (...)]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Cannot be automated. Expose individual values for automation.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
prop_array Velocity_Curve[128](1, 127, 1)
|
|
110
|
+
prop_array Step_Seq[16](-12, 12, 1) = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### prop_text — Text String
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
prop_text <name>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
prop_text Name
|
|
121
|
+
prop_text Status
|
|
122
|
+
set_prop_param(Status, PROP_SET_BY, SET_BY_SCRIPT) ' read-only display
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### prop_meter — Real-Time Measurement
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
prop_meter <name> [size]
|
|
129
|
+
attach_meter(<name>, module, submodule)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
prop_meter Level; attach_meter(Level, M_BUS+1, 0) ' bus level
|
|
134
|
+
prop_meter Level[2]; attach_meter(Level, M_BUS+3, 0) ' stereo
|
|
135
|
+
prop_meter GR; attach_meter(GR, M_BUS+1, M_EFFECT+1) ' gain reduction
|
|
136
|
+
prop_meter Pos; attach_meter(Pos, M_GROUP+0, 0) ' playback position
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### prop_waveform — Waveform Display
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
prop_waveform <name> [(group)]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
prop_waveform Wave_Display
|
|
147
|
+
set_waveform(Wave_Display, -1, 60, 100) ' group/mapping, key, vel
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Property Configuration
|
|
151
|
+
|
|
152
|
+
### set_prop_param(property, param, value)
|
|
153
|
+
|
|
154
|
+
**Numeric parameters:**
|
|
155
|
+
|
|
156
|
+
| Param | Description |
|
|
157
|
+
|-------|-------------|
|
|
158
|
+
| `PROP_MIDI_CC` | Assign MIDI CC (1-119) to property |
|
|
159
|
+
| `PROP_SET_BY` | Control who can modify (see SET_BY flags) |
|
|
160
|
+
| `PROP_NON_REALTIME` | Set to 1 for non-realtime-safe properties |
|
|
161
|
+
| `PROP_MIN` | Override minimum value |
|
|
162
|
+
| `PROP_MAX` | Override maximum value |
|
|
163
|
+
|
|
164
|
+
**Text parameters:**
|
|
165
|
+
|
|
166
|
+
| Param | Description |
|
|
167
|
+
|-------|-------------|
|
|
168
|
+
| `PROP_NAME` | Display name (default: name with `_` → spaces) |
|
|
169
|
+
| `PROP_UNITS` | Units suffix (e.g. `"%"`, `"sec"`, `"semi"`, `"Hz"`) |
|
|
170
|
+
| `PROP_VALUE_TEXT` | Custom display text replacing value+units |
|
|
171
|
+
| `PROP_GROUP` | Automation group name |
|
|
172
|
+
| `PROP_METADATA` | Additional info for UI |
|
|
173
|
+
|
|
174
|
+
Special units: `"L|R"` for pan controls (displays L/R instead of -/+).
|
|
175
|
+
|
|
176
|
+
### SET_BY Flags
|
|
177
|
+
|
|
178
|
+
| Flag | Description |
|
|
179
|
+
|------|-------------|
|
|
180
|
+
| `SET_BY_HOST` | Value set externally, script receives it |
|
|
181
|
+
| `SET_BY_SONG` | Stored/recalled with DAW session |
|
|
182
|
+
| `SET_BY_PATCH` | Stored/recalled with presets |
|
|
183
|
+
| `SET_BY_REMOTE_AUTO` | Can be automated |
|
|
184
|
+
| `SET_BY_SCRIPT` | Script sets value, displayed on GUI (read-only to user) |
|
|
185
|
+
| `SET_BY_GUI` | GUI controls set value, script does NOT receive callback |
|
|
186
|
+
|
|
187
|
+
**Default:** `SET_BY_HOST + SET_BY_SONG + SET_BY_PATCH + SET_BY_REMOTE_AUTO`
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
' Read-only display property
|
|
191
|
+
prop_text Status
|
|
192
|
+
set_prop_param(Status, PROP_SET_BY, SET_BY_SCRIPT)
|
|
193
|
+
|
|
194
|
+
' Non-automatable property
|
|
195
|
+
const NO_AUTO = SET_BY_HOST + SET_BY_SONG + SET_BY_PATCH
|
|
196
|
+
prop_stepped Source
|
|
197
|
+
set_prop_param(Source, PROP_SET_BY, NO_AUTO)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Property Callbacks
|
|
201
|
+
|
|
202
|
+
Every declared property automatically gets a callback:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
prop_linear Volume(0, 1000000, 1) = 500000
|
|
206
|
+
|
|
207
|
+
on Volume
|
|
208
|
+
set_module_param(M_INST, 0, 0, 3, Volume) ' update instrument volume
|
|
209
|
+
end on
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The callback runs when:
|
|
213
|
+
- The UI changes the property
|
|
214
|
+
- The host automates the property
|
|
215
|
+
- The script assigns a new value to the property
|
|
216
|
+
- A preset is loaded
|
|
217
|
+
|
|
218
|
+
## Property ↔ UI Binding
|
|
219
|
+
|
|
220
|
+
Properties declared in the script become instrument parameters that the React UI can bind to using `useInstrumentProperty()` or `bindings`. The property name in UI is **always lowercase**, even if declared uppercase in the script.
|
|
221
|
+
|
|
222
|
+
Script:
|
|
223
|
+
```
|
|
224
|
+
prop_linear Cutoff(0, 1000000, 1)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
React UI:
|
|
228
|
+
```tsx
|
|
229
|
+
const [cutoff, setCutoff] = useInstrumentProperty(instrument, 'cutoff');
|
|
230
|
+
```
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# Script Patterns Reference
|
|
2
|
+
|
|
3
|
+
Real-world patterns for common audio DSP tasks in Gorilla Engine scripts.
|
|
4
|
+
|
|
5
|
+
## Arpeggiator
|
|
6
|
+
|
|
7
|
+
Uses `delay_ppq()` in a `while` loop to step through held notes at musical intervals. Cancel with `end_delay()`.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
on init
|
|
11
|
+
var seqCallback
|
|
12
|
+
var seqEvent = -1
|
|
13
|
+
var seqPos
|
|
14
|
+
var heldKeys[16]
|
|
15
|
+
var numHeldKeys
|
|
16
|
+
var i
|
|
17
|
+
|
|
18
|
+
prop_stepped Rate = 6
|
|
19
|
+
add_step(Rate, "1/4")
|
|
20
|
+
add_step(Rate, "1/8")
|
|
21
|
+
add_step(Rate, "1/16")
|
|
22
|
+
var rateToPPQ[3] = (960, 480, 240)
|
|
23
|
+
|
|
24
|
+
prop_linear Gate(0, 100, 1) = 75
|
|
25
|
+
set_prop_param(Gate, PROP_UNITS, "%")
|
|
26
|
+
|
|
27
|
+
prop_stepped Mode
|
|
28
|
+
add_step(Mode, "Up")
|
|
29
|
+
add_step(Mode, "Down")
|
|
30
|
+
add_step(Mode, "Random")
|
|
31
|
+
end on
|
|
32
|
+
|
|
33
|
+
function NextStep
|
|
34
|
+
release_note(seqEvent)
|
|
35
|
+
|
|
36
|
+
' advance position
|
|
37
|
+
select Mode
|
|
38
|
+
case 0 ' Up
|
|
39
|
+
inc(seqPos)
|
|
40
|
+
if seqPos >= numHeldKeys
|
|
41
|
+
seqPos = 0
|
|
42
|
+
end if
|
|
43
|
+
case 1 ' Down
|
|
44
|
+
dec(seqPos)
|
|
45
|
+
if seqPos < 0
|
|
46
|
+
seqPos = numHeldKeys - 1
|
|
47
|
+
end if
|
|
48
|
+
case 2 ' Random
|
|
49
|
+
seqPos = random(0, numHeldKeys - 1)
|
|
50
|
+
end select
|
|
51
|
+
|
|
52
|
+
seqEvent = new_note(heldKeys[seqPos], 100, 0, 0)
|
|
53
|
+
group_on(seqEvent, ALL_GROUPS)
|
|
54
|
+
|
|
55
|
+
i = rateToPPQ[Rate]
|
|
56
|
+
delay_ppq((i * Gate) / 100) ' gate portion
|
|
57
|
+
|
|
58
|
+
if seqCallback = CALLBACK_ID
|
|
59
|
+
release_note(seqEvent)
|
|
60
|
+
delay_ppq(i - (i * Gate) / 100) ' rest
|
|
61
|
+
end if
|
|
62
|
+
end function
|
|
63
|
+
|
|
64
|
+
function StopSequence
|
|
65
|
+
var temp = seqCallback
|
|
66
|
+
seqCallback = 0
|
|
67
|
+
end_delay(temp, 0)
|
|
68
|
+
release_note(temp)
|
|
69
|
+
release_note(seqEvent)
|
|
70
|
+
end function
|
|
71
|
+
|
|
72
|
+
function StartSequence
|
|
73
|
+
if seqCallback
|
|
74
|
+
StopSequence()
|
|
75
|
+
end if
|
|
76
|
+
seqPos = -1
|
|
77
|
+
seqCallback = CALLBACK_ID
|
|
78
|
+
while seqCallback = CALLBACK_ID
|
|
79
|
+
NextStep()
|
|
80
|
+
end while
|
|
81
|
+
end function
|
|
82
|
+
|
|
83
|
+
on note
|
|
84
|
+
' ... add to heldKeys, then:
|
|
85
|
+
group_off(EVENT_ID, ALL_GROUPS)
|
|
86
|
+
StartSequence()
|
|
87
|
+
end on
|
|
88
|
+
|
|
89
|
+
on release
|
|
90
|
+
' ... remove from heldKeys
|
|
91
|
+
if numHeldKeys = 0
|
|
92
|
+
StopSequence()
|
|
93
|
+
else if EVENT_ID = seqCallback
|
|
94
|
+
ignore_event(EVENT_ID)
|
|
95
|
+
end if
|
|
96
|
+
end on
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Mono / Legato / Glide
|
|
100
|
+
|
|
101
|
+
Maintains a held-note stack. When in mono mode, one voice is re-used with pitch changes instead of creating new voices.
|
|
102
|
+
|
|
103
|
+
### Simple Mono with Glide via EVENT_PARAM
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
on init
|
|
107
|
+
prop_boolean Mono = 1
|
|
108
|
+
prop_exp Glide_Time(1000000, 0, 10000) = 250000
|
|
109
|
+
set_prop_param(Glide_Time, PROP_UNITS, "sec")
|
|
110
|
+
|
|
111
|
+
var heldNotes[128]
|
|
112
|
+
var numHeldNotes
|
|
113
|
+
var currentEvent = 0
|
|
114
|
+
var i
|
|
115
|
+
end on
|
|
116
|
+
|
|
117
|
+
on Glide_Time
|
|
118
|
+
' Set smoothing on the mod routing from Event Param 0 to Tune
|
|
119
|
+
set_module_param(M_GROUP+0, 0, M_MOD+4, 6, Glide_Time)
|
|
120
|
+
end on
|
|
121
|
+
|
|
122
|
+
on note
|
|
123
|
+
' ... add EVENT_KEY to heldNotes ...
|
|
124
|
+
if Mono
|
|
125
|
+
ignore_event(EVENT_ID)
|
|
126
|
+
if currentEvent = 0
|
|
127
|
+
currentEvent = new_note(60, EVENT_VEL, 0, 0)
|
|
128
|
+
end if
|
|
129
|
+
set_event_param(currentEvent, EVENT_PARAM_0, 83333 * (EVENT_KEY - 60))
|
|
130
|
+
end if
|
|
131
|
+
end on
|
|
132
|
+
|
|
133
|
+
on release
|
|
134
|
+
' ... remove from heldNotes ...
|
|
135
|
+
if Mono and numHeldNotes
|
|
136
|
+
set_event_param(currentEvent, EVENT_PARAM_0, 83333 * (heldNotes[numHeldNotes-1] - 60))
|
|
137
|
+
else if currentEvent
|
|
138
|
+
release_note(currentEvent)
|
|
139
|
+
currentEvent = 0
|
|
140
|
+
end if
|
|
141
|
+
end on
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Glide via ModSourceCustom + set_controller (CC Bridge)
|
|
145
|
+
|
|
146
|
+
A second approach where the glide curve shape and speed are controlled by a `ModSourceCustom` whose internal timing is driven by a MIDI CC. The script forwards a UI property to the CC; the instrument's mod matrix does the rest.
|
|
147
|
+
|
|
148
|
+
**Required .inst topology** — set up in Gorilla Editor (`ModSourceCustom` is Tier 3 and cannot be written as JSON):
|
|
149
|
+
1. Add a `ModSourceCustom` to the group (e.g. `"Glide Curve"`)
|
|
150
|
+
2. Inside it, add a `ModRouting`: `Source: "MIDI CC"`, `CC: 5`, `Dest: "Time 1"`, `Curve Active: "On"` with a timing curve baked in
|
|
151
|
+
3. Add a group-level `ModRouting` from this mod source to `Dest: "Tune"`
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
on init
|
|
155
|
+
prop_linear Glide_Time(0, 127, 1) = 64 ' range matches CC 0–127
|
|
156
|
+
end on
|
|
157
|
+
|
|
158
|
+
on Glide_Time
|
|
159
|
+
set_controller(5, Glide_Time) ' CC 5 → Custom Env "Time 1" → glide speed
|
|
160
|
+
end on
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Advantage over EVENT_PARAM + Smooth:** The glide curve shape is fully editable in the editor; you can have non-linear glide responses without any additional script logic.
|
|
164
|
+
|
|
165
|
+
**General CC Bridge rule:** `set_controller(cc, value)` broadcasts to all mod routings with a matching MIDI CC source — one script line drives any number of mod matrix destinations simultaneously.
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
prop_stepped Mono_Mode = 2
|
|
169
|
+
add_step(Mono_Mode, "Retrig") ' release + new note
|
|
170
|
+
add_step(Mono_Mode, "Repitch") ' set_tune on existing voice
|
|
171
|
+
add_step(Mono_Mode, "Xfade") ' crossfade old→new at playback position
|
|
172
|
+
|
|
173
|
+
function legatoTrigger
|
|
174
|
+
select Mono_Mode
|
|
175
|
+
case 0 ' Retrig
|
|
176
|
+
release_note(monoEvent)
|
|
177
|
+
monoEvent = new_note(newKey, newVel, 0, 0)
|
|
178
|
+
|
|
179
|
+
case 1 ' Repitch
|
|
180
|
+
set_event_param(monoEvent, EVENT_PARAM_0, ...)
|
|
181
|
+
|
|
182
|
+
case 2 ' Crossfade
|
|
183
|
+
fade_out(monoEvent, 1000 * Xfade_Length, 1)
|
|
184
|
+
monoEvent = new_note(newKey, newVel, get_event_param(monoEvent, EVENT_PARAM_PLAY_POS), 0)
|
|
185
|
+
fade_in(monoEvent, 1000 * Xfade_Length)
|
|
186
|
+
delay(1)
|
|
187
|
+
set_mod_sources(monoEvent, oldEvent, 1023, 0) ' copy envelope state
|
|
188
|
+
end select
|
|
189
|
+
end function
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Round Robin
|
|
193
|
+
|
|
194
|
+
Cycle through alternate sample sets using `EVENT_PARAM_ALT_MAPPING`.
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
on init
|
|
198
|
+
const MAX_RR = 5
|
|
199
|
+
var rr_value
|
|
200
|
+
var rr_time
|
|
201
|
+
|
|
202
|
+
prop_stepped RR_Mode
|
|
203
|
+
add_step(RR_Mode, "Cycle")
|
|
204
|
+
add_step(RR_Mode, "Random")
|
|
205
|
+
add_step(RR_Mode, "Random Excl.")
|
|
206
|
+
|
|
207
|
+
prop_linear RR_Min_Time(0, 100, 1) = 50
|
|
208
|
+
set_prop_param(RR_Min_Time, PROP_UNITS, "ms")
|
|
209
|
+
end on
|
|
210
|
+
|
|
211
|
+
on note
|
|
212
|
+
var now = TIME_MSEC
|
|
213
|
+
if now - rr_time >= RR_Min_Time
|
|
214
|
+
select RR_Mode
|
|
215
|
+
case 0; rr_value = (rr_value + 1) mod MAX_RR
|
|
216
|
+
case 1; rr_value = random(0, MAX_RR - 1)
|
|
217
|
+
case 2; rr_value = (rr_value + random(1, MAX_RR - 1)) mod MAX_RR
|
|
218
|
+
end select
|
|
219
|
+
rr_time = now
|
|
220
|
+
end if
|
|
221
|
+
set_event_param(EVENT_ID, EVENT_PARAM_ALT_MAPPING, rr_value)
|
|
222
|
+
end on
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Crossfade Between Layers
|
|
226
|
+
|
|
227
|
+
Crossfade between groups using MIDI CCs or direct module params.
|
|
228
|
+
|
|
229
|
+
### Using CC-controlled mod routings
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
' Each group has a mod routing: CC(n) → Volume
|
|
233
|
+
' Script sets CC values for each group to control crossfade
|
|
234
|
+
|
|
235
|
+
on init
|
|
236
|
+
const MAX_LAYERS = 5
|
|
237
|
+
var currentLayer
|
|
238
|
+
var x
|
|
239
|
+
|
|
240
|
+
prop_linear Num_Layers(1, MAX_LAYERS, 1) = MAX_LAYERS
|
|
241
|
+
prop_boolean Equal_Power = 1
|
|
242
|
+
end on
|
|
243
|
+
|
|
244
|
+
function update_crossfade
|
|
245
|
+
if x = 127
|
|
246
|
+
x = 128 * (Num_Layers - 1)
|
|
247
|
+
else
|
|
248
|
+
x = x * (Num_Layers - 1)
|
|
249
|
+
end if
|
|
250
|
+
|
|
251
|
+
var layer = x >> 7
|
|
252
|
+
var fade = x .and. 127
|
|
253
|
+
|
|
254
|
+
' Silence old layers
|
|
255
|
+
if currentLayer != layer
|
|
256
|
+
set_controller(currentLayer, 0)
|
|
257
|
+
set_controller(currentLayer + 1, 0)
|
|
258
|
+
currentLayer = layer
|
|
259
|
+
end if
|
|
260
|
+
|
|
261
|
+
var ep = 0
|
|
262
|
+
if Equal_Power
|
|
263
|
+
ep = (fade * (127 - fade)) / 152
|
|
264
|
+
end if
|
|
265
|
+
|
|
266
|
+
set_controller(currentLayer, 127 - fade + ep)
|
|
267
|
+
set_controller(currentLayer + 1, fade + ep)
|
|
268
|
+
end function
|
|
269
|
+
|
|
270
|
+
on controller
|
|
271
|
+
if EVENT_CC = 1
|
|
272
|
+
x = CC[1]
|
|
273
|
+
update_crossfade()
|
|
274
|
+
end if
|
|
275
|
+
end on
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Using FilterDownmix (multi-channel crossfade)
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
' For multi-channel samples with a Downmix voice effect
|
|
282
|
+
function set_channel_mix
|
|
283
|
+
set_module_param(M_GROUP+0, M_FILTER+1, 0, param, value) ' L
|
|
284
|
+
set_module_param(M_GROUP+0, M_FILTER+1, 0, param + 1, value) ' R
|
|
285
|
+
end function
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Step Sequencer
|
|
289
|
+
|
|
290
|
+
Step through a pitch sequence using held notes as the base key.
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
on init
|
|
294
|
+
prop_array Step_Seq[16](-12, 12, 1) = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
|
|
295
|
+
prop_linear Steps(1, 16, 1) = 16
|
|
296
|
+
|
|
297
|
+
var seqPos
|
|
298
|
+
var seqEvent = -1
|
|
299
|
+
' ... sequencer variables ...
|
|
300
|
+
end on
|
|
301
|
+
|
|
302
|
+
function NextStep
|
|
303
|
+
release_note(seqEvent)
|
|
304
|
+
inc(seqPos)
|
|
305
|
+
if seqPos >= Steps
|
|
306
|
+
seqPos = 0
|
|
307
|
+
end if
|
|
308
|
+
seqEvent = new_note(heldKeys[numHeldKeys-1] + Step_Seq[seqPos], seqVel, 0, 0)
|
|
309
|
+
group_on(seqEvent, ALL_GROUPS)
|
|
310
|
+
delay_ppq(rateToPPQ[Rate])
|
|
311
|
+
end function
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Sliced Loop Playback
|
|
315
|
+
|
|
316
|
+
Play tempo-synced slices from a sample with markers.
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
on init
|
|
320
|
+
const FADE_LEN = 5000
|
|
321
|
+
var zoneID = -1000000 + 1 ' mapping 1, zone 1
|
|
322
|
+
var sliceEvent
|
|
323
|
+
var sliceIndex
|
|
324
|
+
end on
|
|
325
|
+
|
|
326
|
+
on note
|
|
327
|
+
group_off(EVENT_ID, ALL_GROUPS)
|
|
328
|
+
|
|
329
|
+
sliceIndex = 0
|
|
330
|
+
var start = zone_slice_start(zoneID, sliceIndex)
|
|
331
|
+
var length = zone_slice_length(zoneID, sliceIndex)
|
|
332
|
+
|
|
333
|
+
while length
|
|
334
|
+
sliceEvent = new_note(EVENT_KEY, EVENT_VEL, start, -1)
|
|
335
|
+
group_on(sliceEvent, 0)
|
|
336
|
+
inc(sliceIndex)
|
|
337
|
+
|
|
338
|
+
var sliceEnd = zone_slice_start(zoneID, sliceIndex)
|
|
339
|
+
if sliceEnd < 0 ' end of loop
|
|
340
|
+
sliceIndex = 0
|
|
341
|
+
sliceEnd = -sliceEnd
|
|
342
|
+
end if
|
|
343
|
+
fade_out_after(sliceEvent, sliceEnd - start - FADE_LEN, FADE_LEN)
|
|
344
|
+
|
|
345
|
+
delay(length) ' tempo-synced wait
|
|
346
|
+
|
|
347
|
+
fade_out(sliceEvent, FADE_LEN, 1)
|
|
348
|
+
start = zone_slice_start(zoneID, sliceIndex)
|
|
349
|
+
length = zone_slice_length(zoneID, sliceIndex)
|
|
350
|
+
end while
|
|
351
|
+
end on
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Velocity Curve
|
|
355
|
+
|
|
356
|
+
Scale incoming velocities with a custom curve.
|
|
357
|
+
|
|
358
|
+
### Simple 3-point curve
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
on init
|
|
362
|
+
prop_linear Velocity_Min(1, 127, 1) = 1
|
|
363
|
+
prop_linear Velocity_Mid(1, 127, 1) = 64
|
|
364
|
+
prop_linear Velocity_Max(1, 127, 1) = 127
|
|
365
|
+
end on
|
|
366
|
+
|
|
367
|
+
on note
|
|
368
|
+
var vel = EVENT_VEL
|
|
369
|
+
if vel < 64
|
|
370
|
+
vel = Velocity_Min + (vel * (Velocity_Mid - Velocity_Min)) / 64
|
|
371
|
+
else
|
|
372
|
+
vel = Velocity_Mid + ((vel - 63) * (Velocity_Max - Velocity_Mid)) / 64
|
|
373
|
+
end if
|
|
374
|
+
set_vel(EVENT_ID, vel)
|
|
375
|
+
end on
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Full 127-point lookup table
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
on init
|
|
382
|
+
prop_array Velocity_Curve[128](1, 127, 1)
|
|
383
|
+
var vel = 1
|
|
384
|
+
while vel < 128
|
|
385
|
+
Velocity_Curve[vel] = vel
|
|
386
|
+
inc(vel)
|
|
387
|
+
end while
|
|
388
|
+
end on
|
|
389
|
+
|
|
390
|
+
on note
|
|
391
|
+
set_vel(EVENT_ID, Velocity_Curve[EVENT_VEL])
|
|
392
|
+
end on
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Global LFO (Mod Source Shared Across Groups)
|
|
396
|
+
|
|
397
|
+
Convert a group's mod source output to a MIDI CC so other groups and effects can use it.
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
on init
|
|
401
|
+
var globalModEvent = TAG_1 ' no event yet
|
|
402
|
+
end on
|
|
403
|
+
|
|
404
|
+
on note
|
|
405
|
+
' ... manage held notes ...
|
|
406
|
+
if numHeldNotes = 1 ' first note
|
|
407
|
+
fade_out(globalModEvent, 0, 1)
|
|
408
|
+
globalModEvent = new_note(EVENT_KEY, EVENT_VEL, 0, 0)
|
|
409
|
+
group_off(globalModEvent, ALL_GROUPS)
|
|
410
|
+
group_on(globalModEvent, 0) ' group 0 has the LFO → CC99 routing
|
|
411
|
+
end if
|
|
412
|
+
group_off(EVENT_ID, 0) ' normal notes skip group 0
|
|
413
|
+
end on
|
|
414
|
+
|
|
415
|
+
on controller
|
|
416
|
+
if EVENT_CC = 99
|
|
417
|
+
ignore_controller() ' block external CC99
|
|
418
|
+
end if
|
|
419
|
+
end on
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Held-Notes Stack (Reusable Pattern)
|
|
423
|
+
|
|
424
|
+
Most mono/arpeggiator scripts maintain a list of currently held MIDI keys:
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
on init
|
|
428
|
+
const MAX_HELD = 32
|
|
429
|
+
var heldNotes[MAX_HELD]
|
|
430
|
+
var numHeldNotes
|
|
431
|
+
var i
|
|
432
|
+
end on
|
|
433
|
+
|
|
434
|
+
function removeFromHeldNotes
|
|
435
|
+
i = 0
|
|
436
|
+
while i < numHeldNotes
|
|
437
|
+
if heldNotes[i] = EVENT_KEY
|
|
438
|
+
dec(numHeldNotes)
|
|
439
|
+
while i < numHeldNotes
|
|
440
|
+
heldNotes[i] = heldNotes[i + 1]
|
|
441
|
+
inc(i)
|
|
442
|
+
end while
|
|
443
|
+
return
|
|
444
|
+
end if
|
|
445
|
+
inc(i)
|
|
446
|
+
end while
|
|
447
|
+
end function
|
|
448
|
+
|
|
449
|
+
on note
|
|
450
|
+
removeFromHeldNotes() ' in case duplicate (sustain pedal, etc.)
|
|
451
|
+
heldNotes[numHeldNotes] = EVENT_KEY
|
|
452
|
+
inc(numHeldNotes)
|
|
453
|
+
end on
|
|
454
|
+
|
|
455
|
+
on release
|
|
456
|
+
removeFromHeldNotes()
|
|
457
|
+
end on
|
|
458
|
+
|
|
459
|
+
on controller
|
|
460
|
+
if EVENT_CC = 123
|
|
461
|
+
numHeldNotes = 0
|
|
462
|
+
end if
|
|
463
|
+
end on
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## MPE (MIDI Polyphonic Expression)
|
|
467
|
+
|
|
468
|
+
Enable per-note modulation by setting "Use MIDI Channel" on mod routings. Handle RPN 6 for MPE configuration:
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
on init
|
|
472
|
+
var mpeMasterChannel = -1
|
|
473
|
+
prop_linear Pitch_Bend_Range(0, 96, 1) = 48
|
|
474
|
+
end on
|
|
475
|
+
|
|
476
|
+
on controller
|
|
477
|
+
if EVENT_CC = CC_PITCHBEND and EVENT_CHAN = mpeMasterChannel
|
|
478
|
+
' Master pitch bend → apply to all voices via CC
|
|
479
|
+
set_controller_hd(0, 100, Master_Bend_Range * CC[CC_PITCHBEND], 8192 * 12)
|
|
480
|
+
ignore_controller()
|
|
481
|
+
end if
|
|
482
|
+
end on
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Piano Behaviours (Voice Stealing)
|
|
486
|
+
|
|
487
|
+
Intelligent voice stealing based on velocity and decay:
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
' Track all playing notes with their trigger time
|
|
491
|
+
' When max polyphony reached, steal the quietest:
|
|
492
|
+
' quietness = (current_time - note_time) * decay_rate[key]
|
|
493
|
+
' Prefer stealing a repeated key over a different note
|
|
494
|
+
```
|